From f6cf3464da5c00c0a7b43faf431c076ddb224507 Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Fri, 24 Jul 2026 19:09:03 -0400 Subject: [PATCH] feat(agents-audit): land deferred generate display behaviors (META-157) Two CLI behaviors deferred from the 0.4.4 release (PR #19, gate G2) because they change exit-code semantics and were out of that release's approved scope: - `generate --check --dry-run` now fires the drift gate (exit 1) instead of the dry-run branch silently winning and exiting 0. The JSON projection is still printed under --dry-run. - `generate` now surfaces the pre-existing `invalidFileMoved` result field: when a previous workspace.json was invalid and moved aside, the CLI reports where and that manual evidence could not be recovered. Regression tests watched red against pristine main's cli.ts (2 failing / 9 passing), then green after the implementation (11/11). Recovered from the triple-preserved 2026-07-22 snapshot (preserve/vr-639-640-worktree-2026-07-22 tag + docs/audits/worktree-reconciliation/2026-07-22/preservation/tracked-changes.patch). --- packages/agents-audit/CHANGELOG.md | 7 +++ .../agents-audit/src/cli.integration.test.ts | 44 +++++++++++++++++++ packages/agents-audit/src/cli.ts | 13 ++++-- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/packages/agents-audit/CHANGELOG.md b/packages/agents-audit/CHANGELOG.md index ef96af3..09de684 100644 --- a/packages/agents-audit/CHANGELOG.md +++ b/packages/agents-audit/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [Unreleased] + +### Patch Changes + +- `generate --check --dry-run` now fires the drift gate (exit 1, "manual evidence is untouched") instead of the dry-run branch silently winning and exiting 0; the JSON projection is still printed under `--dry-run`. Deferred from 0.4.4 (META-157) because it changes exit-code semantics; landed here as its own reviewed change with regression tests watched red against the pre-change CLI. +- `generate` now surfaces `invalidFileMoved` (result data that has existed since 0.4.3 but was never displayed): when a previous `.agents/workspace.json` was invalid and moved aside, the CLI prints where it was relocated to and that its manual evidence could not be recovered. + ## [0.4.4] - Unreleased ### Patch Changes diff --git a/packages/agents-audit/src/cli.integration.test.ts b/packages/agents-audit/src/cli.integration.test.ts index 5487bae..ad78450 100644 --- a/packages/agents-audit/src/cli.integration.test.ts +++ b/packages/agents-audit/src/cli.integration.test.ts @@ -111,6 +111,50 @@ describe('CLI integration', () => { logSpy.mockRestore(); }); + it('still fails the drift gate when --check is combined with --dry-run', async () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + mocks.generateWorkspaceJson.mockResolvedValueOnce({ + path: '/repo/.agents/workspace.json', + written: false, + skipped: false, + drift: true, + preservedManual: true, + content: { staged: true }, + }); + + const exitCode = await runCli(['node', 'agents-audit', 'generate', '/repo', '--check', '--dry-run']); + + expect(exitCode).toBe(1); + const errorCalls = (errorSpy as unknown as { mock: { calls: unknown[][] } }).mock.calls; + expect(errorCalls.flat().join(' ')).toContain('manual evidence is untouched'); + expect(logSpy).toHaveBeenCalledWith(JSON.stringify({ staged: true }, null, 2)); + errorSpy.mockRestore(); + logSpy.mockRestore(); + }); + + it('surfaces the relocated invalid file when --force recovers a fresh generate', async () => { + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + mocks.generateWorkspaceJson.mockResolvedValueOnce({ + path: '/repo/.agents/workspace.json', + written: true, + skipped: false, + drift: true, + preservedManual: false, + invalidFileMoved: '/repo/.agents/workspace.json.invalid.2026-01-01T00-00-00-000Z', + content: {}, + }); + + const exitCode = await runCli(['node', 'agents-audit', 'generate', '/repo', '--force']); + + expect(exitCode).toBe(0); + const logCalls = (logSpy as unknown as { mock: { calls: unknown[][] } }).mock.calls; + const logs = logCalls.flat().join(' '); + expect(logs).toContain('Generated /repo/.agents/workspace.json'); + expect(logs).toContain('/repo/.agents/workspace.json.invalid.2026-01-01T00-00-00-000Z'); + logSpy.mockRestore(); + }); + it('treats --dir as an invalid option', async () => { const exitCode = await runCli(['node', 'agents-audit', 'scan', '--dir', '/tmp/example']); diff --git a/packages/agents-audit/src/cli.ts b/packages/agents-audit/src/cli.ts index 9153abb..9736dd2 100644 --- a/packages/agents-audit/src/cli.ts +++ b/packages/agents-audit/src/cli.ts @@ -110,17 +110,24 @@ export async function runCli(argv: string[] = process.argv): Promise { }); spinner.stop(); - if (options.dryRun) { - console.log(JSON.stringify(result.content, null, 2)); - } else if (options.check) { + if (options.check) { if (result.drift) { console.error(`Generated sections are stale at ${result.path}; manual evidence is untouched. Run: agents-audit generate ${path}`); exitCode = 1; } else { console.log(`Generated sections are current at ${result.path}`); } + if (options.dryRun) { + console.log(JSON.stringify(result.content, null, 2)); + } + } else if (options.dryRun) { + console.log(JSON.stringify(result.content, null, 2)); } else if (result.skipped) { console.log(`Generated sections already current at ${result.path}; manual evidence preserved`); + } else if (result.invalidFileMoved) { + console.log(`Generated ${result.path}`); + console.log(pc.yellow(` Previous file was invalid and has been moved aside: ${result.invalidFileMoved}`)); + console.log(pc.yellow(' Manual evidence from the previous file was not recovered (it could not be parsed/validated).')); } else { console.log(`Generated ${result.path}`); }