Skip to content

feat(query): resolve --pipeline names client-side in audit/snapshot (v1.6.2)#12

Merged
alexskatell merged 1 commit into
mainfrom
feat-query-pipeline-name-resolver
May 14, 2026
Merged

feat(query): resolve --pipeline names client-side in audit/snapshot (v1.6.2)#12
alexskatell merged 1 commit into
mainfrom
feat-query-pipeline-name-resolver

Conversation

@alexskatell
Copy link
Copy Markdown
Contributor

Why

query audit and query snapshot in v1.6.1 only accept opaque 20-char pipeline IDs. Passing a human-readable name (e.g. 'Sales - Flex - Triage') silently returned rows: [] because the literal string got jammed into WHERE pipeline_id = '...', which never matches.

Live failure trace (Paul, 2026-05-14):

  1. topline --agent query doctor
  2. topline --agent query audit --pipeline 'Sales - Flex - Triage' --since this-week-etrows: []
  3. topline --agent query sql --sql "SELECT id, name FROM pipelines WHERE LOWER(name) LIKE '%flex%' OR LOWER(name) LIKE '%triage%'" — the agent correctly diagnoses "wrong identifier" and writes raw SQL to translate the name
  4. topline --agent query audit --pipeline bna6e9DoPgRchNsjeYS3 --since this-week-et — now it works

That third call is the loophole shape: a primitive gap that forces a raw-SQL wrapper round-trip the skill rules can't bind. Per the auto-memory rule of thumb that landed during the earlier audit-contract work — after three failed skill iterations, build the primitive instead of writing another rule — we're moving the lookup into the CLI rather than tightening pitfall #N+1.

What

  • New resolvePipelineID() runs before the audit/snapshot SQL:
    • Opaque 20-char ID (e.g. CLUy1QapsrEeBiNrmQiL) passes through unchanged.
    • Otherwise tokens are AND'd as case-insensitive LIKEs against pipelines.name.
    • 1 match → use it; surface pipelineResolution: {input, matchedId, matchedName, matchedBy: "name"} in the JSON envelope.
    • 0 matches → error lists every available pipeline (Available pipelines: A (id), B (id), …).
    • >1 matches → error lists the candidates and asks for a more specific name.
  • Audit and snapshot JSON gain pipelineResolution, so agents (and humans) see exactly how --pipeline was matched.
  • Skill docs (skills/hermes/SKILL.md + skills/claude-code/SKILL.md) bumped to v1.6.2:
    • --pipeline now reads as PIPELINE_ID_OR_NAME.
    • New banned-in-default-flow rule: don't run raw query sql to resolve a pipeline name to its ID — query audit/snapshot handle it.
    • Pitfall #17 documents the v1.6.1 failure trace and the v1.6.2 fix.

Tests

  • TestQueryAuditResolvesPipelineByName'flex triage' resolves to the Triage opaque ID; pipelineResolution.matchedId/matchedName/matchedBy are echoed; audit SQL uses the resolved ID.
  • TestQueryAuditUnknownPipelineListsCandidates'no such pipeline' errors with the available-pipelines list.
  • TestQueryAuditAmbiguousPipelineErrors'flex' errors listing both Flex pipelines with an "ambiguous" hint.
  • TestQuerySnapshotResolvesPipelineByName — same resolver path for query snapshot.
  • All existing tests still pass; go build ./... clean; full go test ./... green.

Expected agent trace after this lands

For What activity happened this week in our flex triage pipeline?:

skill_view topline-os-cli
skill_view topline-os-crm-audits
topline --agent query doctor
topline --agent query audit --pipeline 'flex triage' --since this-week-et --status open
answer (citing pipelineResolution.matchedName: "Sales - Flex - Triage")

No intermediate query sql, no python3 -<<'PY', no bash heredoc. The third tool call is gone.

Non-goals

  • Doesn't change the on-the-wire query API or os-mcp views.
  • Doesn't add bind-parameter support to query sql (separate, larger change).
  • Doesn't try to fuzzy-match opaque IDs against names — IDs always pass through.

…v1.6.2)

Why
- v1.6.1 query audit / query snapshot only accept opaque pipeline IDs.
  Passing a name (e.g. 'Sales - Flex - Triage') silently returned
  rows: [], pushing the agent to drop into raw query sql to look up the
  ID and then re-run audit. That's the v1.6.1 -> v1.6.2 loophole shape:
  primitive forces a wrapper round-trip the skill rules can't bind.

What
- New resolvePipelineID() runs before the audit/snapshot SQL:
  - Opaque 20-char ID (e.g. CLUy1QapsrEeBiNrmQiL) passes through.
  - Otherwise tokens are AND'd as case-insensitive LIKEs against the
    pipelines table.
  - 1 match  -> use it, surface matchedId/matchedName/matchedBy.
  - 0 match  -> error lists every available pipeline.
  - >1 match -> error lists the candidates.
- Audit JSON gains pipelineResolution so the agent (and humans) see how
  the input was matched. matchedBy = "id" | "name".
- Skill docs (hermes + claude-code) bumped to v1.6.2:
  - --pipeline now reads PIPELINE_ID_OR_NAME.
  - New banned-in-default-flow rule: don't run raw query sql to resolve
    a pipeline name to its ID.
  - Pitfall #17 documents the v1.6.1 failure trace and the v1.6.2 fix.

Tests
- TestQueryAuditResolvesPipelineByName: 'flex triage' -> Triage ID,
  resolution echo'd in JSON, audit SQL uses opaque ID.
- TestQueryAuditUnknownPipelineListsCandidates: 'no such pipeline'
  errors with the candidate list.
- TestQueryAuditAmbiguousPipelineErrors: 'flex' errors with both
  Flex pipelines and an "ambiguous" hint.
- TestQuerySnapshotResolvesPipelineByName: same resolver path for
  query snapshot.

All resolver and existing tests pass; go build ./... clean.
@alexskatell alexskatell merged commit e015e84 into main May 14, 2026
1 check passed
@alexskatell alexskatell deleted the feat-query-pipeline-name-resolver branch May 14, 2026 17:32
alexskatell added a commit that referenced this pull request May 14, 2026
PR #12's selectIDNamePairs assumed rows came back as positional arrays
([]any aligned with `columns`), but the live hosted warehouse query API
returns rows as column-keyed maps (map[string]any with "id" and "name"
keys). The unit-test fixture also returned positional arrays, so the
mismatch slipped through — verified against prod by running:

  topline --agent query audit --pipeline 'flex triage' ...

which returned `no pipeline matched ... Available pipelines: (none found)`
even though `SELECT id, name FROM pipelines` returned rows fine.

Fix
- selectIDNamePairs now switches on row type:
  - map[string]any: read row["id"] / row["name"] by column name.
  - []any: read by positional index (preserved for parity with any
    deployment that returns array rows).
- Test fixture in pipelineLookupServer rewritten to emit the real
  column-keyed object shape ({"id":"...","name":"..."}), so the
  resolver tests now exercise the actual wire format.

Verified live: `topline --agent query audit --pipeline 'flex triage' ...`
resolves to bna6e9DoPgRchNsjeYS3 (Sales - Flex - Triage) and runs the
audit cleanly.

All existing resolver tests still pass.

Co-authored-by: Alex Skatell <alex@topline.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant