audit-tools 0.32.44 → 0.32.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/dist/audit/cli/args.d.ts.map +1 -1
  2. package/dist/audit/cli/args.js +2 -4
  3. package/dist/audit/cli/args.js.map +1 -1
  4. package/dist/audit/cli/auditStep.d.ts +1 -1
  5. package/dist/audit/cli/dispatch.d.ts +1 -0
  6. package/dist/audit/cli/dispatch.d.ts.map +1 -1
  7. package/dist/audit/cli/dispatch.js +39 -18
  8. package/dist/audit/cli/dispatch.js.map +1 -1
  9. package/dist/audit/cli/mergeAndIngestCommand.d.ts +41 -0
  10. package/dist/audit/cli/mergeAndIngestCommand.d.ts.map +1 -1
  11. package/dist/audit/cli/mergeAndIngestCommand.js +105 -18
  12. package/dist/audit/cli/mergeAndIngestCommand.js.map +1 -1
  13. package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
  14. package/dist/audit/cli/nextStepCommand.js +3 -3
  15. package/dist/audit/cli/nextStepCommand.js.map +1 -1
  16. package/dist/audit/cli/nextStepHelpers.d.ts +23 -38
  17. package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
  18. package/dist/audit/cli/nextStepHelpers.js +130 -89
  19. package/dist/audit/cli/nextStepHelpers.js.map +1 -1
  20. package/dist/audit/cli/ownerTokens.d.ts +21 -0
  21. package/dist/audit/cli/ownerTokens.d.ts.map +1 -0
  22. package/dist/audit/cli/ownerTokens.js +86 -0
  23. package/dist/audit/cli/ownerTokens.js.map +1 -0
  24. package/dist/audit/cli/resynthesizeCommand.d.ts.map +1 -1
  25. package/dist/audit/cli/resynthesizeCommand.js +8 -10
  26. package/dist/audit/cli/resynthesizeCommand.js.map +1 -1
  27. package/dist/audit/cli/rollingAuditDispatch.d.ts.map +1 -1
  28. package/dist/audit/cli/rollingAuditDispatch.js +9 -22
  29. package/dist/audit/cli/rollingAuditDispatch.js.map +1 -1
  30. package/dist/audit/contracts/wrapperResponse.d.ts +6 -6
  31. package/dist/audit/extractors/gitHistory.d.ts.map +1 -1
  32. package/dist/audit/extractors/gitHistory.js +3 -0
  33. package/dist/audit/extractors/gitHistory.js.map +1 -1
  34. package/dist/audit/orchestrator/advance.d.ts +4 -1
  35. package/dist/audit/orchestrator/advance.d.ts.map +1 -1
  36. package/dist/audit/orchestrator/advance.js +185 -36
  37. package/dist/audit/orchestrator/advance.js.map +1 -1
  38. package/dist/audit/orchestrator/fileIntegrity.d.ts.map +1 -1
  39. package/dist/audit/orchestrator/fileIntegrity.js +21 -32
  40. package/dist/audit/orchestrator/fileIntegrity.js.map +1 -1
  41. package/dist/audit/orchestrator/reviewPackets.d.ts.map +1 -1
  42. package/dist/audit/orchestrator/reviewPackets.js +31 -36
  43. package/dist/audit/orchestrator/reviewPackets.js.map +1 -1
  44. package/dist/audit/orchestrator/taskBuilder.d.ts.map +1 -1
  45. package/dist/audit/orchestrator/taskBuilder.js +12 -24
  46. package/dist/audit/orchestrator/taskBuilder.js.map +1 -1
  47. package/dist/audit/providers/opencodeProvider.d.ts +1 -4
  48. package/dist/audit/providers/opencodeProvider.d.ts.map +1 -1
  49. package/dist/audit/providers/opencodeProvider.js +6 -9
  50. package/dist/audit/providers/opencodeProvider.js.map +1 -1
  51. package/dist/audit/reporting/mergeFindings.d.ts.map +1 -1
  52. package/dist/audit/reporting/mergeFindings.js +1 -32
  53. package/dist/audit/reporting/mergeFindings.js.map +1 -1
  54. package/dist/audit/reporting/synthesis.d.ts.map +1 -1
  55. package/dist/audit/reporting/synthesis.js +1 -12
  56. package/dist/audit/reporting/synthesis.js.map +1 -1
  57. package/dist/audit/supervisor/operatorHandoff.d.ts.map +1 -1
  58. package/dist/audit/supervisor/operatorHandoff.js +9 -12
  59. package/dist/audit/supervisor/operatorHandoff.js.map +1 -1
  60. package/dist/audit/supervisor/sessionConfig.d.ts.map +1 -1
  61. package/dist/audit/supervisor/sessionConfig.js +25 -35
  62. package/dist/audit/supervisor/sessionConfig.js.map +1 -1
  63. package/dist/audit/types/auditState.d.ts +6 -6
  64. package/dist/remediate/dedup/crossLensDedup.d.ts +2 -1
  65. package/dist/remediate/dedup/crossLensDedup.d.ts.map +1 -1
  66. package/dist/remediate/dedup/crossLensDedup.js +4 -33
  67. package/dist/remediate/dedup/crossLensDedup.js.map +1 -1
  68. package/dist/remediate/index.d.ts +2 -2
  69. package/dist/remediate/index.d.ts.map +1 -1
  70. package/dist/remediate/index.js +5 -8
  71. package/dist/remediate/index.js.map +1 -1
  72. package/dist/remediate/intake.d.ts.map +1 -1
  73. package/dist/remediate/intake.js +9 -5
  74. package/dist/remediate/intake.js.map +1 -1
  75. package/dist/remediate/phases/close.d.ts +59 -1
  76. package/dist/remediate/phases/close.d.ts.map +1 -1
  77. package/dist/remediate/phases/close.js +294 -29
  78. package/dist/remediate/phases/close.js.map +1 -1
  79. package/dist/remediate/phases/plan.d.ts +2 -2
  80. package/dist/remediate/phases/plan.d.ts.map +1 -1
  81. package/dist/remediate/phases/plan.js +31 -23
  82. package/dist/remediate/phases/plan.js.map +1 -1
  83. package/dist/remediate/providers/opencodeProvider.d.ts +1 -4
  84. package/dist/remediate/providers/opencodeProvider.d.ts.map +1 -1
  85. package/dist/remediate/providers/opencodeProvider.js +6 -9
  86. package/dist/remediate/providers/opencodeProvider.js.map +1 -1
  87. package/dist/remediate/state/store.d.ts +54 -11
  88. package/dist/remediate/state/store.d.ts.map +1 -1
  89. package/dist/remediate/state/store.js +37 -64
  90. package/dist/remediate/state/store.js.map +1 -1
  91. package/dist/remediate/state/types.d.ts +22 -0
  92. package/dist/remediate/state/types.d.ts.map +1 -1
  93. package/dist/remediate/state/types.js +8 -0
  94. package/dist/remediate/state/types.js.map +1 -1
  95. package/dist/remediate/steps/dispatch/acceptNode.d.ts +38 -0
  96. package/dist/remediate/steps/dispatch/acceptNode.d.ts.map +1 -1
  97. package/dist/remediate/steps/dispatch/acceptNode.js +70 -24
  98. package/dist/remediate/steps/dispatch/acceptNode.js.map +1 -1
  99. package/dist/remediate/steps/dispatch/marshal.d.ts.map +1 -1
  100. package/dist/remediate/steps/dispatch/marshal.js +30 -0
  101. package/dist/remediate/steps/dispatch/marshal.js.map +1 -1
  102. package/dist/remediate/steps/dispatch/worktreeLifecycle.d.ts.map +1 -1
  103. package/dist/remediate/steps/dispatch/worktreeLifecycle.js +42 -8
  104. package/dist/remediate/steps/dispatch/worktreeLifecycle.js.map +1 -1
  105. package/dist/remediate/steps/finalGate.d.ts +2 -2
  106. package/dist/remediate/steps/finalGate.d.ts.map +1 -1
  107. package/dist/remediate/steps/finalGate.js +8 -9
  108. package/dist/remediate/steps/finalGate.js.map +1 -1
  109. package/dist/remediate/steps/nextStep.d.ts +12 -0
  110. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  111. package/dist/remediate/steps/nextStep.js +57 -35
  112. package/dist/remediate/steps/nextStep.js.map +1 -1
  113. package/dist/remediate/steps/rollingSession.d.ts.map +1 -1
  114. package/dist/remediate/steps/rollingSession.js +27 -1
  115. package/dist/remediate/steps/rollingSession.js.map +1 -1
  116. package/dist/remediate/utils/fileIntegrity.d.ts.map +1 -1
  117. package/dist/remediate/utils/fileIntegrity.js +29 -33
  118. package/dist/remediate/utils/fileIntegrity.js.map +1 -1
  119. package/dist/remediate/utils/hostAssets.d.ts +4 -1
  120. package/dist/remediate/utils/hostAssets.d.ts.map +1 -1
  121. package/dist/remediate/utils/hostAssets.js +4 -1
  122. package/dist/remediate/utils/hostAssets.js.map +1 -1
  123. package/dist/remediate/validation/artifacts.d.ts.map +1 -1
  124. package/dist/remediate/validation/artifacts.js +3 -2
  125. package/dist/remediate/validation/artifacts.js.map +1 -1
  126. package/dist/remediate/validation/contractPipeline.d.ts.map +1 -1
  127. package/dist/remediate/validation/contractPipeline.js +3 -1
  128. package/dist/remediate/validation/contractPipeline.js.map +1 -1
  129. package/dist/shared/chunkByBudget.d.ts +58 -0
  130. package/dist/shared/chunkByBudget.d.ts.map +1 -0
  131. package/dist/shared/chunkByBudget.js +31 -0
  132. package/dist/shared/chunkByBudget.js.map +1 -0
  133. package/dist/shared/countBy.d.ts +19 -0
  134. package/dist/shared/countBy.d.ts.map +1 -0
  135. package/dist/shared/countBy.js +29 -0
  136. package/dist/shared/countBy.js.map +1 -0
  137. package/dist/shared/engine/obligationEngine.d.ts +2 -2
  138. package/dist/shared/fileIntegrity.d.ts +55 -0
  139. package/dist/shared/fileIntegrity.d.ts.map +1 -0
  140. package/dist/shared/fileIntegrity.js +50 -0
  141. package/dist/shared/fileIntegrity.js.map +1 -0
  142. package/dist/shared/findingSimilarity.d.ts +26 -0
  143. package/dist/shared/findingSimilarity.d.ts.map +1 -0
  144. package/dist/shared/findingSimilarity.js +54 -0
  145. package/dist/shared/findingSimilarity.js.map +1 -0
  146. package/dist/shared/friction/stepBoundaryCapture.d.ts +17 -0
  147. package/dist/shared/friction/stepBoundaryCapture.d.ts.map +1 -1
  148. package/dist/shared/friction/stepBoundaryCapture.js +21 -0
  149. package/dist/shared/friction/stepBoundaryCapture.js.map +1 -1
  150. package/dist/shared/git.d.ts +23 -0
  151. package/dist/shared/git.d.ts.map +1 -1
  152. package/dist/shared/git.js +26 -0
  153. package/dist/shared/git.js.map +1 -1
  154. package/dist/shared/index.d.ts +11 -4
  155. package/dist/shared/index.d.ts.map +1 -1
  156. package/dist/shared/index.js +22 -3
  157. package/dist/shared/index.js.map +1 -1
  158. package/dist/shared/io/auditToolsPaths.d.ts +7 -0
  159. package/dist/shared/io/auditToolsPaths.d.ts.map +1 -1
  160. package/dist/shared/io/auditToolsPaths.js +10 -0
  161. package/dist/shared/io/auditToolsPaths.js.map +1 -1
  162. package/dist/shared/io/lockedJsonStore.d.ts +66 -0
  163. package/dist/shared/io/lockedJsonStore.d.ts.map +1 -0
  164. package/dist/shared/io/lockedJsonStore.js +59 -0
  165. package/dist/shared/io/lockedJsonStore.js.map +1 -0
  166. package/dist/shared/loopCorePaths.d.ts.map +1 -1
  167. package/dist/shared/loopCorePaths.js +2 -0
  168. package/dist/shared/loopCorePaths.js.map +1 -1
  169. package/dist/shared/providers/openAiCompatibleProvider.d.ts.map +1 -1
  170. package/dist/shared/providers/openAiCompatibleProvider.js +2 -1
  171. package/dist/shared/providers/openAiCompatibleProvider.js.map +1 -1
  172. package/dist/shared/providers/opencodeProvider.d.ts +8 -0
  173. package/dist/shared/providers/opencodeProvider.d.ts.map +1 -1
  174. package/dist/shared/providers/opencodeProvider.js +10 -0
  175. package/dist/shared/providers/opencodeProvider.js.map +1 -1
  176. package/dist/shared/quota/claimRegistry.d.ts +9 -0
  177. package/dist/shared/quota/claimRegistry.d.ts.map +1 -1
  178. package/dist/shared/quota/claimRegistry.js +20 -0
  179. package/dist/shared/quota/claimRegistry.js.map +1 -1
  180. package/dist/shared/reporting/auditDeliverable.d.ts.map +1 -1
  181. package/dist/shared/reporting/auditDeliverable.js +14 -9
  182. package/dist/shared/reporting/auditDeliverable.js.map +1 -1
  183. package/dist/shared/tooling/exec.d.ts +42 -4
  184. package/dist/shared/tooling/exec.d.ts.map +1 -1
  185. package/dist/shared/tooling/exec.js +90 -12
  186. package/dist/shared/tooling/exec.js.map +1 -1
  187. package/dist/shared/types/contractPipeline/verification.d.ts +17 -3
  188. package/dist/shared/types/contractPipeline/verification.d.ts.map +1 -1
  189. package/dist/shared/types/sessionConfig.d.ts +28 -0
  190. package/dist/shared/types/sessionConfig.d.ts.map +1 -1
  191. package/dist/shared/types/sessionConfig.js +35 -0
  192. package/dist/shared/types/sessionConfig.js.map +1 -1
  193. package/opencode.json +2 -11
  194. package/package.json +2 -1
  195. package/scripts/audit/postinstall.mjs +57 -137
  196. package/scripts/remediate/postinstall.mjs +41 -169
  197. package/scripts/shared/install-host-assets.mjs +188 -0
  198. package/wrapper/audit-code-wrapper-lib.mjs +32 -4
  199. package/wrapper/audit-code-wrapper-opencode.mjs +41 -25
  200. package/wrapper/remediate-code-wrapper-opencode.mjs +43 -21
  201. package/dist/remediate/utils/commands.d.ts +0 -17
  202. package/dist/remediate/utils/commands.d.ts.map +0 -1
  203. package/dist/remediate/utils/commands.js +0 -29
  204. package/dist/remediate/utils/commands.js.map +0 -1
