@sun-asterisk/sungen 3.3.0-alpha.1 → 3.3.0-alpha.10

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 (463) hide show
  1. package/dist/capabilities/discover.d.ts +4 -0
  2. package/dist/capabilities/discover.d.ts.map +1 -1
  3. package/dist/capabilities/discover.js +16 -2
  4. package/dist/capabilities/discover.js.map +1 -1
  5. package/dist/cli/case-triage-hint.d.ts +14 -0
  6. package/dist/cli/case-triage-hint.d.ts.map +1 -0
  7. package/dist/cli/case-triage-hint.js +23 -0
  8. package/dist/cli/case-triage-hint.js.map +1 -0
  9. package/dist/cli/commands/audit.js +1 -1
  10. package/dist/cli/commands/audit.js.map +1 -1
  11. package/dist/cli/commands/contract-derive-command.d.ts +4 -2
  12. package/dist/cli/commands/contract-derive-command.d.ts.map +1 -1
  13. package/dist/cli/commands/contract-derive-command.js +19 -5
  14. package/dist/cli/commands/contract-derive-command.js.map +1 -1
  15. package/dist/cli/commands/contract.d.ts +3 -0
  16. package/dist/cli/commands/contract.d.ts.map +1 -1
  17. package/dist/cli/commands/contract.js +40 -7
  18. package/dist/cli/commands/contract.js.map +1 -1
  19. package/dist/cli/commands/delivery.d.ts.map +1 -1
  20. package/dist/cli/commands/delivery.js +59 -18
  21. package/dist/cli/commands/delivery.js.map +1 -1
  22. package/dist/cli/commands/gate.d.ts.map +1 -1
  23. package/dist/cli/commands/gate.js +2 -1
  24. package/dist/cli/commands/gate.js.map +1 -1
  25. package/dist/cli/commands/generate.d.ts.map +1 -1
  26. package/dist/cli/commands/generate.js +14 -2
  27. package/dist/cli/commands/generate.js.map +1 -1
  28. package/dist/cli/commands/journey.d.ts.map +1 -1
  29. package/dist/cli/commands/journey.js +6 -4
  30. package/dist/cli/commands/journey.js.map +1 -1
  31. package/dist/cli/commands/next.d.ts +7 -2
  32. package/dist/cli/commands/next.d.ts.map +1 -1
  33. package/dist/cli/commands/next.js +41 -10
  34. package/dist/cli/commands/next.js.map +1 -1
  35. package/dist/cli/commands/triage.d.ts +2 -1
  36. package/dist/cli/commands/triage.d.ts.map +1 -1
  37. package/dist/cli/commands/triage.js +45 -3
  38. package/dist/cli/commands/triage.js.map +1 -1
  39. package/dist/cli/commands/update.d.ts.map +1 -1
  40. package/dist/cli/commands/update.js +6 -2
  41. package/dist/cli/commands/update.js.map +1 -1
  42. package/dist/cli/index.js +5 -1
  43. package/dist/cli/index.js.map +1 -1
  44. package/dist/cli/json-error-on-exit.d.ts +34 -0
  45. package/dist/cli/json-error-on-exit.d.ts.map +1 -0
  46. package/dist/cli/json-error-on-exit.js +63 -0
  47. package/dist/cli/json-error-on-exit.js.map +1 -0
  48. package/dist/cli/project-sungen-invocation.d.ts +13 -0
  49. package/dist/cli/project-sungen-invocation.d.ts.map +1 -0
  50. package/dist/cli/project-sungen-invocation.js +95 -0
  51. package/dist/cli/project-sungen-invocation.js.map +1 -0
  52. package/dist/cli/run-command-hints.d.ts +9 -0
  53. package/dist/cli/run-command-hints.d.ts.map +1 -0
  54. package/dist/cli/run-command-hints.js +84 -0
  55. package/dist/cli/run-command-hints.js.map +1 -0
  56. package/dist/cli/waiting-for-you-block.d.ts +18 -0
  57. package/dist/cli/waiting-for-you-block.d.ts.map +1 -0
  58. package/dist/cli/waiting-for-you-block.js +22 -0
  59. package/dist/cli/waiting-for-you-block.js.map +1 -0
  60. package/dist/contract/accept-opaque-steps.d.ts +3 -0
  61. package/dist/contract/accept-opaque-steps.d.ts.map +1 -0
  62. package/dist/contract/accept-opaque-steps.js +114 -0
  63. package/dist/contract/accept-opaque-steps.js.map +1 -0
  64. package/dist/contract/approve-contract.d.ts +8 -1
  65. package/dist/contract/approve-contract.d.ts.map +1 -1
  66. package/dist/contract/approve-contract.js +6 -0
  67. package/dist/contract/approve-contract.js.map +1 -1
  68. package/dist/contract/build-contract-verdict.js +2 -2
  69. package/dist/contract/build-contract-verdict.js.map +1 -1
  70. package/dist/contract/build-trace-conformance.d.ts +5 -1
  71. package/dist/contract/build-trace-conformance.d.ts.map +1 -1
  72. package/dist/contract/build-trace-conformance.js +12 -14
  73. package/dist/contract/build-trace-conformance.js.map +1 -1
  74. package/dist/contract/classify-contract-change.d.ts +3 -2
  75. package/dist/contract/classify-contract-change.d.ts.map +1 -1
  76. package/dist/contract/classify-contract-change.js.map +1 -1
  77. package/dist/contract/classify-trace-conformance.d.ts +18 -3
  78. package/dist/contract/classify-trace-conformance.d.ts.map +1 -1
  79. package/dist/contract/classify-trace-conformance.js +64 -17
  80. package/dist/contract/classify-trace-conformance.js.map +1 -1
  81. package/dist/contract/contract-schema.d.ts +21 -0
  82. package/dist/contract/contract-schema.d.ts.map +1 -1
  83. package/dist/contract/contract-schema.js +25 -3
  84. package/dist/contract/contract-schema.js.map +1 -1
  85. package/dist/contract/contracts-lock-file.d.ts.map +1 -1
  86. package/dist/contract/contracts-lock-file.js +1 -0
  87. package/dist/contract/contracts-lock-file.js.map +1 -1
  88. package/dist/contract/derive-free-feature.d.ts +19 -0
  89. package/dist/contract/derive-free-feature.d.ts.map +1 -1
  90. package/dist/contract/derive-free-feature.js +40 -2
  91. package/dist/contract/derive-free-feature.js.map +1 -1
  92. package/dist/contract/project-contract-blockers.d.ts.map +1 -1
  93. package/dist/contract/project-contract-blockers.js +4 -2
  94. package/dist/contract/project-contract-blockers.js.map +1 -1
  95. package/dist/contract/record-contract-approval.d.ts +16 -1
  96. package/dist/contract/record-contract-approval.d.ts.map +1 -1
  97. package/dist/contract/record-contract-approval.js +29 -1
  98. package/dist/contract/record-contract-approval.js.map +1 -1
  99. package/dist/contract/render-free-gherkin.d.ts +2 -0
  100. package/dist/contract/render-free-gherkin.d.ts.map +1 -1
  101. package/dist/contract/render-free-gherkin.js +6 -0
  102. package/dist/contract/render-free-gherkin.js.map +1 -1
  103. package/dist/contract/resolve-unit-trace-identities.d.ts +12 -0
  104. package/dist/contract/resolve-unit-trace-identities.d.ts.map +1 -1
  105. package/dist/contract/resolve-unit-trace-identities.js +33 -49
  106. package/dist/contract/resolve-unit-trace-identities.js.map +1 -1
  107. package/dist/contract/trace/collect-trace-evidence.d.ts +16 -5
  108. package/dist/contract/trace/collect-trace-evidence.d.ts.map +1 -1
  109. package/dist/contract/trace/collect-trace-evidence.js +101 -43
  110. package/dist/contract/trace/collect-trace-evidence.js.map +1 -1
  111. package/dist/contract/trace/read-file-once.d.ts +20 -0
  112. package/dist/contract/trace/read-file-once.d.ts.map +1 -0
  113. package/dist/contract/trace/read-file-once.js +135 -0
  114. package/dist/contract/trace/read-file-once.js.map +1 -0
  115. package/dist/contract/trace/scenario-id-claims.d.ts +11 -0
  116. package/dist/contract/trace/scenario-id-claims.d.ts.map +1 -0
  117. package/dist/contract/trace/scenario-id-claims.js +89 -0
  118. package/dist/contract/trace/scenario-id-claims.js.map +1 -0
  119. package/dist/contract/trace/select-final-attempt.d.ts +4 -2
  120. package/dist/contract/trace/select-final-attempt.d.ts.map +1 -1
  121. package/dist/contract/trace/select-final-attempt.js +7 -4
  122. package/dist/contract/trace/select-final-attempt.js.map +1 -1
  123. package/dist/contract/trace/test-step-join.d.ts +3 -3
  124. package/dist/contract/trace/test-step-join.d.ts.map +1 -1
  125. package/dist/contract/trace/test-step-join.js +2 -2
  126. package/dist/contract/trace/test-step-join.js.map +1 -1
  127. package/dist/contract/trace/trace-binding.d.ts +3 -2
  128. package/dist/contract/trace/trace-binding.d.ts.map +1 -1
  129. package/dist/contract/trace/trace-binding.js +6 -3
  130. package/dist/contract/trace/trace-binding.js.map +1 -1
  131. package/dist/contract/trace/trace-file-reader.d.ts +14 -2
  132. package/dist/contract/trace/trace-file-reader.d.ts.map +1 -1
  133. package/dist/contract/trace/trace-file-reader.js +27 -45
  134. package/dist/contract/trace/trace-file-reader.js.map +1 -1
  135. package/dist/contract/trace/trace-freshness.d.ts +27 -0
  136. package/dist/contract/trace/trace-freshness.d.ts.map +1 -0
  137. package/dist/contract/trace/trace-freshness.js +146 -0
  138. package/dist/contract/trace/trace-freshness.js.map +1 -0
  139. package/dist/contract/trace/trace-identity-resolution.d.ts +2 -0
  140. package/dist/contract/trace/trace-identity-resolution.d.ts.map +1 -1
  141. package/dist/contract/trace/trace-identity-resolution.js +6 -3
  142. package/dist/contract/trace/trace-identity-resolution.js.map +1 -1
  143. package/dist/contract/trace/trace-run-record.d.ts +8 -3
  144. package/dist/contract/trace/trace-run-record.d.ts.map +1 -1
  145. package/dist/contract/trace/trace-run-record.js +42 -22
  146. package/dist/contract/trace/trace-run-record.js.map +1 -1
  147. package/dist/contract/trace/trace-steps.js +1 -1
  148. package/dist/contract/trace/trace-steps.js.map +1 -1
  149. package/dist/contract/trace/trace-zip-limits.d.ts +5 -1
  150. package/dist/contract/trace/trace-zip-limits.d.ts.map +1 -1
  151. package/dist/contract/trace/trace-zip-limits.js +6 -2
  152. package/dist/contract/trace/trace-zip-limits.js.map +1 -1
  153. package/dist/contract/verify-approval-decisions.d.ts.map +1 -1
  154. package/dist/contract/verify-approval-decisions.js +9 -4
  155. package/dist/contract/verify-approval-decisions.js.map +1 -1
  156. package/dist/decisions/confirmed-decision-commands.d.ts +11 -0
  157. package/dist/decisions/confirmed-decision-commands.d.ts.map +1 -1
  158. package/dist/decisions/confirmed-decision-commands.js +21 -3
  159. package/dist/decisions/confirmed-decision-commands.js.map +1 -1
  160. package/dist/decisions/decision-schema.d.ts +2 -1
  161. package/dist/decisions/decision-schema.d.ts.map +1 -1
  162. package/dist/decisions/decision-schema.js +2 -1
  163. package/dist/decisions/decision-schema.js.map +1 -1
  164. package/dist/decisions/git-head-trust.d.ts +12 -0
  165. package/dist/decisions/git-head-trust.d.ts.map +1 -1
  166. package/dist/decisions/git-head-trust.js +52 -0
  167. package/dist/decisions/git-head-trust.js.map +1 -1
  168. package/dist/decisions/human-confirmation.d.ts +2 -0
  169. package/dist/decisions/human-confirmation.d.ts.map +1 -1
  170. package/dist/decisions/human-confirmation.js +6 -0
  171. package/dist/decisions/human-confirmation.js.map +1 -1
  172. package/dist/decisions/legacy-waiver-migration.d.ts +8 -1
  173. package/dist/decisions/legacy-waiver-migration.d.ts.map +1 -1
  174. package/dist/decisions/legacy-waiver-migration.js +13 -4
  175. package/dist/decisions/legacy-waiver-migration.js.map +1 -1
  176. package/dist/exporters/csv-exporter.d.ts +11 -2
  177. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  178. package/dist/exporters/csv-exporter.js +8 -4
  179. package/dist/exporters/csv-exporter.js.map +1 -1
  180. package/dist/exporters/matrix/export.d.ts +6 -1
  181. package/dist/exporters/matrix/export.d.ts.map +1 -1
  182. package/dist/exporters/matrix/export.js +11 -4
  183. package/dist/exporters/matrix/export.js.map +1 -1
  184. package/dist/exporters/matrix/xlsx-cells.js +1 -1
  185. package/dist/exporters/matrix/xlsx-cells.js.map +1 -1
  186. package/dist/exporters/xlsx-exporter.d.ts +3 -2
  187. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  188. package/dist/exporters/xlsx-exporter.js +4 -4
  189. package/dist/exporters/xlsx-exporter.js.map +1 -1
  190. package/dist/graph/board-human-actions.d.ts +5 -0
  191. package/dist/graph/board-human-actions.d.ts.map +1 -0
  192. package/dist/graph/board-human-actions.js +83 -0
  193. package/dist/graph/board-human-actions.js.map +1 -0
  194. package/dist/graph/evaluate-graph.d.ts +21 -1
  195. package/dist/graph/evaluate-graph.d.ts.map +1 -1
  196. package/dist/graph/evaluate-graph.js +37 -9
  197. package/dist/graph/evaluate-graph.js.map +1 -1
  198. package/dist/graph/human-action.d.ts +21 -0
  199. package/dist/graph/human-action.d.ts.map +1 -0
  200. package/dist/graph/human-action.js +3 -0
  201. package/dist/graph/human-action.js.map +1 -0
  202. package/dist/graph/manual-triage-decisions.d.ts +19 -0
  203. package/dist/graph/manual-triage-decisions.d.ts.map +1 -0
  204. package/dist/graph/manual-triage-decisions.js +53 -0
  205. package/dist/graph/manual-triage-decisions.js.map +1 -0
  206. package/dist/graph/node-decl.d.ts +16 -0
  207. package/dist/graph/node-decl.d.ts.map +1 -1
  208. package/dist/graph/node-decl.js +4 -1
  209. package/dist/graph/node-decl.js.map +1 -1
  210. package/dist/graph/nodes/test-case-contract-nodes.d.ts +3 -1
  211. package/dist/graph/nodes/test-case-contract-nodes.d.ts.map +1 -1
  212. package/dist/graph/nodes/test-case-contract-nodes.js +22 -6
  213. package/dist/graph/nodes/test-case-contract-nodes.js.map +1 -1
  214. package/dist/graph/nodes/test-case-nodes.d.ts +3 -0
  215. package/dist/graph/nodes/test-case-nodes.d.ts.map +1 -1
  216. package/dist/graph/nodes/test-case-nodes.js +33 -4
  217. package/dist/graph/nodes/test-case-nodes.js.map +1 -1
  218. package/dist/graph/nodes/unit-audit-nodes.d.ts.map +1 -1
  219. package/dist/graph/nodes/unit-audit-nodes.js +1 -0
  220. package/dist/graph/nodes/unit-audit-nodes.js.map +1 -1
  221. package/dist/graph/nodes/unit-contract-node.d.ts +2 -1
  222. package/dist/graph/nodes/unit-contract-node.d.ts.map +1 -1
  223. package/dist/graph/nodes/unit-contract-node.js +16 -1
  224. package/dist/graph/nodes/unit-contract-node.js.map +1 -1
  225. package/dist/graph/nodes/unit-design-nodes.d.ts.map +1 -1
  226. package/dist/graph/nodes/unit-design-nodes.js +2 -1
  227. package/dist/graph/nodes/unit-design-nodes.js.map +1 -1
  228. package/dist/graph/test-case-results.d.ts +1 -0
  229. package/dist/graph/test-case-results.d.ts.map +1 -1
  230. package/dist/graph/test-case-results.js +27 -3
  231. package/dist/graph/test-case-results.js.map +1 -1
  232. package/dist/graph/test-case-satisfied.d.ts +5 -4
  233. package/dist/graph/test-case-satisfied.d.ts.map +1 -1
  234. package/dist/graph/test-case-satisfied.js +4 -0
  235. package/dist/graph/test-case-satisfied.js.map +1 -1
  236. package/dist/graph/test-case-state-shape.d.ts +7 -1
  237. package/dist/graph/test-case-state-shape.d.ts.map +1 -1
  238. package/dist/graph/unit-artifact-probes.d.ts.map +1 -1
  239. package/dist/graph/unit-artifact-probes.js +3 -1
  240. package/dist/graph/unit-artifact-probes.js.map +1 -1
  241. package/dist/graph/unit-results-freshness.d.ts +29 -0
  242. package/dist/graph/unit-results-freshness.d.ts.map +1 -0
  243. package/dist/graph/unit-results-freshness.js +213 -0
  244. package/dist/graph/unit-results-freshness.js.map +1 -0
  245. package/dist/harness/audit.d.ts +3 -2
  246. package/dist/harness/audit.d.ts.map +1 -1
  247. package/dist/harness/audit.js +4 -17
  248. package/dist/harness/audit.js.map +1 -1
  249. package/dist/harness/catalog/gate-rules.yaml +3 -3
  250. package/dist/harness/defect-draft.d.ts.map +1 -1
  251. package/dist/harness/defect-draft.js +12 -2
  252. package/dist/harness/defect-draft.js.map +1 -1
  253. package/dist/harness/defects.d.ts +14 -0
  254. package/dist/harness/defects.d.ts.map +1 -1
  255. package/dist/harness/defects.js +14 -10
  256. package/dist/harness/defects.js.map +1 -1
  257. package/dist/harness/flow-check.d.ts +9 -0
  258. package/dist/harness/flow-check.d.ts.map +1 -1
  259. package/dist/harness/flow-check.js +11 -0
  260. package/dist/harness/flow-check.js.map +1 -1
  261. package/dist/harness/gates/audit-gate-verdict.d.ts.map +1 -1
  262. package/dist/harness/gates/audit-gate-verdict.js +2 -1
  263. package/dist/harness/gates/audit-gate-verdict.js.map +1 -1
  264. package/dist/harness/gates/audit-input-hash.d.ts +2 -0
  265. package/dist/harness/gates/audit-input-hash.d.ts.map +1 -1
  266. package/dist/harness/gates/audit-input-hash.js +30 -2
  267. package/dist/harness/gates/audit-input-hash.js.map +1 -1
  268. package/dist/harness/gates/audit-report-verification.d.ts +17 -6
  269. package/dist/harness/gates/audit-report-verification.d.ts.map +1 -1
  270. package/dist/harness/gates/audit-report-verification.js +55 -21
  271. package/dist/harness/gates/audit-report-verification.js.map +1 -1
  272. package/dist/harness/gates/delivery-checklist.d.ts +3 -0
  273. package/dist/harness/gates/delivery-checklist.d.ts.map +1 -1
  274. package/dist/harness/gates/delivery-checklist.js +39 -6
  275. package/dist/harness/gates/delivery-checklist.js.map +1 -1
  276. package/dist/harness/gates/delivery-decision-gate.d.ts +4 -0
  277. package/dist/harness/gates/delivery-decision-gate.d.ts.map +1 -1
  278. package/dist/harness/gates/delivery-decision-gate.js +26 -1
  279. package/dist/harness/gates/delivery-decision-gate.js.map +1 -1
  280. package/dist/harness/gates/delivery-decision-step.d.ts +4 -1
  281. package/dist/harness/gates/delivery-decision-step.d.ts.map +1 -1
  282. package/dist/harness/gates/delivery-decision-step.js +17 -2
  283. package/dist/harness/gates/delivery-decision-step.js.map +1 -1
  284. package/dist/harness/gates/in-run-decision-check.d.ts.map +1 -1
  285. package/dist/harness/gates/in-run-decision-check.js +40 -12
  286. package/dist/harness/gates/in-run-decision-check.js.map +1 -1
  287. package/dist/harness/gates/migrated-waiver-import-bounds.d.ts +13 -0
  288. package/dist/harness/gates/migrated-waiver-import-bounds.d.ts.map +1 -0
  289. package/dist/harness/gates/migrated-waiver-import-bounds.js +141 -0
  290. package/dist/harness/gates/migrated-waiver-import-bounds.js.map +1 -0
  291. package/dist/harness/gates/migrated-waiver-review.d.ts +14 -4
  292. package/dist/harness/gates/migrated-waiver-review.d.ts.map +1 -1
  293. package/dist/harness/gates/migrated-waiver-review.js +78 -80
  294. package/dist/harness/gates/migrated-waiver-review.js.map +1 -1
  295. package/dist/harness/journey.d.ts +40 -0
  296. package/dist/harness/journey.d.ts.map +1 -1
  297. package/dist/harness/journey.js +155 -22
  298. package/dist/harness/journey.js.map +1 -1
  299. package/dist/harness/next-step.d.ts +19 -0
  300. package/dist/harness/next-step.d.ts.map +1 -1
  301. package/dist/harness/next-step.js +21 -0
  302. package/dist/harness/next-step.js.map +1 -1
  303. package/dist/harness/provenance.d.ts +29 -0
  304. package/dist/harness/provenance.d.ts.map +1 -1
  305. package/dist/harness/provenance.js +109 -9
  306. package/dist/harness/provenance.js.map +1 -1
  307. package/dist/harness/triage-manual-classification.d.ts +32 -0
  308. package/dist/harness/triage-manual-classification.d.ts.map +1 -0
  309. package/dist/harness/triage-manual-classification.js +56 -0
  310. package/dist/harness/triage-manual-classification.js.map +1 -0
  311. package/dist/harness/unit-paths.d.ts +8 -0
  312. package/dist/harness/unit-paths.d.ts.map +1 -1
  313. package/dist/harness/unit-paths.js +23 -1
  314. package/dist/harness/unit-paths.js.map +1 -1
  315. package/dist/hooks/bash-read-only-segment.d.ts +9 -0
  316. package/dist/hooks/bash-read-only-segment.d.ts.map +1 -0
  317. package/dist/hooks/bash-read-only-segment.js +168 -0
  318. package/dist/hooks/bash-read-only-segment.js.map +1 -0
  319. package/dist/hooks/bash-write-targets.d.ts +12 -2
  320. package/dist/hooks/bash-write-targets.d.ts.map +1 -1
  321. package/dist/hooks/bash-write-targets.js +122 -29
  322. package/dist/hooks/bash-write-targets.js.map +1 -1
  323. package/dist/hooks/guard-protected-paths.d.ts +4 -0
  324. package/dist/hooks/guard-protected-paths.d.ts.map +1 -1
  325. package/dist/hooks/guard-protected-paths.js +13 -3
  326. package/dist/hooks/guard-protected-paths.js.map +1 -1
  327. package/dist/hooks/guard-rules.d.ts +0 -17
  328. package/dist/hooks/guard-rules.d.ts.map +1 -1
  329. package/dist/hooks/guard-rules.js +79 -10
  330. package/dist/hooks/guard-rules.js.map +1 -1
  331. package/dist/hooks/guard-tty-spoof-rule.d.ts +3 -1
  332. package/dist/hooks/guard-tty-spoof-rule.d.ts.map +1 -1
  333. package/dist/hooks/guard-tty-spoof-rule.js +3 -2
  334. package/dist/hooks/guard-tty-spoof-rule.js.map +1 -1
  335. package/dist/hooks/hand-back-core.d.ts +9 -4
  336. package/dist/hooks/hand-back-core.d.ts.map +1 -1
  337. package/dist/hooks/hand-back-core.js +24 -11
  338. package/dist/hooks/hand-back-core.js.map +1 -1
  339. package/dist/hooks/session-start-unit-state-hook.d.ts +2 -1
  340. package/dist/hooks/session-start-unit-state-hook.d.ts.map +1 -1
  341. package/dist/hooks/session-start-unit-state-hook.js +9 -4
  342. package/dist/hooks/session-start-unit-state-hook.js.map +1 -1
  343. package/dist/hooks/stop-next-step-hook.d.ts.map +1 -1
  344. package/dist/hooks/stop-next-step-hook.js +1 -0
  345. package/dist/hooks/stop-next-step-hook.js.map +1 -1
  346. package/dist/orchestrator/assets-drift.d.ts +5 -0
  347. package/dist/orchestrator/assets-drift.d.ts.map +1 -1
  348. package/dist/orchestrator/assets-drift.js +12 -3
  349. package/dist/orchestrator/assets-drift.js.map +1 -1
  350. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  351. package/dist/orchestrator/project-initializer.js +1 -0
  352. package/dist/orchestrator/project-initializer.js.map +1 -1
  353. package/dist/orchestrator/templates/ai-src/commands/create-test.md +6 -3
  354. package/dist/orchestrator/templates/ai-src/commands/delivery.md +2 -0
  355. package/dist/orchestrator/templates/ai-src/commands/review.md +2 -0
  356. package/dist/orchestrator/templates/ai-src/commands/run-test.md +1 -1
  357. package/dist/orchestrator/templates/ai-src/partials/waiting-for-you.claude.md +11 -0
  358. package/dist/orchestrator/templates/ai-src/partials/waiting-for-you.codex.md +11 -0
  359. package/dist/orchestrator/templates/ai-src/partials/waiting-for-you.copilot.md +11 -0
  360. package/package.json +3 -3
  361. package/src/capabilities/discover.ts +15 -2
  362. package/src/cli/case-triage-hint.ts +31 -0
  363. package/src/cli/commands/audit.ts +1 -1
  364. package/src/cli/commands/contract-derive-command.ts +20 -8
  365. package/src/cli/commands/contract.ts +41 -7
  366. package/src/cli/commands/delivery.ts +56 -18
  367. package/src/cli/commands/gate.ts +2 -1
  368. package/src/cli/commands/generate.ts +13 -2
  369. package/src/cli/commands/journey.ts +8 -6
  370. package/src/cli/commands/next.ts +41 -12
  371. package/src/cli/commands/triage.ts +41 -3
  372. package/src/cli/commands/update.ts +6 -2
  373. package/src/cli/index.ts +5 -1
  374. package/src/cli/json-error-on-exit.ts +72 -0
  375. package/src/cli/project-sungen-invocation.ts +59 -0
  376. package/src/cli/run-command-hints.ts +47 -0
  377. package/src/cli/waiting-for-you-block.ts +27 -0
  378. package/src/contract/accept-opaque-steps.ts +114 -0
  379. package/src/contract/approve-contract.ts +10 -3
  380. package/src/contract/build-contract-verdict.ts +2 -2
  381. package/src/contract/build-trace-conformance.ts +11 -8
  382. package/src/contract/classify-contract-change.ts +3 -3
  383. package/src/contract/classify-trace-conformance.ts +76 -22
  384. package/src/contract/contract-schema.ts +31 -2
  385. package/src/contract/contracts-lock-file.ts +1 -0
  386. package/src/contract/derive-free-feature.ts +46 -3
  387. package/src/contract/project-contract-blockers.ts +4 -2
  388. package/src/contract/record-contract-approval.ts +41 -1
  389. package/src/contract/render-free-gherkin.ts +5 -0
  390. package/src/contract/resolve-unit-trace-identities.ts +35 -12
  391. package/src/contract/trace/collect-trace-evidence.ts +105 -36
  392. package/src/contract/trace/read-file-once.ts +95 -0
  393. package/src/contract/trace/scenario-id-claims.ts +60 -0
  394. package/src/contract/trace/select-final-attempt.ts +12 -5
  395. package/src/contract/trace/test-step-join.ts +6 -5
  396. package/src/contract/trace/trace-binding.ts +7 -4
  397. package/src/contract/trace/trace-file-reader.ts +34 -13
  398. package/src/contract/trace/trace-freshness.ts +110 -0
  399. package/src/contract/trace/trace-identity-resolution.ts +7 -4
  400. package/src/contract/trace/trace-run-record.ts +37 -15
  401. package/src/contract/trace/trace-steps.ts +1 -1
  402. package/src/contract/trace/trace-zip-limits.ts +6 -2
  403. package/src/contract/verify-approval-decisions.ts +9 -5
  404. package/src/decisions/confirmed-decision-commands.ts +25 -4
  405. package/src/decisions/decision-schema.ts +2 -1
  406. package/src/decisions/git-head-trust.ts +44 -0
  407. package/src/decisions/human-confirmation.ts +5 -0
  408. package/src/decisions/legacy-waiver-migration.ts +14 -3
  409. package/src/exporters/csv-exporter.ts +12 -4
  410. package/src/exporters/matrix/export.ts +11 -4
  411. package/src/exporters/matrix/xlsx-cells.ts +1 -1
  412. package/src/exporters/xlsx-exporter.ts +5 -4
  413. package/src/graph/board-human-actions.ts +80 -0
  414. package/src/graph/evaluate-graph.ts +45 -10
  415. package/src/graph/human-action.ts +33 -0
  416. package/src/graph/manual-triage-decisions.ts +55 -0
  417. package/src/graph/node-decl.ts +18 -0
  418. package/src/graph/nodes/test-case-contract-nodes.ts +28 -7
  419. package/src/graph/nodes/test-case-nodes.ts +37 -5
  420. package/src/graph/nodes/unit-audit-nodes.ts +2 -1
  421. package/src/graph/nodes/unit-contract-node.ts +20 -2
  422. package/src/graph/nodes/unit-design-nodes.ts +2 -1
  423. package/src/graph/test-case-results.ts +25 -4
  424. package/src/graph/test-case-satisfied.ts +7 -4
  425. package/src/graph/test-case-state-shape.ts +7 -2
  426. package/src/graph/unit-artifact-probes.ts +3 -1
  427. package/src/graph/unit-results-freshness.ts +165 -0
  428. package/src/harness/audit.ts +4 -13
  429. package/src/harness/catalog/gate-rules.yaml +3 -3
  430. package/src/harness/defect-draft.ts +13 -2
  431. package/src/harness/defects.ts +22 -9
  432. package/src/harness/flow-check.ts +11 -0
  433. package/src/harness/gates/audit-gate-verdict.ts +2 -1
  434. package/src/harness/gates/audit-input-hash.ts +27 -3
  435. package/src/harness/gates/audit-report-verification.ts +57 -24
  436. package/src/harness/gates/delivery-checklist.ts +39 -7
  437. package/src/harness/gates/delivery-decision-gate.ts +29 -1
  438. package/src/harness/gates/delivery-decision-step.ts +19 -3
  439. package/src/harness/gates/in-run-decision-check.ts +38 -13
  440. package/src/harness/gates/migrated-waiver-import-bounds.ts +103 -0
  441. package/src/harness/gates/migrated-waiver-review.ts +90 -37
  442. package/src/harness/journey.ts +174 -21
  443. package/src/harness/next-step.ts +34 -1
  444. package/src/harness/provenance.ts +99 -11
  445. package/src/harness/triage-manual-classification.ts +76 -0
  446. package/src/harness/unit-paths.ts +19 -0
  447. package/src/hooks/bash-read-only-segment.ts +130 -0
  448. package/src/hooks/bash-write-targets.ts +109 -23
  449. package/src/hooks/guard-protected-paths.ts +13 -3
  450. package/src/hooks/guard-rules.ts +37 -18
  451. package/src/hooks/guard-tty-spoof-rule.ts +6 -3
  452. package/src/hooks/hand-back-core.ts +24 -12
  453. package/src/hooks/session-start-unit-state-hook.ts +9 -4
  454. package/src/hooks/stop-next-step-hook.ts +1 -0
  455. package/src/orchestrator/assets-drift.ts +12 -5
  456. package/src/orchestrator/project-initializer.ts +1 -0
  457. package/src/orchestrator/templates/ai-src/commands/create-test.md +6 -3
  458. package/src/orchestrator/templates/ai-src/commands/delivery.md +2 -0
  459. package/src/orchestrator/templates/ai-src/commands/review.md +2 -0
  460. package/src/orchestrator/templates/ai-src/commands/run-test.md +1 -1
  461. package/src/orchestrator/templates/ai-src/partials/waiting-for-you.claude.md +11 -0
  462. package/src/orchestrator/templates/ai-src/partials/waiting-for-you.codex.md +11 -0
  463. package/src/orchestrator/templates/ai-src/partials/waiting-for-you.copilot.md +11 -0
