@lannguyensi/harness 0.44.0 → 0.45.0

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 (231) hide show
  1. package/CHANGELOG.md +1213 -0
  2. package/dist/cli/adopt/derive.d.ts +16 -7
  3. package/dist/cli/adopt/derive.js +19 -2
  4. package/dist/cli/adopt/derive.js.map +1 -1
  5. package/dist/cli/adopt/index.js +18 -3
  6. package/dist/cli/adopt/index.js.map +1 -1
  7. package/dist/cli/apply/apply.d.ts +1 -0
  8. package/dist/cli/apply/apply.js +62 -6
  9. package/dist/cli/apply/apply.js.map +1 -1
  10. package/dist/cli/apply/generate-codex-config.js +5 -6
  11. package/dist/cli/apply/generate-codex-config.js.map +1 -1
  12. package/dist/cli/apply/generate-opencode-config.d.ts +58 -0
  13. package/dist/cli/apply/generate-opencode-config.js +210 -0
  14. package/dist/cli/apply/generate-opencode-config.js.map +1 -0
  15. package/dist/cli/apply/generate-settings.d.ts +90 -32
  16. package/dist/cli/apply/generate-settings.js +133 -10
  17. package/dist/cli/apply/generate-settings.js.map +1 -1
  18. package/dist/cli/apply/index.d.ts +2 -1
  19. package/dist/cli/apply/index.js +2 -1
  20. package/dist/cli/apply/index.js.map +1 -1
  21. package/dist/cli/approve/stdin-report.d.ts +15 -0
  22. package/dist/cli/approve/stdin-report.js +1 -1
  23. package/dist/cli/approve/stdin-report.js.map +1 -1
  24. package/dist/cli/approve/understanding.d.ts +75 -0
  25. package/dist/cli/approve/understanding.js +150 -2
  26. package/dist/cli/approve/understanding.js.map +1 -1
  27. package/dist/cli/audit.d.ts +1 -1
  28. package/dist/cli/audit.js +3 -2
  29. package/dist/cli/audit.js.map +1 -1
  30. package/dist/cli/doctor/claude-mcp.d.ts +19 -1
  31. package/dist/cli/doctor/claude-mcp.js +36 -10
  32. package/dist/cli/doctor/claude-mcp.js.map +1 -1
  33. package/dist/cli/doctor/codex.d.ts +3 -1
  34. package/dist/cli/doctor/codex.js +6 -9
  35. package/dist/cli/doctor/codex.js.map +1 -1
  36. package/dist/cli/doctor/format.js +94 -8
  37. package/dist/cli/doctor/format.js.map +1 -1
  38. package/dist/cli/doctor/index.d.ts +23 -0
  39. package/dist/cli/doctor/index.js +106 -2
  40. package/dist/cli/doctor/index.js.map +1 -1
  41. package/dist/cli/doctor/opencode.d.ts +24 -0
  42. package/dist/cli/doctor/opencode.js +107 -0
  43. package/dist/cli/doctor/opencode.js.map +1 -0
  44. package/dist/cli/doctor/target-checks.d.ts +11 -0
  45. package/dist/cli/doctor/target-checks.js +25 -0
  46. package/dist/cli/doctor/target-checks.js.map +1 -0
  47. package/dist/cli/doctor/toolchain-parity.d.ts +63 -0
  48. package/dist/cli/doctor/toolchain-parity.js +154 -0
  49. package/dist/cli/doctor/toolchain-parity.js.map +1 -0
  50. package/dist/cli/doctor/types.d.ts +85 -1
  51. package/dist/cli/doctor/types.js +1 -1
  52. package/dist/cli/doctor/types.js.map +1 -1
  53. package/dist/cli/doctor/understanding-mode-env.d.ts +39 -0
  54. package/dist/cli/doctor/understanding-mode-env.js +83 -0
  55. package/dist/cli/doctor/understanding-mode-env.js.map +1 -0
  56. package/dist/cli/dry-run.js +32 -17
  57. package/dist/cli/dry-run.js.map +1 -1
  58. package/dist/cli/explain.d.ts +1 -1
  59. package/dist/cli/explain.js +1 -1
  60. package/dist/cli/explain.js.map +1 -1
  61. package/dist/cli/index.js +58 -4
  62. package/dist/cli/index.js.map +1 -1
  63. package/dist/cli/init/composer.js +36 -13
  64. package/dist/cli/init/composer.js.map +1 -1
  65. package/dist/cli/init/interactive.d.ts +7 -4
  66. package/dist/cli/init/interactive.js +79 -27
  67. package/dist/cli/init/interactive.js.map +1 -1
  68. package/dist/cli/init/profiles.d.ts +2 -2
  69. package/dist/cli/init/profiles.js +32 -1
  70. package/dist/cli/init/profiles.js.map +1 -1
  71. package/dist/cli/init/templates.d.ts +2 -1
  72. package/dist/cli/init/templates.js +130 -20
  73. package/dist/cli/init/templates.js.map +1 -1
  74. package/dist/cli/pack/approve-escape.js +69 -8
  75. package/dist/cli/pack/approve-escape.js.map +1 -1
  76. package/dist/cli/pack/hook-branch-protection.js +1 -1
  77. package/dist/cli/pack/hook-branch-protection.js.map +1 -1
  78. package/dist/cli/pack/hook-codex-pre-tool-use.js +27 -1
  79. package/dist/cli/pack/hook-codex-pre-tool-use.js.map +1 -1
  80. package/dist/cli/pack/hook-post-merge-gate.js +1 -1
  81. package/dist/cli/pack/hook-post-merge-gate.js.map +1 -1
  82. package/dist/cli/pack/hook-pre-tool-use.js +91 -4
  83. package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
  84. package/dist/cli/pack/hook-solution-acceptance.js +19 -2
  85. package/dist/cli/pack/hook-solution-acceptance.js.map +1 -1
  86. package/dist/cli/pack/understanding-report-schema-hint.d.ts +13 -0
  87. package/dist/cli/pack/understanding-report-schema-hint.js +52 -3
  88. package/dist/cli/pack/understanding-report-schema-hint.js.map +1 -1
  89. package/dist/cli/pause/index.js +1 -1
  90. package/dist/cli/pause/index.js.map +1 -1
  91. package/dist/cli/policy/intercept.d.ts +148 -0
  92. package/dist/cli/policy/intercept.js +379 -48
  93. package/dist/cli/policy/intercept.js.map +1 -1
  94. package/dist/cli/session-start/index.d.ts +12 -3
  95. package/dist/cli/session-start/index.js +67 -11
  96. package/dist/cli/session-start/index.js.map +1 -1
  97. package/dist/cli/session-start/stale-base-check.d.ts +71 -0
  98. package/dist/cli/session-start/stale-base-check.js +379 -0
  99. package/dist/cli/session-start/stale-base-check.js.map +1 -0
  100. package/dist/cli/session-start/toolchain-parity.d.ts +142 -8
  101. package/dist/cli/session-start/toolchain-parity.js +244 -36
  102. package/dist/cli/session-start/toolchain-parity.js.map +1 -1
  103. package/dist/cli/smoke/assertions.d.ts +9 -2
  104. package/dist/cli/smoke/assertions.js +9 -2
  105. package/dist/cli/smoke/assertions.js.map +1 -1
  106. package/dist/cli/uninstall/index.js +18 -2
  107. package/dist/cli/uninstall/index.js.map +1 -1
  108. package/dist/cli/validate/checks.d.ts +3 -1
  109. package/dist/cli/validate/checks.js +298 -3
  110. package/dist/cli/validate/checks.js.map +1 -1
  111. package/dist/io/claude-mcp.d.ts +122 -1
  112. package/dist/io/claude-mcp.js +214 -12
  113. package/dist/io/claude-mcp.js.map +1 -1
  114. package/dist/io/duration.js.map +1 -0
  115. package/dist/io/expand-home.js.map +1 -0
  116. package/dist/io/extract.js.map +1 -0
  117. package/dist/io/harness-lock.js +1 -1
  118. package/dist/io/harness-lock.js.map +1 -1
  119. package/dist/{runtime → io}/ledger-record.d.ts +1 -1
  120. package/dist/io/ledger-record.js.map +1 -0
  121. package/dist/policies/index.d.ts +2 -2
  122. package/dist/policies/index.js +2 -2
  123. package/dist/policies/index.js.map +1 -1
  124. package/dist/policies/ledger-client.js +10 -6
  125. package/dist/policies/ledger-client.js.map +1 -1
  126. package/dist/policies/producers.js +1 -1
  127. package/dist/policies/producers.js.map +1 -1
  128. package/dist/policies/requires.js +2 -2
  129. package/dist/policies/requires.js.map +1 -1
  130. package/dist/policy-packs/builtin/branch-protection.js +28 -3
  131. package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
  132. package/dist/policy-packs/builtin/post-merge-gate.js +45 -9
  133. package/dist/policy-packs/builtin/post-merge-gate.js.map +1 -1
  134. package/dist/policy-packs/builtin/solution-acceptance-runtime.d.ts +139 -8
  135. package/dist/policy-packs/builtin/solution-acceptance-runtime.js +246 -12
  136. package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +1 -1
  137. package/dist/policy-packs/builtin/solution-acceptance.js +17 -3
  138. package/dist/policy-packs/builtin/solution-acceptance.js.map +1 -1
  139. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.d.ts +25 -0
  140. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.js +89 -0
  141. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.js.map +1 -0
  142. package/dist/policy-packs/builtin/understanding-before-execution/index.d.ts +13 -0
  143. package/dist/policy-packs/builtin/understanding-before-execution/index.js +39 -0
  144. package/dist/policy-packs/builtin/understanding-before-execution/index.js.map +1 -0
  145. package/dist/policy-packs/builtin/understanding-before-execution/ledger.d.ts +25 -0
  146. package/dist/policy-packs/builtin/understanding-before-execution/ledger.js +56 -0
  147. package/dist/policy-packs/builtin/understanding-before-execution/ledger.js.map +1 -0
  148. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.d.ts +20 -0
  149. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js +79 -0
  150. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js.map +1 -0
  151. package/dist/policy-packs/builtin/understanding-before-execution/markers.d.ts +103 -0
  152. package/dist/policy-packs/builtin/understanding-before-execution/markers.js +203 -0
  153. package/dist/policy-packs/builtin/understanding-before-execution/markers.js.map +1 -0
  154. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.d.ts +177 -0
  155. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js +309 -0
  156. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js.map +1 -0
  157. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.d.ts +106 -0
  158. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js +209 -0
  159. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js.map +1 -0
  160. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.d.ts +78 -0
  161. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.js +185 -0
  162. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.js.map +1 -0
  163. package/dist/policy-packs/builtin/understanding-before-execution-runtime.d.ts +1 -536
  164. package/dist/policy-packs/builtin/understanding-before-execution-runtime.js +9 -1095
  165. package/dist/policy-packs/builtin/understanding-before-execution-runtime.js.map +1 -1
  166. package/dist/policy-packs/builtin/understanding-before-execution.d.ts +109 -1
  167. package/dist/policy-packs/builtin/understanding-before-execution.js +183 -15
  168. package/dist/policy-packs/builtin/understanding-before-execution.js.map +1 -1
  169. package/dist/policy-packs/registry.d.ts +11 -0
  170. package/dist/policy-packs/registry.js +1 -1
  171. package/dist/policy-packs/registry.js.map +1 -1
  172. package/dist/policy-packs/runtime.d.ts +1 -1
  173. package/dist/policy-packs/runtime.js +10 -1
  174. package/dist/policy-packs/runtime.js.map +1 -1
  175. package/dist/runtime/agent-facing.js +1 -1
  176. package/dist/runtime/agent-facing.js.map +1 -1
  177. package/dist/runtime/bash-match-facts.d.ts +9 -7
  178. package/dist/runtime/bash-match-facts.js +9 -7
  179. package/dist/runtime/bash-match-facts.js.map +1 -1
  180. package/dist/runtime/bash-match-registry.d.ts +75 -38
  181. package/dist/runtime/bash-match-registry.js +279 -232
  182. package/dist/runtime/bash-match-registry.js.map +1 -1
  183. package/dist/runtime/bash-prefix-parse.d.ts +7 -0
  184. package/dist/runtime/bash-prefix-parse.js +205 -17
  185. package/dist/runtime/bash-prefix-parse.js.map +1 -1
  186. package/dist/runtime/command-normalize.d.ts +112 -0
  187. package/dist/runtime/command-normalize.js +411 -38
  188. package/dist/runtime/command-normalize.js.map +1 -1
  189. package/dist/runtime/hermetic-spawn-guard.js +6 -0
  190. package/dist/runtime/hermetic-spawn-guard.js.map +1 -1
  191. package/dist/runtime/index.d.ts +2 -2
  192. package/dist/runtime/index.js +2 -2
  193. package/dist/runtime/index.js.map +1 -1
  194. package/dist/runtime/intercept.d.ts +78 -3
  195. package/dist/runtime/intercept.js +151 -39
  196. package/dist/runtime/intercept.js.map +1 -1
  197. package/dist/runtime/ledger-add.js +1 -1
  198. package/dist/runtime/ledger-add.js.map +1 -1
  199. package/dist/runtime/read-only-bash.js +303 -24
  200. package/dist/runtime/read-only-bash.js.map +1 -1
  201. package/dist/schema/doctor.d.ts +25 -0
  202. package/dist/schema/doctor.js +23 -0
  203. package/dist/schema/doctor.js.map +1 -0
  204. package/dist/schema/extract.js +1 -1
  205. package/dist/schema/extract.js.map +1 -1
  206. package/dist/schema/index.d.ts +76 -0
  207. package/dist/schema/index.js +9 -0
  208. package/dist/schema/index.js.map +1 -1
  209. package/dist/schema/requires.js +2 -2
  210. package/dist/schema/requires.js.map +1 -1
  211. package/dist/schema/risk.d.ts +7 -0
  212. package/dist/schema/risk.js +23 -0
  213. package/dist/schema/risk.js.map +1 -1
  214. package/dist/schema/stale-base-check.d.ts +18 -0
  215. package/dist/schema/stale-base-check.js +37 -0
  216. package/dist/schema/stale-base-check.js.map +1 -0
  217. package/dist/schema/toolchain-parity.d.ts +3 -0
  218. package/dist/schema/toolchain-parity.js +13 -0
  219. package/dist/schema/toolchain-parity.js.map +1 -1
  220. package/package.json +3 -2
  221. package/dist/policies/duration.js.map +0 -1
  222. package/dist/policies/extract.js.map +0 -1
  223. package/dist/runtime/expand-home.js.map +0 -1
  224. package/dist/runtime/ledger-record.js.map +0 -1
  225. /package/dist/{policies → io}/duration.d.ts +0 -0
  226. /package/dist/{policies → io}/duration.js +0 -0
  227. /package/dist/{runtime → io}/expand-home.d.ts +0 -0
  228. /package/dist/{runtime → io}/expand-home.js +0 -0
  229. /package/dist/{policies → io}/extract.d.ts +0 -0
  230. /package/dist/{policies → io}/extract.js +0 -0
  231. /package/dist/{runtime → io}/ledger-record.js +0 -0