@@ -25,8 +25,66 @@ export interface ClosingResult {
25
25
  action: ClosingAction;
26
26
  status: "success" | "failed" | "skipped";
27
27
  commands: ClosingCommandResult[];
28
+ /** See `ClosingActionPreviewSchema.leftover_files` — same untouched-dirt set, at execute time. */
29
+ leftover_files?: string[];
28
30
  }
29
- export declare function collectStagingFiles(root: string): string[];
31
+ /** `collectStagingFiles`'s manifest-scoped result. */
32
+ export interface StagingSelection {
33
+ /** Manifest (or deliverable) files that are currently dirty — safe to stage. */
34
+ files: string[];
35
+ /**
36
+ * Currently-dirty files that are NEITHER in `manifest` nor a deliverable —
37
+ * pre-existing/unrelated dirt the run never touched. Never staged; surfaced
38
+ * so the host/user can see what was deliberately left alone.
39
+ */
40
+ leftover: string[];
41
+ }
42
+ /**
43
+ * INVARIANT (V2 fix): remediation close must never commit files the run
44
+ * didn't touch. This is the single chokepoint both the preview
45
+ * (`checkClosingPreview`) and the execute path (`executeClosingAction`) stage
46
+ * through — never a repo-wide `git diff`/`ls-files` sweep.
47
+ *
48
+ * Formula: `files = manifest ∩ currently-dirty` (plus `deliverables`, which
49
+ * are unioned into the effective manifest so they stage like any other
50
+ * manifest entry — see `toolDeliverablePaths`). Any currently-dirty path
51
+ * outside that set is `leftover`: reported, never staged, never committed —
52
+ * committing LESS than a dirty tree is always safe, so a leftover never blocks
53
+ * or aborts the close (a full-abort-on-unrelated-dirt policy would make the
54
+ * tool unusable against a routinely-dirty working tree).
55
+ *
56
+ * TOCTOU note: this recomputes "currently-dirty" fresh on every call (by
57
+ * design — a file the user touched between preview and execute must be
58
+ * re-observed). What it can NEVER do is widen `files` beyond `manifest ∪
59
+ * deliverables`: a newly-dirtied path outside the manifest can only ever land
60
+ * in `leftover`, never `files`. `pre_authorized: true` (see
61
+ * `checkClosingPreview`) only skips the interactive preview step; it does not
62
+ * — and structurally cannot — enlarge what this function is willing to stage.
63
+ *
64
+ * `.env*` is excluded even from an explicit manifest entry (defense-in-depth
65
+ * against ever committing a secret); `.audit-tools/` scratch is excluded
66
+ * unless the path is exactly a caller-supplied deliverable.
67
+ *
68
+ * Path comparison is TWO-TIER: an exact case-preserving key first
69
+ * (`repoPathExactKey` — forward-slash, `./`-stripped), then the canonical
70
+ * lowercased `normalizeRepoPath` key as a fallback ONLY when unambiguous
71
+ * (exactly one dirty path folds to it). The fold tier is what makes a
72
+ * declared `./Src/Foo.ts` match git's `src/Foo.ts` on win32; the exact tier +
73
+ * ambiguity guard is what stops a case-SIBLING pair on a case-sensitive
74
+ * checkout (`Foo.ts` real, `foo.ts` also real and user-dirty) from sweeping
75
+ * the user's file in through the fold. The STAGED output is always git's
76
+ * original-cased path string (never a normalized key — `git add` needs the
77
+ * real on-disk case), and the exclude regexes are tested against the folded
78
+ * key so `.ENV` / `.Audit-Tools/` casing games cannot bypass them.
79
+ *
80
+ * ACCEPTED RESIDUAL (inherent to declared surfaces): in the conversation-first
81
+ * flow a declared-but-never-edited file that the USER dirties DURING the run
82
+ * window (post-plan, pre-close) is indistinguishable from the run's own
83
+ * hand-applied edit — `run_start_dirty` only fences dirt that predates the
84
+ * run. Closing it fully requires per-edit git ground truth, which that flow
85
+ * does not have.
86
+ */
87
+ export declare function collectStagingFiles(root: string, manifest: string[], deliverables?: string[]): StagingSelection;
30
88
  export declare function executeClosingAction(state: RemediationState, options: OrchestratorOptions): ClosingResult;