@@ -1,22 +1,110 @@
1
1
  /**
2
- * Provenance stamp for deterministic reports (score-determinism D1).
2
+ * Provenance stamp for deterministic reports (score-determinism D1), and the RULES the audit's
3
+ * verdict was judged by (#933, owner decision W4).
3
4
  *
4
- * The audit score is a pure function of the project files + the bundled catalog.
5
- * Stamping the sungen version + a catalog hash into the report lets two users who
6
- * get different scores see IMMEDIATELY whether the cause is a version/catalog
7
- * mismatch (the main residual source of cross-user variance) rather than a real
8
- * difference in their test design.
5
+ * The audit score is a pure function of the project files + the bundled catalogs + the scoring
6
+ * code. Stamping them into the report lets two users who get different scores see IMMEDIATELY
7
+ * whether the cause is a rules mismatch rather than a real difference in their test design — and
8
+ * lets `verifyAuditReport` tell a report judged by today's rules from one judged by other rules:
9
+ *
10
+ * sungenVersion which package wrote it — informational: a newer sungen with the same rules
11
+ * gives the same verdict, so the version alone never makes a report stale
12
+ * catalogHash sha256(universal-viewpoints.yaml), first 12 hex — the viewpoint catalog
13
+ * rulesHash sha256(AUDIT_RULES_VERSION + every catalog file under harness/catalog/ + each
14
+ * bundled driver's sources/catalogs), first 12 hex — everything the verdict
15
+ * depends on that is not the project's own files. No package VERSION is hashed:
16
+ * packages are lockstep-versioned, so a release would stale every report
9
17
  */