@@ -0,0 +1,203 @@
1
+ // Session-scoped approval marker file (filesystem source of the
2
+ // two-source approval check), split out of the former monolithic
3
+ // understanding-before-execution-runtime.ts (structural concentration
4
+ // slice 2, agent-tasks 348a4d42). Pure move: see
5
+ // src/policy-packs/builtin/understanding-before-execution/index.ts for
6
+ // the re-exported public surface.
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import { atomicWriteFile } from "../../../io/atomic-write.js";
10
+ import { readRegularFileRejectingSymlink } from "../../../io/read-regular-file.js";
11
+ import { signMarker, verifyMarkerSignature } from "../../../runtime/approval-signing.js";
12
+ import { rejectMalformedSessionId } from "../../../runtime/reject-malformed-session-id.js";
13
+ import { safeJsonParse } from "./persisted-reports.js";
14
+ export const APPROVAL_MARKER_DIRNAME = ".approvals";
15
+ /** Filesystem path of the per-session approval marker. */
16
+ export function approvalMarkerPathFor(generatedDir, sessionId) {
17
+ rejectMalformedSessionId(sessionId);
18
+ return path.join(generatedDir, APPROVAL_MARKER_DIRNAME, sessionId);
19
+ }
20
+ /**
21
+ * Operator-side: write the marker file the gate consults. Atomic so a
22
+ * crash mid-write cannot leave a half-empty file the gate would accept
23
+ * as approved. Caller is `harness approve understanding`, which the
24
+ * operator runs from their un-hooked shell; if the agent could call
25
+ * this path the gate's value would collapse, so it lives behind the
26
+ * approve CLI rather than as a generally importable verb.
27
+ *
28
+ * The marker is HMAC-signed (harness/f9485cc7) over (sessionId,
29
+ * approvedAt, approvedBy, reportContentHash) using an operator-side key
30
+ * lazily generated at `<generatedDir>/.approval-signing.key` — see
31
+ * `src/runtime/approval-signing.ts` for the key-management contract and
32
+ * the honest trust model. Mode 0600 on the marker file itself, best-
33
+ * effort (matches the signing key's own permission convention); this is
34
+ * defense-in-depth alongside the signature, not a substitute for it.
35
+ */
36
+ export function writeApprovalMarker(generatedDir, sessionId, marker) {
37
+ const filePath = approvalMarkerPathFor(generatedDir, sessionId);
38
+ const signed = signMarker(generatedDir, sessionId, marker);
39
+ atomicWriteFile(filePath, `${JSON.stringify(signed, null, 2)}\n`, { mode: 0o600 });
40
+ return filePath;
41
+ }
42
+ /**
43
+ * Gate-side: is the per-session marker file present, readable, AND
44
+ * validly signed? (harness/f9485cc7 — this replaced the earlier
45
+ * "existence is enough" contract: a marker's mere presence used to
46
+ * satisfy the gate even with a malformed or corrupted body, on the
47
+ * theory that Edit/Write/Bash being gated meant only the operator could
48
+ * ever create the file. That theory holds only as long as no OTHER
49
+ * write primitive reaches `harness.generated/` — exactly the invariant
50
+ * this signing scheme no longer has to trust blindly.) A marker with no
51
+ * `signature`, a wrong `alg`, or a signature that fails to verify is
52
+ * REJECTED — see `MarkerCheck.forged` — with the SAME `matched: false`
53
+ * outcome as no marker at all, but a distinct, forgery-specific detail
54
+ * string so callers can tell the two apart for audit.
55
+ *
56
+ * `opts.maxAgeMs` (agent-tasks/d8ee60ca): when set, a validly-signed
57
+ * marker whose `approvedAt` is older than the cutoff returns
58
+ * `matched:false` with an "expired" detail so the agent gets the same
59
+ * "no approval" UX as a never-approved session and must re-approve.
60
+ * Expiry is only evaluated AFTER signature verification succeeds — an
61
+ * unsigned/forged marker is rejected outright, never "expired".
62
+ */
63
+ export function checkApprovalMarker(generatedDir, sessionId, opts = {}) {
64
+ // Construct the path defensively: a malformed sessionId must fail CLOSED
65
+ // here (no valid marker, so the gate blocks and demands approval), not
66
+ // throw out of the gate hook, which the top-level handler turns into a
67
+ // non-blocking exit that would let the gated tool proceed.
68
+ let filePath;
69
+ try {
70
+ filePath = approvalMarkerPathFor(generatedDir, sessionId);
71
+ }
72
+ catch (err) {
73
+ return {
74
+ matched: false,
75
+ detail: `invalid sessionId for approval marker: ${err instanceof Error ? err.message : String(err)}`,
76
+ marker: null,
77
+ expired: false,
78
+ forged: false,
79
+ };
80
+ }
81
+ // Shared symlink-rejecting read (src/io/read-regular-file.ts): the lstat
82
+ // reject is defense-in-depth against a planted symlink
83
+ // (agent-tasks/d39f160e); each failure mode keeps its distinct detail.
84
+ const read = readRegularFileRejectingSymlink(filePath);
85
+ if (read.kind === "missing") {
86
+ return {
87
+ matched: false,
88
+ detail: `no approval marker at ${filePath}`,
89
+ marker: null,
90
+ expired: false,
91
+ forged: false,
92
+ };
93
+ }
94
+ if (read.kind === "symlink") {
95
+ return {
96
+ matched: false,
97
+ detail: `approval marker is a symlink, refusing for safety: ${filePath}`,
98
+ marker: null,
99
+ expired: false,
100
+ forged: false,
101
+ };
102
+ }
103
+ if (read.kind === "not-regular") {
104
+ return {
105
+ matched: false,
106
+ detail: `approval marker path is not a regular file: ${filePath}`,
107
+ marker: null,
108
+ expired: false,
109
+ forged: false,
110
+ };
111
+ }
112
+ if (read.kind === "unreadable") {
113
+ // A genuine I/O error reading an existing regular file (permissions
114
+ // changed mid-flight, etc.) — not evidence of tampering, but its
115
+ // signature cannot be proven either, so fail closed. Kept distinct
116
+ // from `forged` (no claim of an active forgery attempt).
117
+ return {
118
+ matched: false,
119
+ detail: `approval marker at ${filePath} exists but could not be read (I/O error); treating as unapproved since its signature cannot be verified`,
120
+ marker: null,
121
+ expired: false,
122
+ forged: false,
123
+ };
124
+ }
125
+ // read.kind === "ok"
126
+ const parsed = safeJsonParse(read.content);
127
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
128
+ return {
129
+ matched: false,
130
+ detail: `forged/unsigned marker rejected: marker body at ${filePath} is not a JSON object`,
131
+ marker: null,
132
+ expired: false,
133
+ forged: true,
134
+ };
135
+ }
136
+ const obj = parsed;
137
+ const verification = verifyMarkerSignature(generatedDir, sessionId, obj);
138
+ if (!verification.ok) {
139
+ if (verification.kind === "key-unavailable") {
140
+ // Fail-closed I/O problem (permission error / disk issue reading or
141
+ // creating the signing key), NOT evidence of an active forgery
142
+ // attempt — a marker written by the operator with a perfectly valid
143
+ // signature would also fail this exact check if the key file became
144
+ // unreadable. Kept distinct from `forged` (review LOW 1,
145
+ // harness/f9485cc7) so a broken key file doesn't read as an attack
146
+ // in audit output, mirroring the `read.kind === "unreadable"` branch
147
+ // above.
148
+ return {
149
+ matched: false,
150
+ detail: `approval marker at ${filePath} could not be verified: ${verification.reason}; treating as unapproved`,
151
+ marker: null,
152
+ expired: false,
153
+ forged: false,
154
+ };
155
+ }
156
+ return {
157
+ matched: false,
158
+ detail: `forged/unsigned marker rejected: ${verification.reason} (${filePath})`,
159
+ marker: null,
160
+ expired: false,
161
+ forged: true,
162
+ };
163
+ }
164
+ const approvedAt = obj["approvedAt"];
165
+ const approvedBy = obj["approvedBy"];
166
+ const reportContentHash = typeof obj["reportContentHash"] === "string" ? obj["reportContentHash"] : null;
167
+ const marker = { approvedAt, approvedBy, reportContentHash };
168
+ if (opts.maxAgeMs !== undefined) {
169
+ const approvedAtMs = Date.parse(approvedAt);
170
+ if (Number.isFinite(approvedAtMs)) {
171
+ const nowMs = (opts.now ?? new Date()).getTime();
172
+ const ageMs = nowMs - approvedAtMs;
173
+ if (ageMs > opts.maxAgeMs) {
174
+ const ageMin = Math.round(ageMs / 60_000);
175
+ const maxMin = Math.round(opts.maxAgeMs / 60_000);
176
+ return {
177
+ matched: false,
178
+ detail: `approval marker ${path.basename(filePath)} expired: age ${ageMin}m > max ${maxMin}m (approved at ${approvedAt})`,
179
+ marker,
180
+ expired: true,
181
+ forged: false,
182
+ };
183
+ }
184
+ }
185
+ }
186
+ return {
187
+ matched: true,
188
+ detail: `approved via marker ${path.basename(filePath)}: approved at ${approvedAt} by ${approvedBy}, signature verified`,
189
+ marker,
190
+ expired: false,
191
+ forged: false,
192
+ };
193
+ }
194
+ /** Clear the per-session marker (used by `harness approve --revoke` and tests). */
195
+ export function clearApprovalMarker(generatedDir, sessionId) {
196
+ try {
197
+ fs.rmSync(approvalMarkerPathFor(generatedDir, sessionId));
198
+ }
199
+ catch {
200
+ /* already gone */
201
+ }
202
+ }
203
+ //# sourceMappingURL=markers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markers.js","sourceRoot":"","sources":["../../../../src/policy-packs/builtin/understanding-before-execution/markers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,177 @@
1
+ export interface PersistedReport {
2
+ filePath: string;
3
+ sessionId: string | null;
4
+ approvalStatus: string | null;
5
+ approvedAt: string | null;
6
+ /**
7
+ * ISO timestamp the producer stamped when it wrote the report; null
8
+ * for legacy reports without the field.
9
+ */
10
+ createdAt: string | null;
11
+ /**
12
+ * Effective creation time in epoch ms, resolved `createdAt` →
13
+ * filename ISO prefix → file mtime. Unlike mtime alone this survives
14
+ * the approval rewrite (which bumps mtime and would otherwise make a
15
+ * weeks-old report sort as the freshest, harness-discovery C1).
16
+ */
17
+ createdAtMs: number;
18
+ }
19
+ /**
20
+ * Env var the persisted-report directory can be set from. Honored by
21
+ * harness (`defaultReportsDir` below + emitted by `harness apply` onto
22
+ * the pack-contributed hook commands) AND by `@lannguyensi/understanding-gate`
23
+ * (its `core/persistence.js:resolveReportDir` reads the same name), so
24
+ * the three actors that touch the directory — Stop hook (package),
25
+ * PreToolUse blocker (harness), `harness approve understanding` — can
26
+ * agree on the path regardless of each process's cwd.
27
+ */
28
+ export declare const REPORTS_DIR_ENV = "UNDERSTANDING_GATE_REPORT_DIR";
29
+ /**
30
+ * Resolve the persisted-report directory. Precedence:
31
+ * 1. `UNDERSTANDING_GATE_REPORT_DIR` (taken verbatim — apply emits an
32
+ * absolute path, operator-exported values are shell-expanded before
33
+ * we see them).
34
+ * 2. `<cwd>/.understanding-gate/reports` — backward-compat fallback.
35
+ * Callers that have a stable anchor (the manifest directory) pass
36
+ * it as `cwd` so the fallback agrees with whatever path apply
37
+ * baked into the hook commands.
38
+ */
39
+ export declare function defaultReportsDir(cwd?: string): string;
40
+ /**
41
+ * Project root anchor for the reports directory: `<dir-of-manifest>/.understanding-gate/reports`.
42
+ * Used by `harness apply` to bake an absolute, manifest-anchored value into
43
+ * the pack-contributed hook commands' env, and by `harness approve` as the
44
+ * fallback when `UNDERSTANDING_GATE_REPORT_DIR` is unset.
45
+ */
46
+ export declare function reportsDirForManifest(manifestPath: string): string;
47
+ export declare function safeJsonParse(text: string): unknown;
48
+ /**
49
+ * List persisted reports under `dir`, newest-first by creation time
50
+ * (JSON `createdAt`, falling back to the filename ISO prefix, falling
51
+ * back to mtime). Missing directory returns []. Any I/O error on a
52
+ * single file is silently skipped; the caller falls through to the
53
+ * ledger result.
54
+ *
55
+ * Creation time, NOT mtime, is the sort key: `harness approve
56
+ * understanding` rewrites the report it flips, which bumps mtime and
57
+ * made an old just-approved report sort as the freshest
58
+ * (harness-discovery C1). mtime survives only as the last-resort
59
+ * fallback for files that carry neither timestamp.
60
+ */
61
+ export declare function listPersistedReports(dir: string): PersistedReport[];
62
+ /**
63
+ * Maximum age a sessionId-null report may have for the tolerant
64
+ * fallback to adopt it on the `harness approve understanding` path.
65
+ * Sized for the real flow (Stop hook persists at turn end, operator
66
+ * approves within minutes) with slack for a slow read-through. Live
67
+ * repro that motivated it: a 17-day-old pending report got adopted,
68
+ * validated, and stamped for a fresh session because the producer had
69
+ * silently failed to persist the fresh report (harness-discovery C1,
70
+ * friction-log #67).
71
+ */
72
+ export declare const TOLERANT_FALLBACK_MAX_AGE_MS: number;
73
+ /**
74
+ * Tolerance for a sessionId-less candidate whose `createdAt` lies in
75
+ * the FUTURE relative to the approve-time clock. A future creation
76
+ * time is suspect either way: a forged `createdAt` (the producer's
77
+ * Metadata block lets the agent author it) or serious clock skew.
78
+ * Beyond this skew the fallback rejects the candidate just like a
79
+ * stale one rather than trusting a timestamp that cannot be right.
80
+ */
81
+ export declare const TOLERANT_FALLBACK_FUTURE_SKEW_MS: number;
82
+ export interface FindReportOptions {
83
+ /**
84
+ * Behaviour of the sessionId-null tolerant fallback (older Stop-hook
85
+ * package versions write reports without a `sessionId` field):
86
+ * - `"any"` (default): adopt the freshest sessionId-null report
87
+ * regardless of its `approvalStatus`. The gate read path
88
+ * (`checkPersistedReport`) and post-tool-use expiry
89
+ * (`expirePersistedReport`) rely on this so they keep finding the
90
+ * session's own report.
91
+ * - `"uncompleted"`: skip sessionId-null reports whose
92
+ * `approvalStatus` is a terminal `approved` / `expired`. Such a
93
+ * report belongs to a prior, finished approval cycle (often from
94
+ * a different task days ago) and must not be silently re-adopted
95
+ * as the current session's approval. `harness approve
96
+ * understanding` passes this so it never flips a stale unrelated
97
+ * report into the live session (harness/0dce3880 friction #1).
98
+ */
99
+ tolerantFallback?: "any" | "uncompleted";
100
+ /**
101
+ * Maximum age (relative to `now`) of a sessionId-null candidate the
102
+ * tolerant fallback may adopt; older candidates are skipped and
103
+ * surfaced via `FindReportSelection.staleRejected`. Strict sessionId
104
+ * matches are never age-limited. Unset means no limit (the legacy
105
+ * gate-read / expiry contract).
106
+ */
107
+ maxFallbackAgeMs?: number;
108
+ /** Clock anchor for the age computation; defaults to the wall clock. */
109
+ now?: Date;
110
+ }
111
+ export interface FindReportSelection {
112
+ report: PersistedReport | null;
113
+ /**
114
+ * True when `report` was adopted via the sessionId-null tolerant
115
+ * fallback rather than a strict sessionId match. Callers that bind
116
+ * the report to a session (the approve flow) surface this loudly so
117
+ * the operator can verify the adoption.
118
+ */
119
+ fallbackAdopted: boolean;
120
+ /**
121
+ * sessionId-null candidates skipped for exceeding `maxFallbackAgeMs`,
122
+ * newest first. Lets the caller distinguish "no report at all" from
123
+ * "only stale candidates existed", which are different failures: the
124
+ * latter usually means the producer failed to persist the fresh
125
+ * report (harness-discovery C1).
126
+ */
127
+ staleRejected: PersistedReport[];
128
+ }
129
+ /**
130
+ * Select the freshest report for a given session_id, or the freshest
131
+ * applicable report when the persisted file lacks a sessionId field
132
+ * (older package versions). `report: null` when nothing matches.
133
+ *
134
+ * The strict (sessionId-equals) match always wins. The tolerant
135
+ * fallback's appetite is controlled by `opts.tolerantFallback` and
136
+ * `opts.maxFallbackAgeMs` — see `FindReportOptions`. The selection
137
+ * result carries enough context (`fallbackAdopted`, `staleRejected`)
138
+ * for the caller to be loud about non-strict adoptions.
139
+ */
140
+ export declare function selectReportForSession(reports: PersistedReport[], sessionId: string, opts?: FindReportOptions): FindReportSelection;
141
+ /**
142
+ * Back-compat wrapper around `selectReportForSession` for callers that
143
+ * only need the report (the gate read and expiry paths).
144
+ */
145
+ export declare function findLatestReportForSession(reports: PersistedReport[], sessionId: string, opts?: FindReportOptions): PersistedReport | null;
146
+ export interface PersistedReportApprovalCheck {
147
+ approved: boolean;
148
+ detail: string;
149
+ report: PersistedReport | null;
150
+ }
151
+ /**
152
+ * Flip the latest matching persisted report's approvalStatus to
153
+ * `expired` so it no longer satisfies the gate's persisted-report
154
+ * fallback (harness/1ee26e77 follow-up: post-tool-use expiry was
155
+ * marker-only; the persisted report at .understanding-gate/reports/
156
+ * silently kept satisfying the gate even after task_finish deleted
157
+ * the marker).
158
+ *
159
+ * Atomic rewrite. Preserves the rest of the report body so the audit
160
+ * trail (the operator's actual Understanding text + previous approval
161
+ * timestamps) stays intact; only the status fields change.
162
+ *
163
+ * Returns `{ ok: true, filePath, previousStatus }` on success,
164
+ * `{ ok: false, reason }` when no matching report exists or rewrite
165
+ * failed. Non-throwing: caller (post-tool-use hook) uses this as a
166
+ * best-effort cleanup so a missing report dir or unrelated I/O issue
167
+ * does not escalate into a session-breaking hook failure.
168
+ */
169
+ export declare function expirePersistedReport(reportsDir: string, sessionId: string, now?: Date): {
170
+ ok: true;
171
+ filePath: string;
172
+ previousStatus: string | null;
173
+ } | {
174
+ ok: false;
175
+ reason: string;
176
+ };
177
+ export declare function checkPersistedReport(reportsDir: string, sessionId: string): PersistedReportApprovalCheck;