From a34cb55ad71823d4f8209c9a7a9d4c470b184416 Mon Sep 17 00:00:00 2001 From: billbenetti Date: Sat, 7 Nov 2026 05:04:32 -0800 Subject: [PATCH] fix(cli): add --full to rig workflow trace so a long trail comes back complete (#2656) (#2667) rig workflow trace gains --full, which passes ?full=true to the daemon's existing GET /api/workflow//trace route; there is no new daemon route. Without the flag the request has no query, as before. --json prints the daemon payload unchanged. Human output adds one line naming the --full command when the trail came back with trailCompleteness recent. docs/as-built/cli-reference.md lists the option. --- docs/as-built/cli-reference.md | 2 +- packages/cli/src/commands/workflow.ts | 14 ++++++-- packages/cli/test/workflow.test.ts | 51 +++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/docs/as-built/cli-reference.md b/docs/as-built/cli-reference.md index a66169d47..6b3caf4a4 100644 --- a/docs/as-built/cli-reference.md +++ b/docs/as-built/cli-reference.md @@ -600,7 +600,7 @@ Herdr session per request, including session-bound previews and window reuse. | `rig workflow specs` | — | `--json` | | `rig workflow guidance ` | — | `--packet `
`--component `
`--full`
`--json` | | `rig workflow show ` | — | `--json` | -| `rig workflow trace ` | — | `--json` | +| `rig workflow trace ` | — | `--full`
`--json` | | `rig workflow continue ` | — | `--json` | | `rig workflow run ` | — | `--root-objective ` **required**
`--created-by ` **required**
`--entry-owner `
`--rig `
`--project `
`--mission `
`--slice `
`--tags `
`--json` | | `rig workflow watch ` | — | `--json` | diff --git a/packages/cli/src/commands/workflow.ts b/packages/cli/src/commands/workflow.ts index 99762df68..5514be540 100644 --- a/packages/cli/src/commands/workflow.ts +++ b/packages/cli/src/commands/workflow.ts @@ -639,17 +639,22 @@ exception routes only. Existing obligations keep their owners and evidence. cmd .command("trace ") .description("Show one workflow instance + its append-only step trail (audit-only verdict)") + .option("--full", "Return the complete step trail instead of the 50 most recent closures (trailCompleteness: complete)") .option("--json", "JSON output for agents") .addHelpText("after", ` +The default trail holds the 50 most recent closures; trailCompleteness says +"recent" when older ones were left out. --full asks the daemon for all of them. + Examples: $ rig workflow trace WF01ABC $ rig workflow trace WF01ABC --json | jq '.trail[] | {step, actor, exit}' + $ rig workflow trace WF01ABC --full --json | jq '.trailCompleteness, (.trail | length)' `) - .action(async (instanceId: string, opts: { json?: boolean }) => { + .action(async (instanceId: string, opts: { json?: boolean; full?: boolean }) => { const deps = getDeps(); await withClient(deps, async (client) => { - const res = await client.get<{ instance?: unknown; reconciliation?: unknown; trail?: unknown[]; frontier?: unknown[]; failures?: unknown[]; boundaryObligations?: unknown[]; unknowns?: string[] }>( - `/api/workflow/${encodeURIComponent(instanceId)}/trace`, + const res = await client.get<{ instance?: unknown; reconciliation?: unknown; trail?: unknown[]; trailCompleteness?: string; frontier?: unknown[]; failures?: unknown[]; boundaryObligations?: unknown[]; unknowns?: string[] }>( + `/api/workflow/${encodeURIComponent(instanceId)}/trace${opts.full ? "?full=true" : ""}`, ); // WF3 FR-2: human mode renders the per-step tree (mini-req 2's // one-screen bar); --json byte-identical (BR-2). @@ -667,6 +672,9 @@ Examples: } as Parameters[0]; const trail = (res.data.trail ?? []) as Parameters[1]; for (const line of renderTraceTree(instance, trail)) console.log(line); + if (res.data.trailCompleteness === "recent") { + console.log(` (the ${trail.length} most recent closures; older ones left out - all of them: rig workflow trace ${instanceId} --full)`); + } }); }); diff --git a/packages/cli/test/workflow.test.ts b/packages/cli/test/workflow.test.ts index b7829aec4..b34628a4c 100644 --- a/packages/cli/test/workflow.test.ts +++ b/packages/cli/test/workflow.test.ts @@ -331,6 +331,57 @@ describe("rig workflow CLI (PL-004 Phase D)", () => { expect(calls.find((c) => c.path === "/api/workflow/inst-1/trace")).toBeDefined(); }); + it("trace without --full keeps the daemon's default (no ?full=true)", async () => { + const { deps, calls } = makeDeps({ + routes: { "GET /api/workflow/inst-1/trace": { status: 200, data: { instance: {}, trail: [], trailCompleteness: "recent" } } }, + }); + const program = createProgram({ workflowDeps: deps }); + program.exitOverride(); + await program.parseAsync(["node", "rig", "workflow", "trace", "inst-1", "--json"]); + expect(calls.map((c) => c.path)).toEqual(["/api/workflow/inst-1/trace"]); + }); + + it("trace --full passes ?full=true through and prints the complete trail", async () => { + const trail = Array.from({ length: 52 }, (_, i) => ({ stepId: `s${i}`, actorSession: "a@r", closureReason: "done" })); + const { deps, calls } = makeDeps({ + routes: { "GET /api/workflow/inst-1/trace?full=true": { status: 200, data: { instance: {}, trail, trailCompleteness: "complete" } } }, + }); + const logs: string[] = []; + const spy = vi.spyOn(console, "log").mockImplementation((...a: unknown[]) => { logs.push(a.join(" ")); }); + try { + const program = createProgram({ workflowDeps: deps }); + program.exitOverride(); + await program.parseAsync(["node", "rig", "workflow", "trace", "inst-1", "--full", "--json"]); + } finally { + spy.mockRestore(); + } + expect(calls.map((c) => c.path)).toEqual(["/api/workflow/inst-1/trace?full=true"]); + const out = JSON.parse(logs.join("\n")) as { trail: unknown[]; trailCompleteness: string }; + expect(out.trailCompleteness).toBe("complete"); + expect(out.trail).toHaveLength(52); + }); + + it("trace in human mode says when the trail was cut and how to get all of it", async () => { + const { deps } = makeDeps({ + routes: { + "GET /api/workflow/inst-1/trace": { + status: 200, + data: { instance: { instanceId: "inst-1", status: "active" }, trail: [], trailCompleteness: "recent" }, + }, + }, + }); + const logs: string[] = []; + const spy = vi.spyOn(console, "log").mockImplementation((...a: unknown[]) => { logs.push(a.join(" ")); }); + try { + const program = createProgram({ workflowDeps: deps }); + program.exitOverride(); + await program.parseAsync(["node", "rig", "workflow", "trace", "inst-1"]); + } finally { + spy.mockRestore(); + } + expect(logs.join("\n")).toContain("rig workflow trace inst-1 --full"); + }); + it("continue POSTs /api/workflow/:instanceId/continue", async () => { const { deps, calls } = makeDeps({ routes: { "POST /api/workflow/inst-1/continue": { status: 200, data: { instance: {}, trail: [] } } },