10
18
  import * as fs from 'fs';
11
19
  import * as path from 'path';
12
20
  import * as crypto from 'crypto';
13
21
 
22
+ /**
23
+ * The version of the audit's SCORING CODE. Bump it (by one) in the same change whenever what
24
+ * `runAudit` computes for the same project can change: a sensor, a threshold, a gate term
25
+ * (audit-gate-terms.ts), the score model or weights, a finding that feeds the verdict, or the
26
+ * audit sensor of a bundled driver. Every report scored before the bump then reads stale and asks
27
+ * for one `sungen audit`. Catalog edits (harness/catalog/*.yaml) and bundled-driver changes (their
28
+ * sources and catalogs are hashed) need no bump.
29
+ * A release that changes none of these keeps every report trusted across the upgrade.
30
+ * Enforced by tests/audit/run.ts: its rules lock (tests/audit/audit-rules.lock.json) fails when the
31
+ * fixtures' audit output changes under an unchanged rules hash. After the bump, re-record it:
32
+ * npx tsx tests/audit/run.ts --record-rules-lock
33
+ */
34
+ export const AUDIT_RULES_VERSION = 1;
35
+
14
36
  export interface Provenance {
15
37
  sungenVersion: string;
16
38
  catalogHash: string; // sha256(universal-viewpoints.yaml), first 12 hex — the catalog that drives the score
39
+ rulesHash: string; // sha256(AUDIT_RULES_VERSION + harness/catalog/*.yaml + bundled drivers), first 12 hex — the verdict's rules
40
+ }
41
+
42
+ /** A bundled driver as an input of the rules: its package dir, or null when it is not installed. */
43
+ export interface DriverRulesInput { name: string; version?: string; dir: string | null }
44
+
45
+ /** What is not a driver's rule: dependencies, build output, tests, docs, editor and lock files. */
46
+ const SKIP_DIR = new Set(['node_modules', 'dist', 'tests', 'test', 'coverage']);
47
+ const SKIP_FILE = /^(?:\.|package(?:-lock)?\.json$|tsconfig.*\.json$|.*\.(?:md|tsbuildinfo)$)/;
48
+
49
+ function driverFiles(root: string, rel = ''): string[] {
50
+ const out: string[] = [];
51
+ for (const e of fs.readdirSync(path.join(root, rel), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, 'en'))) {
52
+ if (e.name.startsWith('.')) continue;
53
+ const r = rel ? `${rel}/${e.name}` : e.name;
54
+ if (e.isDirectory()) { if (!SKIP_DIR.has(e.name)) out.push(...driverFiles(root, r)); }
55
+ else if (e.isFile() && !SKIP_FILE.test(e.name)) out.push(r);
56
+ }
57
+ return out;
58
+ }
59
+
60
+ /** The package root above a driver's resolved entry file (the dir whose package.json names it). */
61
+ function packageRootOf(entry: string, name: string): string | null {
62
+ let dir = path.dirname(entry);
63
+ for (let i = 0; i < 8; i++) {
64
+ try { if (JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))?.name === name) return dir; } catch { /* keep walking */ }
65
+ const parent = path.dirname(dir);
66
+ if (parent === dir) break;
67
+ dir = parent;
68
+ }
69
+ return null;
17
70
  }
