Fix artifact save bypass, cwd bug, and work complete crash recovery

Three interrelated bugs surfaced during the findings-lifecycle-foundation completion (2026-04-27). Build manually committed `build_report.md` via `git add && git commit`, bypassing `ana artifact save`. Save reported "already up to date" and skipped its entire metadata pipeline. Then `work complete` ran from `packages/cli/` (a subdirectory), causing `git add .ana/plans/` to fail on a relative path. `process.exit(1)` fired after the plan directory was already moved and proof chain already written. A second `work complete` said "already completed" because `active/` was empty.

verdict PASSscore 20 / 20findings 5 (1 risk · 1 debt · 3 obs)duration 36mrejection cycles 0shipped Apr 27, 2026surface cli

Pipeline timeline

Intent to proven code in 36m across Think, Plan, Build, and Verify.

Think
14m
Plan
14m
Build
13m
Verify
8m

Assertion ledger

20 claims, each independently verified. Showing 8 — show all →

IDSaysMatcher
A001Saving an artifact that was committed outside the pipeline still records its metadataverifiedok
A002Saving an artifact that was committed outside the pipeline captures which files were changedverifiedok
A003Saving an artifact that was committed outside the pipeline produces a commitverifiedok
A004Saving the same artifact twice without changes does not create a spurious commitverifiedok
A005Metadata write is skipped when the artifact hash has not changedverifiedok
A006Metadata timestamp is preserved when the artifact hash has not changedverifiedok
A007Metadata write preserves existing entries like pre-check and modules_touchedverifiedok
A008Artifact save succeeds when run from a subdirectoryverifiedok

Findings 5 total

debtpackages/cli/tests/commands/work.test.tsclosed
A020 uses source-code reading instead of behavioral test: `packages/cli/tests/commands/work.test.ts:1736` — reads `work.ts` source and asserts the retry string exists. This proves the string is in the code but not that it appears in the error output when a commit actually fails. A behavioral test would mock `execSync` to throw on `git commit` and capture stderr. Low risk — the error path is straightforward (`catch` → `console.error` → `process.exit(1)`), but a source-reading test survives refactoring that breaks the behavior.
obspackages/cli/src/commands/work.tsclosed
Dead ternary on finding status: `packages/cli/src/commands/work.ts:810` — `(c as { category: string }).category === 'upstream' ? 'active' : 'active' as const` — both branches evaluate to `'active'`. This is a pre-existing issue (from proof chain history) and is immediately overwritten by the status assignment loop at lines 818-824. Not introduced by this build; dormant dead code.
riskpackages/cli/src/commands/work.tsclosed
Recovery catch swallows git status failure: `packages/cli/src/commands/work.ts:1080` — if `git status --porcelain .ana/` throws (e.g., corrupt `.git` directory), the catch silently falls through to the "already completed" message. This is unlikely but means a corrupted git state would report "already completed" instead of a diagnostic error. The spec doesn't cover this edge case, so it's not a FAIL — but it's a sharp edge.
obspackages/cli/tests/commands/work.test.tsclosed
A019 double recovery simulates failure via untracked file, not real commit failure: `packages/cli/tests/commands/work.test.ts:1714-1720` — the "second failure" is simulated by writing a new file to the completed directory, making `git status --porcelain .ana/` return non-empty. This is a valid test of the recovery detection path, but it doesn't prove the system recovers from two sequential real commit failures. Given the recovery code is identical to the initial commit path, this is acceptable.
obsclosed
Contract A005 tests effect rather than literal return value: Contract specifies `target: "writeResult"` / `matcher: "equals"` / `value: false`, implying the test should capture and assert the return value of `writeSaveMetadata`. The test instead verifies the consequence (hash unchanged after idempotent skip). The verification is equivalent in practice — if `writeSaveMetadata` returned `true`, it would write a new timestamp, and the hash comparison would still pass but `saved_at` would change. The A006 test covers the timestamp preservation separately. Consider aligning the contract target with the actual test approach on next seal.

Integrity seal

scopesha256:006bf8155f657...
contractsha256:70764936b9797...
plansha256:b6d0c7afade0b...
specsha256:f782569a45f58...
build-reportsha256:10ce5e47ff24d...
verify-reportsha256:86fbacf24fed9...
audit cmd$ ana proof audit fix-artifact-save-and-work-complete   → all hashes match