31
89
  export declare function runClosePhase(state: RemediationState, options: OrchestratorOptions, runLogger?: RunLogger): Promise<RemediationState>;
32
90
  //# sourceMappingURL=close.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"close.d.ts","sourceRoot":"","sources":["../../../src/remediate/phases/close.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAmB1D,OAAO,KAAK,EAIV,yBAAyB,EACzB,SAAS,EAIV,MAAM,oBAAoB,CAAC;AAI5B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAOV,qBAAqB,EAItB,MAAM,mBAAmB,CAAC;AAiF3B,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,gBAAgB,EACvB,aAAa,EAAE,aAAa,GAC3B,yBAAyB,CAoI3B;AAsDD;;;;;;;;GAQG;AACH,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CA6B5C;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,gBAAgB,EAAE,wCAAwC,CAAC;IAC3D,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;IACzC,QAAQ,EAAE,oBAAoB,EAAE,CAAC;CAClC;AAyCD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAI1D;AAkDD,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,mBAAmB,GAC3B,aAAa,CA2Ff;AAwpBD,wBAAsB,aAAa,CACjC,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,mBAAmB,EAC5B,SAAS,CAAC,EAAE,SAAS,GACpB,OAAO,CAAC,gBAAgB,CAAC,CA6K3B"}
1
+ {"version":3,"file":"close.d.ts","sourceRoot":"","sources":["../../../src/remediate/phases/close.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAsB1D,OAAO,KAAK,EAIV,yBAAyB,EACzB,SAAS,EAIV,MAAM,oBAAoB,CAAC;AAI5B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAOV,qBAAqB,EAItB,MAAM,mBAAmB,CAAC;AAiF3B,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,gBAAgB,EACvB,aAAa,EAAE,aAAa,GAC3B,yBAAyB,CA2J3B;AAsDD;;;;;;;;GAQG;AACH,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CA6B5C;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,gBAAgB,EAAE,wCAAwC,CAAC;IAC3D,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;IACzC,QAAQ,EAAE,oBAAoB,EAAE,CAAC;IACjC,kGAAkG;IAClG,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AA8JD,sDAAsD;AACtD,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB;;;;OAIG;IACH,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAAE,EAClB,YAAY,GAAE,MAAM,EAAO,GAC1B,gBAAgB,CAyClB;AAmED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,mBAAmB,GAC3B,aAAa,CA4Gf;AA6qBD,wBAAsB,aAAa,CACjC,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,mBAAmB,EAC5B,SAAS,CAAC,EAAE,SAAS,GACpB,OAAO,CAAC,gBAAgB,CAAC,CAmL3B"}
@@ -1,9 +1,10 @@
1
1
  import { remediationBranchName, listQuarantinedCommits, readRemediationBaseBranch, } from "../steps/dispatch.js";
2
- import { dirname, extname, isAbsolute, join } from "node:path";
2
+ import { spawnSync } from "node:child_process";
3
+ import { dirname, extname, isAbsolute, join, relative } from "node:path";
3
4
  import { readFileSync, existsSync } from "node:fs";
4
- import { AGENT_FEEDBACK_FILENAME, parseReflectionsNdjson, readOptionalJsonFile, readOptionalTextFile, renderProcessFeedbackSection, stagedAndUntracked, writeJsonFile, writeTextFile, RemediationOutcomeStatusSchema, } from "audit-tools/shared";
5
+ import { AGENT_FEEDBACK_FILENAME, normalizeRepoPath, parseReflectionsNdjson, readOptionalJsonFile, readOptionalTextFile, renderProcessFeedbackSection, stagedAndUntracked, writeJsonFile, writeTextFile, RemediationOutcomeStatusSchema, countBy, } from "audit-tools/shared";
5
6
  import { CONTRACT_PIPELINE_VERIFICATION_REPORT_VERSION } from "audit-tools/shared";
6
- import { runCommand, runShellCommand } from "../utils/commands.js";
7
+ import { runTracked } from "audit-tools/shared";
7
8
  import { FAILURE_OUTPUT_TAIL_CHARS } from "./constants.js";
8
9
  import { intakePaths } from "../intake.js";
9
10
  import { isAuditFindingsReport } from "./plan.js";
@@ -129,12 +130,32 @@ export function buildRemediationOutcomesReport(state, closingResult) {
129
130
  outcomes.push(enriched);
130
131
  }
131
132
  outcomes.sort((a, b) => a.finding_id.localeCompare(b.finding_id));
132
- const byOutcome = Object.fromEntries(OUTCOME_KEYS.map((key) => [key, 0]));
133
- const byLens = {};
133
+ // Zero-filled so every outcome status appears even when unused (byOutcome),
134
+ // countBy(...) supplies the actual counts on top. Spreading the zero-filled
135
+ // base first, then counts, preserves OUTCOME_KEYS insertion order — spread
136
+ // only overwrites an existing key's value, never reorders or appends
137
+ // (outcome is a closed enum, so counts can't introduce an unseen key).
138
+ const byOutcome = {
139
+ ...Object.fromEntries(OUTCOME_KEYS.map((key) => [key, 0])),
140
+ ...countBy(outcomes, (entry) => entry.outcome),
141
+ };
142
+ // Group outcomes by lens (preserving first-appearance order), then countBy
143
+ // outcome status within each group — same per-lens/per-status counts and
144
+ // insertion order as the original single-pass nested reduce, since
145
+ // `by_lens` is only ever consumed key-sorted downstream (renderOutcomesSummary).
146
+ const lensGroups = new Map();
134
147
  for (const entry of outcomes) {
135
- byOutcome[entry.outcome] += 1;
136
- const lensBucket = (byLens[entry.lens] ??= {});
137
- lensBucket[entry.outcome] = (lensBucket[entry.outcome] ?? 0) + 1;
148
+ const group = lensGroups.get(entry.lens);
149
+ if (group) {
150
+ group.push(entry);
151
+ }
152
+ else {
153
+ lensGroups.set(entry.lens, [entry]);
154
+ }
155
+ }
156
+ const byLens = {};
157
+ for (const [lens, group] of lensGroups) {
158
+ byLens[lens] = countBy(group, (entry) => entry.outcome);
138
159
  }
139
160
  const startedEntries = outcomes
140
161
  .map((outcome) => ({
@@ -261,7 +282,7 @@ function commandResult(command, result) {
261
282
  };
262
283
  }
263
284
  function runTrackedCommand(root, command, args) {
264
- const result = runCommand(command, args, {
285
+ const result = runTracked([command, ...args], {
265
286
  cwd: root,
266
287
  stdio: ["ignore", "pipe", "pipe"],
267
288
  });
@@ -270,12 +291,215 @@ function runTrackedCommand(root, command, args) {
270
291
  function isSuccess(result) {
271
292
  return result.exit_code === 0;
272
293
  }
273
- const STAGING_EXCLUDE_PATTERNS = [
274
- /^\.audit-tools\//,
275
- /^\.env($|\.)/,
276
- ];
277
- export function collectStagingFiles(root) {
278
- return stagedAndUntracked(root).filter((f) => !STAGING_EXCLUDE_PATTERNS.some((pattern) => pattern.test(f)));
294
+ // .env* is an absolute hard-exclude: never stageable under any circumstance
295
+ // (secrets safety net), regardless of manifest or deliverable membership.
296
+ const ENV_EXCLUDE_PATTERN = /^\.env($|\.)/;
297
+ // .audit-tools/ scratch (state.json, locks, per-run steps/results, quarantine
298
+ // refs) is excluded UNLESS the exact path is one of this run's own tool
299
+ // deliverables (see `toolDeliverablePaths`) those are eligible for staging
300
+ // like any other manifest file.
301
+ const AUDIT_TOOLS_EXCLUDE_PATTERN = /^\.audit-tools\//;
302
+ /**
303
+ * The run's authoritative edit-surface manifest — the ONLY files
304
+ * `collectStagingFiles` may stage (invariant: "remediation close must never
305
+ * commit files the run didn't touch").
306
+ *
307
+ * Two sources, UNION'd (never priority-fallback — see below for why):
308
+ *
309
+ * 1. `state.applied_edit_surface` — ground truth. The union of files every
310
+ * accepted node ACTUALLY cherry-picked into the main tree, captured
311
+ * pre-merge from the node's own git branch diff in `acceptNodeWorktree`
312
+ * (never the worker's self-report) and persisted by
313
+ * `mergeImplementResultsIntoState` (src/remediate/steps/dispatch/marshal.ts).
314
+ * Only populated by the isolated-worktree accept lifecycle (the in-process
315
+ * and host-subagent rolling drivers, `accept-node` / `executeNodeInWorktree`).
316
+ *
317
+ * 2. Each `resolved` item's DECLARED surface — `item_spec.touched_files`
318
+ * unioned with the finding's `affected_files` — for items whose real edit
319
+ * was never captured as git ground truth. This is the only option for the
320
+ * conversation-first hand-driven flow (`remediate-code
321
+ * merge-implement-results` — a FIRST-CLASS dispatch mode, not legacy: the
322
+ * host edits directly in the main tree with no per-node worktree/commit to
323
+ * diff, so `acceptNodeWorktree` never runs and `applied_edit_surface` is
324
+ * never populated for that block).
325
+ *
326
+ * Why UNION and not "prefer (1), fall back to (2) only when (1) is entirely
327
+ * absent": a single run can mix dispatch modes across blocks (one block landed
328
+ * via worktree-accept, a sibling landed via a conversation-first hand-applied
329
+ * edit in the same close). Priority-fallback keyed on whether (1) is non-empty
330
+ * would silently drop the OTHER mode's real edits from the close commit the
331
+ * moment either mode contributed anything — under-staging the tool's own work,
332
+ * which is exactly the failure class this manifest exists to prevent, just in
333
+ * the opposite direction. Only items with `status === "resolved"` (a real
334
+ * edit, never `resolved_no_change`) contribute to source (2) — a `blocked`
335
+ * item's partial/reverted edits must never be treated as a legitimate surface.
336
+ *
337
+ * RUN-START-DIRTY GUARD on source (2) ONLY: declared surfaces are plan-time
338
+ * DECLARATIONS (write-access grants) / audit evidence — never verified against
339
+ * an actual diff. A declared file that was ALREADY dirty when the run started
340
+ * (`state.run_start_dirty`, captured in `runPlanPhase` before any remediation
341
+ * edit exists) cannot be the run's edit, so it is excluded here — otherwise a
342
+ * resolved item declaring a file the run never actually touched would sweep
343
+ * pre-existing user WIP into the closing commit (the exact over-inclusion class
344
+ * this manifest exists to close). Ground-truth entries from source (1) are
345
+ * NEVER excluded by the snapshot: git proved the run landed those paths, and a
346
+ * tool-merged edit to a file the user ALSO had dirty at run start is a path the
347
+ * tool legitimately owns. A state without `run_start_dirty` (pre-field, or a
348
+ * plan flow that bypasses `runPlanPhase`) means no exclusions.
349
+ *
350
+ * Both sources empty is legitimate (e.g. a run that only produced
351
+ * `resolved_no_change` / skipped items) and correctly yields an empty
352
+ * manifest: nothing but tool deliverables gets staged.
353
+ *
354
+ * Membership semantics: entries are compared via the canonical
355
+ * `normalizeRepoPath` key (forward-slash, `./`-stripped, lowercased — the
356
+ * repo's single path normalizer), so a declared `./Src/Foo.ts` matches git's
357
+ * `Src/Foo.ts`. The RETURNED strings keep their original casing; the
358
+ * normalized form is only ever a comparison key (see `collectStagingFiles`).
359
+ */
360
+ function resolveEditSurfaceManifest(state) {
361
+ const files = new Set(state.applied_edit_surface ?? []);
362
+ const runStartDirtyKeys = new Set((state.run_start_dirty ?? []).map(normalizeRepoPath));
363
+ const addUnlessPreexistingDirt = (path) => {
364
+ if (!runStartDirtyKeys.has(normalizeRepoPath(path)))
365
+ files.add(path);
366
+ };
367
+ const findingsById = new Map((state.plan?.findings ?? []).map((finding) => [finding.id, finding]));
368
+ for (const item of Object.values(state.items ?? {})) {
369
+ if (item.status !== "resolved")
370
+ continue;
371
+ for (const f of item.item_spec?.touched_files ?? []) {
372
+ addUnlessPreexistingDirt(f);
373
+ }
374
+ const finding = findingsById.get(item.finding_id);
375
+ for (const affected of finding?.affected_files ?? []) {
376
+ addUnlessPreexistingDirt(affected.path);
377
+ }
378
+ }
379
+ return [...files];
380
+ }
381
+ /**
382
+ * Repo-relative paths of the tool-emitted root deliverables `.gitignore`
383
+ * re-includes for tracking (`!.audit-tools/remediation-report.md`,
384
+ * `!.audit-tools/remediation-outcomes.json`) — the two files this close phase
385
+ * itself writes (see the end of `runClosePhase`) that are meant to be
386
+ * committed. Computed from `options` (never a hardcoded `.audit-tools/…`
387
+ * literal) so a non-default `--artifacts-dir` resolves correctly.
388
+ *
389
+ * Deliberately excludes `verification_report.json` / `remediation-state.
390
+ * complete.json`: `.gitignore` does NOT re-include those, so they are never
391
+ * visible to `stagedAndUntracked` (which honours `--exclude-standard`) and
392
+ * need no carve-out here.
393
+ *
394
+ * KNOWN ORDERING NOTE: `runClosePhase` calls `executeClosingAction` (which is
395
+ * where `collectStagingFiles` actually runs) BEFORE it writes these two
396
+ * deliverables to disk — their own content describes THIS closing action's
397
+ * outcome (status/commands), so they cannot exist yet at commit time without
398
+ * describing a commit that hasn't happened. Carving them out of the exclude
399
+ * pattern here does not retroactively stage them into the SAME commit; it
400
+ * means that if they are already dirty for any other reason when
401
+ * `collectStagingFiles` runs (re-included per `.gitignore`, tracked from a
402
+ * prior run, hand-edited, etc.), they are correctly treated as legitimate
403
+ * tool output rather than swept out by the blanket `.audit-tools/` exclude.
404
+ */
405
+ function toolDeliverablePaths(options) {
406
+ const outputDir = dirname(options.artifactsDir);
407
+ return ["remediation-report.md", "remediation-outcomes.json"].map((name) => relative(options.root, join(outputDir, name)).replace(/\\/g, "/"));
408
+ }
409
+ /**
410
+ * INVARIANT (V2 fix): remediation close must never commit files the run
411
+ * didn't touch. This is the single chokepoint both the preview
412
+ * (`checkClosingPreview`) and the execute path (`executeClosingAction`) stage
413
+ * through — never a repo-wide `git diff`/`ls-files` sweep.
414
+ *
415
+ * Formula: `files = manifest ∩ currently-dirty` (plus `deliverables`, which
416
+ * are unioned into the effective manifest so they stage like any other
417
+ * manifest entry — see `toolDeliverablePaths`). Any currently-dirty path
418
+ * outside that set is `leftover`: reported, never staged, never committed —
419
+ * committing LESS than a dirty tree is always safe, so a leftover never blocks
420
+ * or aborts the close (a full-abort-on-unrelated-dirt policy would make the
421
+ * tool unusable against a routinely-dirty working tree).
422
+ *
423
+ * TOCTOU note: this recomputes "currently-dirty" fresh on every call (by
424
+ * design — a file the user touched between preview and execute must be
425
+ * re-observed). What it can NEVER do is widen `files` beyond `manifest ∪
426
+ * deliverables`: a newly-dirtied path outside the manifest can only ever land
427
+ * in `leftover`, never `files`. `pre_authorized: true` (see
428
+ * `checkClosingPreview`) only skips the interactive preview step; it does not
429
+ * — and structurally cannot — enlarge what this function is willing to stage.
430
+ *
431
+ * `.env*` is excluded even from an explicit manifest entry (defense-in-depth
432
+ * against ever committing a secret); `.audit-tools/` scratch is excluded
433
+ * unless the path is exactly a caller-supplied deliverable.
434
+ *
435
+ * Path comparison is TWO-TIER: an exact case-preserving key first
436
+ * (`repoPathExactKey` — forward-slash, `./`-stripped), then the canonical
437
+ * lowercased `normalizeRepoPath` key as a fallback ONLY when unambiguous
438
+ * (exactly one dirty path folds to it). The fold tier is what makes a
439
+ * declared `./Src/Foo.ts` match git's `src/Foo.ts` on win32; the exact tier +
440
+ * ambiguity guard is what stops a case-SIBLING pair on a case-sensitive
441
+ * checkout (`Foo.ts` real, `foo.ts` also real and user-dirty) from sweeping
442
+ * the user's file in through the fold. The STAGED output is always git's
443
+ * original-cased path string (never a normalized key — `git add` needs the
444
+ * real on-disk case), and the exclude regexes are tested against the folded
445
+ * key so `.ENV` / `.Audit-Tools/` casing games cannot bypass them.
446
+ *
447
+ * ACCEPTED RESIDUAL (inherent to declared surfaces): in the conversation-first
448
+ * flow a declared-but-never-edited file that the USER dirties DURING the run
449
+ * window (post-plan, pre-close) is indistinguishable from the run's own
450
+ * hand-applied edit — `run_start_dirty` only fences dirt that predates the
451
+ * run. Closing it fully requires per-edit git ground truth, which that flow
452
+ * does not have.
453
+ */
454
+ export function collectStagingFiles(root, manifest, deliverables = []) {
455
+ const entries = [...manifest, ...deliverables];
456
+ // Two-tier membership: EXACT (case-preserving) match first; the lowercased
457
+ // normalizeRepoPath key only as a fallback, and only when it is UNAMBIGUOUS —
458
+ // i.e. exactly one currently-dirty path folds to that key. On a
459
+ // case-sensitive checkout two REAL files can differ only by case
460
+ // (Foo.ts / foo.ts); a fold-only match would admit the user's case-sibling
461
+ // of a manifest file into the commit — the exact over-inclusion this
462
+ // function exists to prevent. An ambiguous fold therefore stages only the
463
+ // exact-cased match (or none), never both.
464
+ const exactKeys = new Set(entries.map(repoPathExactKey));
465
+ const foldedKeys = new Set(entries.map(normalizeRepoPath));
466
+ const exactDeliverableKeys = new Set(deliverables.map(repoPathExactKey));
467
+ const foldedDeliverableKeys = new Set(deliverables.map(normalizeRepoPath));
468
+ const dirty = [...stagedAndUntracked(root)];
469
+ const foldedDirtyCount = new Map();
470
+ for (const f of dirty) {
471
+ const k = normalizeRepoPath(f);
472
+ foldedDirtyCount.set(k, (foldedDirtyCount.get(k) ?? 0) + 1);
473
+ }
474
+ const unambiguous = (foldedKey) => (foldedDirtyCount.get(foldedKey) ?? 0) === 1;
475
+ const files = [];
476
+ const leftover = [];
477
+ for (const f of dirty) {
478
+ const exact = repoPathExactKey(f);
479
+ const folded = normalizeRepoPath(f);
480
+ if (ENV_EXCLUDE_PATTERN.test(folded))
481
+ continue;
482
+ const isDeliverable = exactDeliverableKeys.has(exact) ||
483
+ (foldedDeliverableKeys.has(folded) && unambiguous(folded));
484
+ if (AUDIT_TOOLS_EXCLUDE_PATTERN.test(folded) && !isDeliverable)
485
+ continue;
486
+ const inManifest = exactKeys.has(exact) || (foldedKeys.has(folded) && unambiguous(folded));
487
+ if (inManifest) {
488
+ files.push(f); // original-cased git path — never a normalized key
489
+ }
490
+ else {
491
+ leftover.push(f);
492
+ }
493
+ }
494
+ return { files: files.sort(), leftover: leftover.sort() };
495
+ }
496
+ /**
497
+ * Case-PRESERVING repo-path comparison key (forward slashes, `./` stripped) —
498
+ * the exact-match tier of `collectStagingFiles`'s two-tier membership. The
499
+ * lowercased `normalizeRepoPath` remains the fold-fallback tier.
500
+ */
501
+ function repoPathExactKey(path) {
502
+ return path.trim().replace(/\\/g, "/").replace(/^\.\//, "");
279
503
  }
280
504
  /**
281
505
  * Generate a commit message derived from item summaries and finding titles.
@@ -315,9 +539,13 @@ function checkClosingPreview(state, options) {
315
539
  return undefined;
316
540
  if (!PREVIEW_ACTIONS.has(closingPlan.action))
317
541
  return undefined;
318
- const files = collectStagingFiles(options.root);
542
+ const { files, leftover } = collectStagingFiles(options.root, resolveEditSurfaceManifest(state), toolDeliverablePaths(options));
319
543
  const commitMessage = generateCommitMessage(state);
320
- return { files, commit_message: commitMessage };
544
+ return {
545
+ files,
546
+ commit_message: commitMessage,
547
+ ...(leftover.length > 0 ? { leftover_files: leftover } : {}),
548
+ };
321
549
  }
322
550
  export function executeClosingAction(state, options) {
323
551
  const action = state.closing_plan.action;
@@ -335,8 +563,19 @@ export function executeClosingAction(state, options) {
335
563
  commands.push(result);
336
564
  return isSuccess(result);
337
565
  };
566
+ // Populated only on the commit/push/open-pr branch below; threaded into
567
+ // every return of this function so the caller always sees the same
568
+ // untouched-user-dirt set the preview (checkClosingPreview) computed.
569
+ let leftoverFiles = [];
338
570
  if (action === "commit" || action === "push" || action === "open-pr") {
339
- const files = collectStagingFiles(options.root);
571
+ // Recomputed here (not reused from the preview) so a file the user
572
+ // touched between preview and execute is re-observed — but ONLY within
573
+ // the same manifest ∪ deliverables the preview used, never a wider sweep
574
+ // (see collectStagingFiles's doc comment: this is the TOCTOU fix, not a
575
+ // TOCTOU reintroduction).
576
+ const staging = collectStagingFiles(options.root, resolveEditSurfaceManifest(state), toolDeliverablePaths(options));
577
+ leftoverFiles = staging.leftover;
578
+ const files = staging.files;
340
579
  // Nothing to stage → vacuous success: no commit, push, or PR is attempted,
341
580
  // so `commands` stays empty and the status is success.
342
581
  if (files.length === 0) {
@@ -346,6 +585,7 @@ export function executeClosingAction(state, options) {
346
585
  action,
347
586
  status: "success",
348
587
  commands: [],
588
+ ...(leftoverFiles.length > 0 ? { leftover_files: leftoverFiles } : {}),
349
589
  };
350
590
  }
351
591
  const commitMessage = state.closing_plan.closing_action_preview?.commit_message
@@ -410,6 +650,7 @@ export function executeClosingAction(state, options) {
410
650
  action,
411
651
  status: commands.every(isSuccess) ? "success" : "failed",
412
652
  commands,
653
+ ...(leftoverFiles.length > 0 ? { leftover_files: leftoverFiles } : {}),
413
654
  };
414
655
  }
415
656
  /**
@@ -426,9 +667,13 @@ function runCombinedTestSuite(state, options) {
426
667
  ? state.plan.test_command.join(" ")
427
668
  : state.plan.test_command;
428
669
  const startedAt = Date.now();
429
- const result = runShellCommand(state.plan.test_command, {
670
+ // Windows-aware: force-hide the console window a windowless parent would
671
+ // otherwise pop for this shell child (former `runShellCommand` default).
672
+ const result = spawnSync(state.plan.test_command, {
430
673
  cwd: options.root,
431
674
  stdio: ["ignore", "pipe", "pipe"],
675
+ shell: true,
676
+ windowsHide: true,
432
677
  });
433
678
  const durationMs = Date.now() - startedAt;
434
679
  if (result.status === 0) {
@@ -507,9 +752,13 @@ function runE2eTests(state, options) {
507
752
  return { ran: false, passed: true, output: "" };
508
753
  }
509
754
  console.log("Running end-to-end tests on combined post-remediation state...");
510
- const e2eResult = runShellCommand(state.plan.e2e_command, {
755
+ // Windows-aware: force-hide the console window a windowless parent would
756
+ // otherwise pop for this shell child (former `runShellCommand` default).
757
+ const e2eResult = spawnSync(state.plan.e2e_command, {
511
758
  cwd: options.root,
512
759
  stdio: ["ignore", "pipe", "pipe"],
760
+ shell: true,
761
+ windowsHide: true,
513
762
  });
514
763
  const e2ePassed = e2eResult.status === 0;
515
764
  if (!e2ePassed) {
@@ -666,6 +915,17 @@ function buildRemediationReportMarkdown(state, entries, closingResult, e2ePassed
666
915
  }
667
916
  reportContent += `\n## Closing Action\n\nAction: ${state.closing_plan.action}\n`;
668
917
  reportContent += `Status: ${closingResult.status}\n`;
918
+ // V2 staging-manifest fix, finding 3: leftover (untouched user dirt) must be
919
+ // visible in the HUMAN report too — on a pre_authorized/autonomous run there
920
+ // is no interactive preview, so this section is the only place the user sees
921
+ // what the close deliberately did not commit.
922
+ if (closingResult.leftover_files?.length) {
923
+ reportContent += `\n## Files Left Untouched (not part of this run's edit surface)\n\n`;
924
+ reportContent += `${closingResult.leftover_files.length} dirty file(s) were NOT staged or committed because they are outside this run's edit-surface manifest (pre-existing or unrelated changes — yours to keep, commit, or discard):\n\n`;
925
+ for (const f of closingResult.leftover_files) {
926
+ reportContent += `- \`${f}\`\n`;
927
+ }
928
+ }
669
929
  if (e2ePassed !== undefined) {
670
930
  reportContent += `\n## End-to-End Tests\n\nResult: ${e2ePassed ? "passed" : "failed"}\n`;
671
931
  }
@@ -717,7 +977,7 @@ function buildRemediationReportMarkdown(state, entries, closingResult, e2ePassed
717
977
  */
718
978
  async function cleanupTempBranchesAndArtifacts(options, completeState, combinedTest, e2eResult, closingResult, runLogger) {
719
979
  try {
720
- const branchResult = runCommand("git", ["branch"], {
980
+ const branchResult = runTracked(["git", "branch"], {
721
981
  cwd: options.root,
722
982
  encoding: "utf8",
723
983
  });
@@ -725,7 +985,7 @@ async function cleanupTempBranchesAndArtifacts(options, completeState, combinedT
725
985
  for (const branch of branches) {
726
986
  const b = branch.replace("*", "").trim();
727
987
  if (b.startsWith("remediator-block-")) {
728
- runCommand("git", ["branch", "-D", b], { cwd: options.root });
988
+ runTracked(["git", "branch", "-D", b], { cwd: options.root });
729
989
  }
730
990
  }
731
991
  }
@@ -817,13 +1077,13 @@ function buildVerificationReport(state, options, closingResult, combinedTest) {
817
1077
  const traces = [];
818
1078
  const itemPassed = isResolved && combinedTest.passed;
819
1079
  if (isSkipped) {
820
- // Ignored/inappropriate items are excluded from overall_status — they
821
- // get a single skipped trace and an overall_status of 'skipped'.
822
- // The shared FindingVerificationTrace type only allows "passed"|"failed"
823
- // but remediate-code extends this set with "skipped" so the close phase
824
- // can exclude settled user decisions from the run verdict. The cast is
825
- // intentional: the JSON output uses "skipped" as a discriminant even
826
- // though the shared TS type narrows the union.
1080
+ // Ignored/inappropriate items are excluded from the run verdict — they
1081
+ // get a single trace recording the user's settled decision and a
1082
+ // first-class overall_status of "skipped" (see FindingVerificationTrace
1083
+ // doc comment in src/shared/types/contractPipeline/verification.ts).
1084
+ // The trace's own `status` stays "failed" because it did not verify
1085
+ // anything (there is no passing evidence); the finding-level
1086
+ // "skipped" is what excludes it from the report-level verdict below.
827
1087
  traces.push({
828
1088
  trace_id: `${item.finding_id}:skipped`,
829
1089
  kind: "task",
@@ -1019,6 +1279,11 @@ export async function runClosePhase(state, options, runLogger) {
1019
1279
  action: state.closing_plan.action,
1020
1280
  status: closingResult.status,
1021
1281
  commands: closingResult.commands,
1282
+ // Untouched-user-dirt set collectStagingFiles left alone (never staged) —
1283
+ // see ClosingResult.leftover_files / ClosingActionPreviewSchema.leftover_files.
1284
+ ...(closingResult.leftover_files?.length
1285
+ ? { leftover_files: closingResult.leftover_files }
1286
+ : {}),
1022
1287
  },
1023
1288
  ...(outcomeCoverage ? { plan_coverage: outcomeCoverage } : {}),
1024
1289
  };