18
71
 
72
+ /** Each bundled driver as discovery would load it (project anchor, then core's), or `absent`. */
73
+ export function bundledDriverRulesInputs(): DriverRulesInput[] {
74
+ // Lazy: discovery pulls the capability registry, which nothing else here needs.
75
+ const { bundledDriverNames, resolveDriverCopy } = require('../capabilities/discover') as typeof import('../capabilities/discover');
76
+ return bundledDriverNames().map((name) => {
77
+ const copy = resolveDriverCopy(name);
78
+ const dir = copy ? packageRootOf(copy.resolved, name) : null;
79
+ return { name, version: copy?.version, dir };
80
+ });
81
+ }
82
+
83
+ const CATALOG_DIR = path.join(__dirname, 'catalog');
84
+ const sha12 = (s: string | Buffer): string => crypto.createHash('sha256').update(s).digest('hex').slice(0, 12);
85
+
86
+ /**
87
+ * The rules hash: the scoring version, every *.yaml of the catalog dir (by name), and each bundled
88
+ * driver — every source / catalog file of its package (by relative path; build output, tests and
89
+ * dependencies excluded), or a stable `absent` marker when it is not installed. A driver's version is
90
+ * deliberately left out: releases bump every package in lockstep, and only content changes the rules.
91
+ */
92
+ export function rulesHashOf(rulesVersion: number, catalogDir: string = CATALOG_DIR, drivers: DriverRulesInput[] = bundledDriverRulesInputs()): string {
93
+ const files = fs.readdirSync(catalogDir).filter((f) => f.endsWith('.yaml')).sort();
94
+ const h = crypto.createHash('sha256').update(`audit-rules:${rulesVersion}\n`);
95
+ for (const f of files) h.update(`${f}\n`).update(fs.readFileSync(path.join(catalogDir, f))).update('\n');
96
+ for (const d of [...drivers].sort((a, b) => a.name.localeCompare(b.name, 'en'))) {
97
+ h.update(`driver:${d.name}\n`);
98
+ if (!d.dir) { h.update('absent\n'); continue; }
99
+ for (const f of driverFiles(d.dir)) h.update(`${f}\n`).update(fs.readFileSync(path.join(d.dir, f))).update('\n');
100
+ }
101
+ return h.digest('hex').slice(0, 12);
102
+ }
103
+
104
+ let cached: Provenance | null = null;
105
+
19
106
  export function getProvenance(): Provenance {
107
+ if (cached) return cached;
20
108
  let sungenVersion = 'unknown';
21
109
  try {
22
110
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf-8'));
@@ -24,10 +112,10 @@ export function getProvenance(): Provenance {
24
112
  } catch { /* version stays 'unknown' */ }
25
113
 
26
114
  let catalogHash = 'unknown';
27
- try {
28
- const cat = fs.readFileSync(path.join(__dirname, 'catalog', 'universal-viewpoints.yaml'), 'utf-8');
29
- catalogHash = crypto.createHash('sha256').update(cat).digest('hex').slice(0, 12);
30
- } catch { /* hash stays 'unknown' */ }
115
+ try { catalogHash = sha12(fs.readFileSync(path.join(CATALOG_DIR, 'universal-viewpoints.yaml'), 'utf-8')); } catch { /* hash stays 'unknown' */ }
116
+ let rulesHash = 'unknown';
117
+ try { rulesHash = rulesHashOf(AUDIT_RULES_VERSION); } catch { /* hash stays 'unknown' */ }
31
118
 
32
- return { sungenVersion, catalogHash };
119
+ cached = { sungenVersion, catalogHash, rulesHash };
120
+ return cached;
33
121
  }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `sungen triage --screen <u> --case <id> --as product|test-defect|environment --reason "…"` (#928):
3
+ * classify one red by hand — the one `sungen triage` left unclassified, or one it got wrong — as a
4
+ * `triage-classification` decision in the unit's `decisions/` (read back by graph/manual-triage-decisions.ts).
5
+ *
6
+ * validated first the class, a reason, a case that exists, is red and is not exempt — before any
7
+ * prompt, so a human never confirms something that is then rejected
8
+ * product stricter than any automatic class (it leads to a QA decision): anyone may record it
9
+ * test-defect, can hide a product defect behind "fix the test" / "re-run": a human types a
10
+ * environment confirmation on a terminal (DOS-13.4); without one it is refused, nothing is
11
+ * written and the exact command is printed. Only after the typed phrase does the
12
+ * entry carry `humanConfirmed: true`, and a reader ignores one without it
13
+ * bound evidence_hash = the case's red as it reads now; a re-run voids the classification
14
+ */
15
+ import type { AppendResult } from '../decisions/append-decision';
16
+ import { appendDecision } from '../decisions/append-decision';
17
+ import type { DecisionDraft } from '../decisions/decision-schema';
18
+ import { gitUserName } from '../decisions/decision-source';
19
+ import { ConfirmationIO, formatCommand, requireTypedConfirmation } from '../decisions/human-confirmation';
20
+ import { resolveUnitCandidates } from '../cli/resolve-unit';
21
+ import { HUMAN_ONLY_TRIAGE, MANUAL_TRIAGE_AS, ManualTriageClass, TRIAGE_DECISION_KIND } from '../graph/manual-triage-decisions';
22
+ import { exemptionOf } from '../graph/test-case-satisfied';
23
+ import { testCaseStates } from '../graph/test-case-states';
24
+ import { unitScopeOfDir } from './gates/unit-gate-overrides';
25
+
26
+ export interface ClassifyRedCaseInput {
27
+ projectRoot: string;
28
+ unit: string;
29
+ caseId: string;
30
+ /** product | test-defect | environment */
31
+ as: string;
32
+ reason?: string;
33
+ io: ConfirmationIO;
34
+ }
35
+
36
+ export interface ClassifyRedCaseResult extends AppendResult { caseId: string; class: ManualTriageClass }
37
+
38
+ export async function classifyRedCase(i: ClassifyRedCaseInput): Promise<ClassifyRedCaseResult> {
39
+ const cls = Object.prototype.hasOwnProperty.call(MANUAL_TRIAGE_AS, i.as) ? MANUAL_TRIAGE_AS[i.as] : undefined;
40
+ if (!cls) throw new Error(`--as must be one of ${Object.keys(MANUAL_TRIAGE_AS).join(', ')} (got "${i.as}")`);
41
+ const reason = (i.reason ?? '').trim();
42
+ if (!reason) throw new Error('--reason is required: a classification records WHY this red is what you say it is');
43
+ const found = resolveUnitCandidates(i.unit, i.projectRoot);
44
+ if (found.length !== 1) throw new Error(`"${i.unit}" does not name exactly one unit under qa/screens, qa/flows or qa/api`);
45
+ const unitDir = found[0].dir;
46
+ const scope = unitScopeOfDir(i.projectRoot, unitDir);
47
+ if (!scope) throw new Error(`"${i.unit}" has no decisions scope`);
48
+ const cases = testCaseStates(i.projectRoot, i.unit, { unitDir, auditFindings: [] });
49
+ const tc = cases.find((c) => c.id === i.caseId);
50
+ if (!tc) {
51
+ const known = cases.slice(0, 8).map((c) => c.id).join(', ');
52
+ throw new Error(`no test case "${i.caseId}" in ${i.unit}${known ? ` — known: ${known}${cases.length > 8 ? ', …' : ''}` : ''}`);
53
+ }
54
+ if (tc.result !== 'failed' || !tc.evidence) throw new Error(`${tc.id} has no red result to classify (result: ${tc.result})`);
55
+ const exempt = exemptionOf(tc);
56
+ if (exempt) throw new Error(`${tc.id} is exempt (${exempt}) — it needs no classification`);
57
+
58
+ const human = HUMAN_ONLY_TRIAGE.has(cls);
59
+ if (human) {
60
+ await requireTypedConfirmation({
61
+ what: `classify the red ${tc.id} of ${i.unit} as ${cls}`,
62
+ phrase: tc.id,
63
+ command: formatCommand(['sungen', 'triage', found[0].kind === 'api' ? '--api' : '--screen', i.unit, '--case', tc.id, '--as', i.as, '--reason', reason]),
64
+ summary: [`case: ${tc.title}`, `automatic triage: ${tc.triage ? `${tc.triage.class} (${tc.triage.ruleId})` : 'none current'}`, `reason: ${reason}`,
65
+ `recorded by: ${gitUserName(i.projectRoot)}`, `as ${cls} it leaves the product-defect path — confirm only after reading its evidence (trace, error)`,
66
+ 'it stops counting as soon as the case runs again'],
67
+ io: i.io,
68
+ });
69
+ }
70
+ const draft: DecisionDraft = {
71
+ kind: TRIAGE_DECISION_KIND, anchors: [], text: `${tc.id} is ${cls}: ${tc.title}`, reason,
72
+ payload: { case: tc.id, class: cls, ...(human ? { humanConfirmed: true } : {}) },
73
+ source: { by: gitUserName(i.projectRoot), where: 'cli' }, valid_from: new Date().toISOString(), visibility: 'internal', evidence_hash: tc.evidence,
74
+ };
75
+ return { ...appendDecision(i.projectRoot, scope, draft), caseId: tc.id, class: cls };
76
+ }
@@ -49,3 +49,22 @@ export function featureFilesFor(unitDir: string, unit: string): string[] {
49
49
  ...names.filter((n) => n !== canonicalName),
50
50
  ].map((n) => path.join(dir, n));
51
51
  }
52
+
53
+ /**
54
+ * The catalog-resolution id for a unit dir (relative to qa/): screen · flows/<flow> · api/<area> ·
55
+ * api/flows/<flow>; a bare screen is its basename. Shared by the audit, which scores and links the
56
+ * unit under this id, and its inputsHash, which reads what the audit reads under the same id.
57
+ */
58
+ export function catalogIdFromScreenDir(screenDir: string): string {
59
+ const parts = screenDir.split(path.sep);
60
+ const qa = parts.lastIndexOf('qa');
61
+ if (qa >= 0) {
62
+ if (parts[qa + 1] === 'api' && parts[qa + 2] === 'flows' && parts[qa + 3]) return `api/flows/${parts[qa + 3]}`;
63
+ if (parts[qa + 1] === 'api' && parts[qa + 2]) return `api/${parts[qa + 2]}`;
64
+ if (parts[qa + 1] === 'flows' && parts[qa + 2]) return `flows/${parts[qa + 2]}`;
65
+ }
66
+ return path.basename(screenDir);
67
+ }
68
+
69
+ /** A screen unit — the only kind a sibling flow credits (TQ-4); flows and api units are not. */
70
+ export const isScreenUnitId = (catalogId: string): boolean => !/^(flows|api)\//.test(catalogId);
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Which Bash segments only READ, so they may name a protected path freely (bash-protected-mention).
3
+ *
4
+ * An allowlist, fail-closed (#968): a command is read-only only in the forms listed here. It drops
5
+ * back to "the guard cannot tell" when
6
+ * - the command word has a `/` outside the system bin dirs (`./cat`, `/tmp/evil/jq`),
7
+ * - a word holds `$`, `{`, `*`, `?` or `[` (`$'--output'`, `${…}`, `--{output=…,stat}`, a `q*` glob can
8
+ * hide an option name or a path),
9
+ * - an option runs a program or writes a file, also abbreviated where the tool accepts that
10
+ * (`rg --pre`, `git diff --output` / `--outp`, ugrep's `grep --filter` / `--save-config` / `--pager`
11
+ * / `--config`, `sort -o` / `--o…` / `--c…`),
12
+ * - git has any global option before its subcommand except `--no-pager` / `-P` / `--no-optional-locks`
13
+ * (`-c`, `--config-env`, `--exec-path`, `-C`, `--git-dir`… can point it at another program),
14
+ * - its output is piped into anything that is not a read-only command or filter (`| xargs rm`, `| sh`).
15
+ * And the whole LINE loses the exemption (lineKeepsReadOnlyExemption) when it uses command
16
+ * substitution or changes shell state that a later command obeys: any `NAME=value`, `export`,
17
+ * `declare`, `typeset`, `set`, `setopt`, `shopt`, `alias`, `unalias`, `hash`, `unset`, `read`,
18
+ * `enable`, `builtin`, `trap`, `source` / `.`, `eval`, or a function definition.
19
+ * `git grep` is not read-only at all: `-O` runs a program, also inside a flag cluster (`-nOrm`) and
20
+ * as an abbreviation (`--open=`). `node -e|-p` counts only when its script can do nothing but read
21
+ * (isReadOnlyNodeEval).
22
+ *
23
+ * Accepted floor, not closed here: `sungen` is listed read-only (it writes approval records only
24
+ * through its own human gates); a path built at runtime (`$VAR` away from a protected name), a
25
+ * symlink, or a script that writes without naming the path is out of reach; a bare directory
26
+ * (`rm -rf qa/decisions`, `cd qa && rm -rf decisions/`) is not a protected-path mention (#970);
27
+ * `git -C <dir> …` is not followed; a same-line `D=qa/decisions; … $D` is not tracked; pathless
28
+ * destructive git (`clean -fdx`, `stash`, `reset --hard`, `checkout .`, `apply`, `am`) is not seen;
29
+ * and a quoting trick the tokenizer reads differently from the shell can turn a deny into an ask.
30
+ * The backstop is release-time decision integrity: the git HEAD comparison and the in-run rule.
31
+ */
32
+ import * as path from 'path';
33
+ import type { BashSegment } from './bash-write-targets';
34
+
35
+ const READ_ONLY = new Set(['cat', 'ls', 'head', 'tail', 'grep', 'rg', 'wc', 'diff', 'stat', 'jq', 'sungen', 'sungen.js']);
36
+ const GIT_READ_ONLY = new Set(['diff', 'log', 'show', 'status', 'check-ignore', 'ls-files', 'ls-tree', 'blame', 'rev-parse', 'cat-file']);
37
+ /** The only git global options allowed before a read-only subcommand. */
38
+ const GIT_SAFE_GLOBALS = new Set(['--no-pager', '-P', '--no-optional-locks']);
39
+ /** Filters that write only to stdout — safe as a pipe sink, in the forms isPipeFilter allows. */
40
+ const FILTERS = new Set(['sort', 'uniq', 'cut', 'tr']);
41
+
42
+ /** A command word with a `/` counts only from these dirs: `./cat` or `/tmp/evil/jq` is not `cat` / `jq`. */
43
+ const SYSTEM_BIN = new Set(['/usr/bin', '/bin', '/usr/local/bin', '/opt/homebrew/bin']);
44
+ /** Characters that let the shell build a word the guard cannot read: expansions, braces, globs. */
45
+ const OPAQUE_WORD = /[$*?[{]/;
46
+
47
+ /** Is `word` the long option `--<one of names>`, or an abbreviation of one at least `min` chars long? */
48
+ const longOpt = (names: string[], min: number) => (word: string): boolean => {
49
+ const m = /^--([^=]+)/.exec(word);
50
+ return !!m && names.some((n) => n === m[1] || (m[1].length >= min && n.startsWith(m[1])));
51
+ };
52
+ /** Options that make an otherwise read-only command run a program or write a file. */
53
+ const UNSAFE_OPT: Record<string, (word: string) => boolean> = {
54
+ rg: (w) => /^--(?:pre|hostname-bin)(?:=|$)/.test(w),
55
+ git: longOpt(['output', 'ext-diff', 'textconv'], 2),
56
+ // `grep` may be ugrep: --filter runs programs, --save-config writes, --pager / --view run one, --config / ---FILE load those
57
+ grep: (w) => w.startsWith('---') || longOpt(['filter', 'save-config', 'pager', 'view', 'config'], 3)(w),
58
+ };
59
+ const trustedWords = (seg: BashSegment): boolean =>
60
+ !seg.assigns.length && !seg.words.some((w) => OPAQUE_WORD.test(w))
61
+ && (!seg.cmdPath.includes('/') || SYSTEM_BIN.has(path.posix.dirname(seg.cmdPath)));
62
+
63
+ /** `$(…)`, backticks, `<(…)`, `>(…)`: a command's output becomes another command's words. */
64
+ const SUBSTITUTION = /\$\(|`|[<>]\(/;
65
+ /** Commands that change shell state a later command on the line obeys. */
66
+ const SHELL_STATE = new Set([
67
+ 'export', 'declare', 'typeset', 'local', 'readonly', 'set', 'setopt', 'shopt', 'alias', 'unalias', 'hash', 'unset',
68
+ 'read', 'enable', 'builtin', 'trap', 'source', '.', 'eval', 'function',
69
+ ]);
70
+ /** `name() {` / `name() (` — a function definition (`function name` is caught by SHELL_STATE). */
71
+ const FUNCTION_DEF = /(?:^|[\s;&|])[\w.:-]+\s*\(\s*\)\s*[{(]/;
72
+
73
+ /** May any read-only segment of this line keep its exemption? */
74
+ export function lineKeepsReadOnlyExemption(command: string, segs: BashSegment[]): boolean {
75
+ return !SUBSTITUTION.test(command) && !FUNCTION_DEF.test(command)
76
+ && !segs.some((s) => s.assigns.length || SHELL_STATE.has(s.cmd));
77
+ }
78
+
79
+ /** `node -e` / `node -p` whose script can only read: no write/exec API, no computed access, no escapes. */
80
+ const NODE_EVAL_FLAGS = new Set(['-e', '-p', '--eval', '--print']);
81
+ const NODE_UNSAFE = new RegExp([
82
+ '[`\\[\\\\]', // template literals, computed property access, string escapes
83
+ 'flag', // `readFileSync(p, { flag: 'w' })` truncates p
84
+ 'write', 'append', '\\brm', 'unlink', 'rename', 'copy', '\\bcp', 'mkd', 'truncate', 'stream', 'open', 'link',
85
+ 'chmod', 'chown', 'utime', 'exec', 'spawn', 'fork', 'child', 'eval', 'function', 'import', 'constructor',
86
+ 'proto', 'reflect', 'proxy', 'global', 'process', 'module', 'object', 'concat', '\\bvm\\b', 'worker', 'binding',
87
+ ].join('|'), 'i');
88
+ const SAFE_REQUIRE = /\brequire\(\s*(['"])(?:(?:node:)?(?:fs|path)|[./][^'"]*\.json)\1\s*\)/g;
89
+
90
+ export function isReadOnlyNodeEval(args: string[]): boolean {
91
+ if (args.length !== 2 || !NODE_EVAL_FLAGS.has(args[0])) return false;
92
+ const script = args[1];
93
+ return !NODE_UNSAFE.test(script) && !/\brequire\b/.test(script.replace(SAFE_REQUIRE, ''));
94
+ }
95
+
96
+ /** `git [--no-pager|-P|--no-optional-locks]… <read-only subcommand> …` — anchored, no other global option. */
97
+ function isReadOnlyGit(words: string[]): boolean {
98
+ let k = 0;
99
+ while (GIT_SAFE_GLOBALS.has(words[k])) k++;
100
+ return GIT_READ_ONLY.has(words[k] ?? '');
101
+ }
102
+
103
+ /** Does this segment only read? (`words` holds its arguments and redirection targets.) */
104
+ export function isReadOnlySegment(seg: BashSegment): boolean {
105
+ if (!seg.cmd || !trustedWords(seg)) return false;
106
+ const unsafe = UNSAFE_OPT[seg.cmd];
107
+ if (unsafe && seg.words.some(unsafe)) return false;
108
+ if (seg.cmd === 'git') return isReadOnlyGit(seg.words);
109
+ if (seg.cmd === 'node') return isReadOnlyNodeEval(seg.words);
110
+ return READ_ONLY.has(seg.cmd);
111
+ }
112
+
113
+ /** A pipe sink that cannot act on what it reads: a read-only command, or a stdout-only filter. */
114
+ function isPipeFilter(seg: BashSegment): boolean {
115
+ if (isReadOnlySegment(seg)) return true;
116
+ if (!FILTERS.has(seg.cmd) || !trustedWords(seg)) return false;
117
+ // `sort -o out` / `--compress-program=…` (any `--o…` / `--c…` abbreviation) write or run; `uniq in out` writes its second operand.
118
+ if (seg.cmd === 'sort') return !seg.words.some((w) => /^(?:-[^-]*o|--[oc])/.test(w));
119
+ if (seg.cmd === 'uniq') return seg.words.every((w) => w.startsWith('-'));
120
+ return true; // cut, tr: operands are inputs or character sets
121
+ }
122
+
123
+ /** Does everything downstream of segment `i` in its pipeline only read / filter? */
124
+ export function pipelineStaysReadOnly(segs: BashSegment[], i: number): boolean {
125
+ for (let j = i; segs[j]?.pipesTo; j++) {
126
+ const next = segs[j + 1];
127
+ if (!next || !isPipeFilter(next)) return false;
128
+ }
129
+ return true;
130
+ }
@@ -1,18 +1,47 @@
1
1
  /**
2
2
  * The files a Bash command line WRITES through the three forms the pre-tool guard covers (P0.1):
3
- * output redirection (`>`, `>>`, `>|`, `&>`, `2>`…), `tee`, and in-place `sed -i`.
3
+ * output redirection (`>`, `>>`, `>|`, `&>`, `2>`, `>&file`, zsh `>!`…), `tee`, in-place `sed -i`, and
4
+ * `git … --output <file>` (also abbreviated, `--outp=`) (#968).
4
5
  *
5
- * A floor, not a shell interpreter: quotes, escapes, `;`/`&&`/`||`/`|` and a leading `cd` are
6
- * understood; anything else that can write a file (`cp`, a script, `python -c`) is out of scope by
6
+ * A floor, not a shell interpreter: quotes, escapes, `$'…'` ANSI-C strings, backslash-newline
7
+ * continuations, `;`/`&&`/`||`/`|` and `cd` / `pushd` (with their options; an operand it cannot resolve
8
+ * marks the directory unknown) are understood; anything else that can write a file (`cp`, a script, `python -c`) is out of scope by
7
9
  * design. The command string is untrusted — it is only tokenized here, never executed.
8
10
  */
9
11
  import * as path from 'path';
10
12
 
11
- type Tok = { kind: 'word'; value: string } | { kind: 'sep' } | { kind: 'redir'; writes: boolean };
13
+ type Tok = { kind: 'word'; value: string } | { kind: 'sep'; pipe?: boolean } | { kind: 'redir'; writes: boolean; dup?: boolean };
12
14
 
13
15
  /** Commands that run another command: the real one is the next word. */
14
16
  const WRAPPERS = new Set(['sudo', 'env', 'command', 'nohup', 'time', 'exec', 'nice']);
15
17
  const ASSIGNMENT = /^[A-Za-z_]\w*=/;
18
+ const ANSI_C: Record<string, string> = {
19
+ a: '\x07', b: '\b', e: '\x1b', E: '\x1b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v', '\\': '\\', "'": "'", '"': '"', '?': '?',
20
+ };
21
+
22
+ /** Decode a bash `$'…'` body starting at `j` (just past `$'`); returns the text and the index past the closing `'`. */
23
+ function ansiC(cmd: string, j: number): [string, number] {
24
+ let out = '';
25
+ while (j < cmd.length && cmd[j] !== "'") {
26
+ if (cmd[j] !== '\\' || j + 1 >= cmd.length) { out += cmd[j++]; continue; }
27
+ const e = cmd[j + 1];
28
+ j += 2;
29
+ if (e in ANSI_C) out += ANSI_C[e];
30
+ else if (/[0-7]/.test(e)) {
31
+ let m = e;
32
+ while (m.length < 3 && /[0-7]/.test(cmd[j] ?? '')) m += cmd[j++];
33
+ out += String.fromCharCode(parseInt(m, 8) & 0xff);
34
+ } else if (e === 'x' || e === 'u' || e === 'U') {
35
+ const max = e === 'x' ? 2 : e === 'u' ? 4 : 8;
36
+ let h = '';
37
+ while (h.length < max && /[0-9a-fA-F]/.test(cmd[j] ?? '')) h += cmd[j++];
38
+ const cp = parseInt(h, 16);
39
+ out += !h ? `\\${e}` : cp <= 0x10ffff ? String.fromCodePoint(cp) : '\ufffd'; // never throw: a throw fails open
40
+ } else if (e === 'c' && j < cmd.length) out += String.fromCharCode(cmd.charCodeAt(j++) & 31);
41
+ else out += `\\${e}`;
42
+ }
43
+ return [out, j + 1];
44
+ }
16
45
 
17
46
  function tokenize(cmd: string): Tok[] {
18
47
  const toks: Tok[] = [];
@@ -21,25 +50,39 @@ function tokenize(cmd: string): Tok[] {
21
50
  const flush = (): void => { if (inWord) toks.push({ kind: 'word', value: word }); word = ''; inWord = false; };
22
51
  for (let i = 0; i < cmd.length;) {
23
52
  const ch = cmd[i];
24
- if (ch === "'") {
53
+ if (ch === '$' && cmd[i + 1] === "'") {
54
+ const [text, next] = ansiC(cmd, i + 2);
55
+ word += text; inWord = true; i = next;
56
+ } else if (ch === '$' && cmd[i + 1] === '"') {
57
+ i++; // `$"…"` (locale translation) reads as `"…"`
58
+ } else if (ch === "'") {
25
59
  const end = cmd.indexOf("'", i + 1);
26
60
  word += cmd.slice(i + 1, end < 0 ? cmd.length : end); inWord = true; i = end < 0 ? cmd.length : end + 1;
27
61
  } else if (ch === '"') {
28
62
  let j = i + 1;
29
- for (; j < cmd.length && cmd[j] !== '"'; j++) { if (cmd[j] === '\\' && j + 1 < cmd.length) j++; word += cmd[j]; }
63
+ for (; j < cmd.length && cmd[j] !== '"'; j++) {
64
+ if (cmd[j] === '\\' && cmd[j + 1] === '\n') { j++; continue; } // line continuation
65
+ if (cmd[j] === '\\' && j + 1 < cmd.length) j++;
66
+ word += cmd[j];
67
+ }
30
68
  inWord = true; i = j + 1;
69
+ } else if (ch === '\\' && cmd[i + 1] === '\n') {
70
+ i += 2; // line continuation: removed entirely
31
71
  } else if (ch === '\\' && i + 1 < cmd.length) {
32
72
  word += cmd[i + 1]; inWord = true; i += 2;
33
73
  } else if (ch === ' ' || ch === '\t') {
34
74
  flush(); i++;
35
75
  } else if ('\n;|()'.includes(ch) || (ch === '&' && cmd[i + 1] !== '>')) {
36
- flush(); toks.push({ kind: 'sep' }); i += (ch === '|' || ch === '&') && cmd[i + 1] === ch ? 2 : 1;
76
+ const doubled = (ch === '|' || ch === '&') && cmd[i + 1] === ch;
77
+ flush(); toks.push({ kind: 'sep', pipe: ch === '|' && !doubled }); i += doubled ? 2 : 1;
37
78
  } else if (ch === '>' || (ch === '&' && cmd[i + 1] === '>')) {
38
79
  if (ch === '>' && inWord && /^\d+$/.test(word)) { word = ''; inWord = false; } else flush(); // `2>`: an fd, not a word
39
80
  i += ch === '&' ? 2 : 1;
40
- if (cmd[i] === '>' || cmd[i] === '|') i++;
41
- if (cmd[i] === '&') { i++; toks.push({ kind: 'redir', writes: false }); } // `>&2`: an fd dup
42
- else toks.push({ kind: 'redir', writes: true });
81
+ if (cmd[i] === '>') i++; // `>>`, `&>>`
82
+ const dup = cmd[i] === '&'; // `>&x`, `>>&x`: an fd dup only when x is an fd
83
+ if (dup) i++;
84
+ if (cmd[i] === '|' || cmd[i] === '!') i++; // clobber: `>|`, `>!`, `>>!`, `&>!`, `>&|`, `>&!`
85
+ toks.push({ kind: 'redir', writes: true, dup });
43
86
  } else if (ch === '<') {
44
87
  flush(); i++;
45
88
  while (cmd[i] === '<') i++;
@@ -58,43 +101,86 @@ const isInPlaceFlag = (a: string): boolean => a === '--in-place' || a.startsWith
58
101
  export interface BashSegment {
59
102
  /** The command name (basename, after env assignments and wrappers like `sudo`), or ''. */
60
103
  cmd: string;
104
+ /** The command word as written (`./cat`, `/usr/bin/grep`, `cat`). */
105
+ cmdPath: string;
61
106
  /** Its arguments, and every redirection target, as written. */
62
107
  words: string[];
63
108
  /** The directory it runs in — a preceding `cd` in the same line is followed. */
64
109
  dir: string;
65
- /** Absolute paths it writes via redirection, `tee` or `sed -i`. */
110
+ /** Absolute paths it writes via redirection, `tee`, `sed -i` or `git … --output`. */
66
111
  writes: string[];
112
+ /** False once a `cd` / `pushd` / `popd` on the line went somewhere the guard cannot resolve. */
113
+ dirKnown: boolean;
114
+ /** It writes a relative path while `dirKnown` is false — the guard cannot tell where. */
115
+ writesInUnknownDir: boolean;
116
+ /** Leading `NAME=value` assignments (a lone `X=…` is a segment too, with cmd ''). */
117
+ assigns: string[];
118
+ /** Its stdout feeds the next segment through `|`. */
119
+ pipesTo: boolean;
120
+ }
121
+
122
+ /** `git diff|log|show … --output=<file>` / `--output <file>` writes that file (git takes `--outp` for `--output`). */
123
+ function gitOutputTargets(args: string[]): string[] {
124
+ const out: string[] = [];
125
+ args.forEach((a, k) => {
126
+ const m = /^--([\w-]+)(=([\s\S]*))?$/.exec(a);
127
+ if (!m || m[1].length < 2 || !'output'.startsWith(m[1])) return;
128
+ if (m[2] !== undefined) out.push(m[3]);
129
+ else if (args[k + 1]) out.push(args[k + 1]);
130
+ });
131
+ return out.filter((t) => t && t !== '/dev/null' && t !== '-');
132
+ }
133
+
134
+ /** The operand of `cd` / `pushd` after its options (`-L -P -e -@`, pushd `-n`, `--`), or null when there is not exactly one. */
135
+ function cdOperand(args: string[]): string | null {
136
+ let k = 0;
137
+ while (k < args.length && /^-[LPe@n]+$/.test(args[k])) k++;
138
+ if (args[k] === '--') k++;
139
+ return args.length - k === 1 ? args[k] : null;
67
140
  }
68
141
 
69
142
  export function bashSegments(command: string, cwd: string): BashSegment[] {
70
143
  const out: BashSegment[] = [];
71
144
  const toks = tokenize(command);
72
145
  let dir = cwd;
146
+ let dirKnown = true;
73
147
  let seg: string[] = [];
74
148
  let redirected: string[] = [];
75
- let writes: string[] = [];
76
- const endSegment = (): void => {
149
+ let rawWrites: string[] = [];
150
+ const endSegment = (pipesTo = false): void => {
77
151
  let k = 0;
78
152
  while (k < seg.length && (ASSIGNMENT.test(seg[k]) || WRAPPERS.has(seg[k]))) k++;
79
- const cmd = path.basename(seg[k] ?? '');
153
+ const assigns = seg.slice(0, k).filter((w) => ASSIGNMENT.test(w));
154
+ const cmdPath = seg[k] ?? '';
155
+ const cmd = path.basename(cmdPath);
80
156
  const args = seg.slice(k + 1).filter((a) => a !== '');
81
- if (cmd === 'tee') writes.push(...args.filter((a) => !a.startsWith('-')).map((a) => path.resolve(dir, a)));
82
- else if ((cmd === 'sed' || cmd === 'gsed') && args.some(isInPlaceFlag)) {
83
- writes.push(...args.filter((a) => !a.startsWith('-')).map((a) => path.resolve(dir, a)));
157
+ if (cmd === 'tee') rawWrites.push(...args.filter((a) => !a.startsWith('-')));
158
+ else if ((cmd === 'sed' || cmd === 'gsed') && args.some(isInPlaceFlag)) rawWrites.push(...args.filter((a) => !a.startsWith('-')));
159
+ else if (cmd === 'git') rawWrites.push(...gitOutputTargets(args));
160
+ const writes = rawWrites.map((a) => path.resolve(dir, a));
161
+ const writesInUnknownDir = !dirKnown && rawWrites.some((a) => !path.isAbsolute(a));
162
+ if (cmd || redirected.length || assigns.length) {
163
+ out.push({ cmd, cmdPath, words: [...args, ...redirected], dir, dirKnown, writes, writesInUnknownDir, assigns, pipesTo });
164
+ }
165
+ if (cmd === 'cd' || cmd === 'pushd' || cmd === 'popd') {
166
+ const target = cmd === 'popd' ? null : cdOperand(args);
167
+ if (target && !/[$~`]/.test(target) && target !== '-' && !/^[+-]\d+$/.test(target)) {
168
+ dir = path.resolve(dir, target);
169
+ dirKnown = dirKnown || path.isAbsolute(target);
170
+ } else dirKnown = false; // `cd`, `cd ~`, `cd $D`, `cd -`, `popd`: somewhere the guard cannot resolve
84
171
  }
85
- if (cmd || redirected.length) out.push({ cmd, words: [...args, ...redirected], dir, writes });
86
- if (cmd === 'cd' && args.length === 1 && !/^[-~]/.test(args[0])) dir = path.resolve(dir, args[0]);
87
- seg = []; redirected = []; writes = [];
172
+ seg = []; redirected = []; rawWrites = [];
88
173
  };
89
174
  for (let t = 0; t < toks.length; t++) {
90
175
  const tok = toks[t];
91
- if (tok.kind === 'sep') { endSegment(); continue; }
176
+ if (tok.kind === 'sep') { endSegment(tok.pipe === true); continue; }
92
177
  if (tok.kind === 'redir') {
93
178
  const target = toks[t + 1];
94
179
  if (target?.kind !== 'word') continue;
95
180
  t++;
96
181
  redirected.push(target.value);
97
- if (tok.writes && target.value && target.value !== '/dev/null' && target.value !== '-') writes.push(path.resolve(dir, target.value));
182
+ const fd = tok.dup && /^(?:\d+-?|-)$/.test(target.value); // `2>&1`, `>&2`, `2>&-`
183
+ if (tok.writes && !fd && target.value && target.value !== '/dev/null' && target.value !== '-') rawWrites.push(target.value);
98
184
  continue;
99
185
  }
100
186
  seg.push(tok.value);
@@ -103,7 +189,7 @@ export function bashSegments(command: string, cwd: string): BashSegment[] {
103
189
  return out;
104
190
  }
105
191
 
106
- /** Absolute paths the command writes via redirection, `tee` or `sed -i`, resolved against `cwd`. */
192
+ /** Absolute paths the command writes via redirection, `tee`, `sed -i` or `git --output`, resolved against `cwd`. */
107
193
  export function bashWriteTargets(command: string, cwd: string): string[] {
108
194
  return bashSegments(command, cwd).flatMap((s) => s.writes);
109
195
  }
@@ -10,7 +10,8 @@
10
10
  * — the CLI still writes the last two itself; the guard only judges an agent's tool calls
11
11
  * trace evidence — DENY an agent write (phase 08; the verdict trusts it only as far as this guard and git review):
12
12
  * .sungen/trace-identity/** (the driver's identity answers — written by the CLI)
13
- * test-results/sungen-run-record*.json (run id + test-file hashes — written by the Playwright reporter)
13
+ * test-results[-free]/sungen-run-record*.json (run id + test-file hashes — written by the Playwright reporter;
14
+ * test-results-free/ is the free run's own outputDir, #941)
14
15
  * hook control — ASK before an agent write (the agent must not switch its own guard off):
15
16
  * qa/sungen.config.yaml · .claude/settings.json · .claude/settings.local.json ·
16
17
  * .claude/hooks/sungen-hook.cjs
@@ -25,7 +26,7 @@ const LOCK = /^qa\/(?:.+\/)?contracts\.lock\.yaml$/i;
25
26
  const DECISION = /^qa\/(?:(?:screens|flows|api(?:\/flows)?)\/[^/]+\/)?decisions\/[^/]+/i;
26
27
  const APPROVAL_STATE = /^\.sungen\/(?:journey\/[^/]+\.state\.json|viewpoint-baseline\.json)$/i;
27
28
  /** Trace evidence (phase 08, DOS-14.6): the driver's identity answers and the run records. */
28
- const TRACE_EVIDENCE = /^(?:\.sungen\/trace-identity\/.+|test-results\/sungen-run-record(?:\.[A-Za-z0-9._-]+)?\.json)$/i;
29
+ const TRACE_EVIDENCE = /^(?:\.sungen\/trace-identity\/.+|test-results(?:-free)?\/sungen-run-record(?:\.[A-Za-z0-9._-]+)?\.json)$/i;
29
30
  const HOOK_CONTROL = ['qa/sungen.config.yaml', '.claude/settings.json', '.claude/settings.local.json', '.claude/hooks/sungen-hook.cjs'];
30
31
 
31
32
  /** The same paths as they appear INSIDE a command string (e.g. in `bash -c "…"` or `python -c "…"`). */
@@ -62,9 +63,18 @@ export function hookControlPath(abs: string): string | null {
62
63
  return HOOK_CONTROL.find((rel) => p.endsWith(`/${rel}`)) ?? null;
63
64
  }
64
65
 
66
+ /** Names near a protected path — for a word the shell expands (`$Q/decisions/x`), whose real text is unknown. */
67
+ const NEAR_PROTECTED = /qa|\.sungen|\.claude|decision|contracts|lock\.ya?ml|run-record|trace-identity|viewpoint|journey|settings|sungen-hook|sungen\.config/i;
68
+
69
+ /** A word the shell rewrites at runtime (`$…`, a literal `\`) that sits near a protected name: the guard cannot read it. */
70
+ export const opaqueNearProtected = (word: string): boolean => /[$\\]/.test(word) && NEAR_PROTECTED.test(word);
71
+
72
+ /** Does this text (a command word, a heredoc, a whole command line) name a protected path as written? */
73
+ export const mentionsProtectedText = (text: string): boolean => MENTIONS.some((re) => re.test(text));
74
+
65
75
  /** Does this command word name a protected path — as text, or once resolved against `dir`? */
66
76
  export function mentionsProtectedPath(word: string, dir: string): boolean {
67
- if (MENTIONS.some((re) => re.test(word))) return true;
77
+ if (mentionsProtectedText(word)) return true;
68
78
  const abs = path.resolve(dir, word);
69
79
  return approvalRecordPath(abs) !== null || traceEvidencePath(abs) !== null || hookControlPath(abs) !== null;
70
80
  }