@deftai/directive-core 0.103.0 → 0.105.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 (255) hide show
  1. package/dist/authz/classify.js +682 -35
  2. package/dist/check/cached-orchestrator.d.ts +1 -1
  3. package/dist/check/cached-orchestrator.js +2 -2
  4. package/dist/check/gate-lists.d.ts +3 -2
  5. package/dist/check/gate-lists.js +5 -2
  6. package/dist/check/index.d.ts +1 -0
  7. package/dist/check/index.js +1 -0
  8. package/dist/check/named-cause.d.ts +1 -1
  9. package/dist/check/named-cause.js +33 -3
  10. package/dist/check/session-completed-ac.d.ts +39 -0
  11. package/dist/check/session-completed-ac.js +198 -0
  12. package/dist/codebase/default-extractor.d.ts +5 -0
  13. package/dist/codebase/default-extractor.js +7 -15
  14. package/dist/deposit/copy-tree.js +60 -0
  15. package/dist/deposit/python-free.js +26 -18
  16. package/dist/doctor/checks.d.ts +8 -0
  17. package/dist/doctor/checks.js +37 -25
  18. package/dist/doctor/doctor-state.d.ts +4 -0
  19. package/dist/doctor/doctor-state.js +7 -1
  20. package/dist/doctor/main.js +9 -14
  21. package/dist/doctor/types.d.ts +9 -0
  22. package/dist/fs/contained-write.d.ts +115 -1
  23. package/dist/fs/contained-write.js +242 -5
  24. package/dist/fs/mutation-ledger.d.ts +54 -0
  25. package/dist/fs/mutation-ledger.js +133 -0
  26. package/dist/fs/non-product-dirs.d.ts +79 -0
  27. package/dist/fs/non-product-dirs.js +96 -0
  28. package/dist/hooks/dispatcher.d.ts +3 -1
  29. package/dist/hooks/dispatcher.js +14 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +3 -0
  32. package/dist/init-deposit/agent-hooks.js +17 -15
  33. package/dist/init-deposit/core-guard-pin-content.d.ts +7 -0
  34. package/dist/init-deposit/core-guard-pin-content.js +393 -0
  35. package/dist/init-deposit/gitignore.js +9 -0
  36. package/dist/init-deposit/hygiene.d.ts +47 -11
  37. package/dist/init-deposit/hygiene.js +172 -37
  38. package/dist/init-deposit/legacy-detect.js +6 -2
  39. package/dist/init-deposit/refresh.d.ts +21 -1
  40. package/dist/init-deposit/refresh.js +106 -9
  41. package/dist/init-deposit/scaffold.js +28 -279
  42. package/dist/init-deposit/slash-deposit.js +13 -7
  43. package/dist/init-deposit/xbrief-projections.d.ts +8 -1
  44. package/dist/init-deposit/xbrief-projections.js +24 -11
  45. package/dist/intake/clause-derivation.d.ts +74 -0
  46. package/dist/intake/clause-derivation.js +409 -0
  47. package/dist/intake/index.d.ts +1 -0
  48. package/dist/intake/index.js +1 -0
  49. package/dist/intake/issue-ingest.d.ts +3 -1
  50. package/dist/intake/issue-ingest.js +36 -43
  51. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +5 -0
  52. package/dist/lifecycle/completed-tracked-on-delivery.js +85 -18
  53. package/dist/literal-acceptance/capture.d.ts +10 -0
  54. package/dist/literal-acceptance/capture.js +224 -75
  55. package/dist/literal-acceptance/evaluate.d.ts +29 -4
  56. package/dist/literal-acceptance/evaluate.js +70 -2
  57. package/dist/literal-acceptance/index.d.ts +3 -3
  58. package/dist/literal-acceptance/index.js +3 -3
  59. package/dist/literal-acceptance/safety.d.ts +41 -0
  60. package/dist/literal-acceptance/safety.js +126 -2
  61. package/dist/literal-acceptance/types.d.ts +8 -0
  62. package/dist/orphan-active/evaluate.d.ts +7 -1
  63. package/dist/orphan-active/evaluate.js +102 -23
  64. package/dist/platform/platform-capabilities.js +2 -3
  65. package/dist/policy/base-branch.d.ts +37 -0
  66. package/dist/policy/base-branch.js +114 -0
  67. package/dist/policy/branch-sync.d.ts +80 -0
  68. package/dist/policy/branch-sync.js +211 -0
  69. package/dist/policy/ceremony-dial-escalation.js +1 -0
  70. package/dist/policy/ceremony-dial.js +4 -14
  71. package/dist/policy/delivery-branch.d.ts +5 -3
  72. package/dist/policy/delivery-branch.js +7 -3
  73. package/dist/policy/file-size-thresholds.d.ts +24 -0
  74. package/dist/policy/file-size-thresholds.js +23 -0
  75. package/dist/policy/forge-outage-retry.d.ts +47 -0
  76. package/dist/policy/forge-outage-retry.js +157 -0
  77. package/dist/policy/index.d.ts +7 -0
  78. package/dist/policy/index.js +66 -7
  79. package/dist/policy/project-invariants.d.ts +50 -0
  80. package/dist/policy/project-invariants.js +205 -0
  81. package/dist/policy/sync-default.d.ts +129 -0
  82. package/dist/policy/sync-default.js +452 -0
  83. package/dist/policy/sync-max-files.d.ts +93 -0
  84. package/dist/policy/sync-max-files.js +192 -0
  85. package/dist/pr-monitor/absent-required.d.ts +11 -0
  86. package/dist/pr-monitor/absent-required.js +61 -0
  87. package/dist/pr-monitor/constants.d.ts +7 -0
  88. package/dist/pr-monitor/constants.js +7 -0
  89. package/dist/pr-monitor/index.d.ts +2 -1
  90. package/dist/pr-monitor/index.js +2 -1
  91. package/dist/pr-monitor/main.js +4 -0
  92. package/dist/pr-monitor/monitor.js +24 -1
  93. package/dist/pr-wait-mergeable/classify.js +6 -0
  94. package/dist/preflight/evaluate.d.ts +9 -0
  95. package/dist/preflight/evaluate.js +49 -1
  96. package/dist/preflight/index.d.ts +5 -1
  97. package/dist/preflight/index.js +2 -0
  98. package/dist/preflight/intended-placement.d.ts +62 -0
  99. package/dist/preflight/intended-placement.js +275 -0
  100. package/dist/preflight/project-invariants-gate.d.ts +29 -0
  101. package/dist/preflight/project-invariants-gate.js +82 -0
  102. package/dist/product-first-done-gate/acceptance-resolver.d.ts +128 -0
  103. package/dist/product-first-done-gate/acceptance-resolver.js +144 -0
  104. package/dist/product-first-done-gate/acceptance.d.ts +7 -4
  105. package/dist/product-first-done-gate/acceptance.js +79 -8
  106. package/dist/product-first-done-gate/empty-resolution.d.ts +5 -2
  107. package/dist/product-first-done-gate/empty-resolution.js +13 -2
  108. package/dist/product-first-done-gate/evaluate.d.ts +29 -0
  109. package/dist/product-first-done-gate/evaluate.js +307 -33
  110. package/dist/product-first-done-gate/index.d.ts +3 -1
  111. package/dist/product-first-done-gate/index.js +2 -1
  112. package/dist/release/build-dist.d.ts +101 -0
  113. package/dist/release/build-dist.js +428 -46
  114. package/dist/run-summary/emit.d.ts +50 -4
  115. package/dist/run-summary/emit.js +262 -26
  116. package/dist/run-summary/index.d.ts +3 -2
  117. package/dist/run-summary/index.js +2 -1
  118. package/dist/run-summary/session-id.d.ts +21 -0
  119. package/dist/run-summary/session-id.js +92 -0
  120. package/dist/run-summary/share.d.ts +1 -1
  121. package/dist/run-summary/share.js +2 -2
  122. package/dist/run-summary/types.d.ts +38 -6
  123. package/dist/run-summary/types.js +1 -0
  124. package/dist/scope/acceptance-activate-gate.d.ts +1 -1
  125. package/dist/scope/acceptance-activate-gate.js +4 -4
  126. package/dist/scope/acceptance-evidence.d.ts +27 -0
  127. package/dist/scope/acceptance-evidence.js +66 -0
  128. package/dist/scope/capacity-stamp.d.ts +2 -0
  129. package/dist/scope/capacity-stamp.js +4 -0
  130. package/dist/scope/delivery-evidence.d.ts +9 -3
  131. package/dist/scope/delivery-evidence.js +39 -27
  132. package/dist/scope/index.d.ts +1 -0
  133. package/dist/scope/index.js +1 -0
  134. package/dist/scope/parent-lineage.js +7 -6
  135. package/dist/scope/project-invariant-coverage.d.ts +54 -0
  136. package/dist/scope/project-invariant-coverage.js +322 -0
  137. package/dist/scope/transition.js +66 -4
  138. package/dist/scope-provenance/compare-intent.d.ts +27 -0
  139. package/dist/scope-provenance/compare-intent.js +233 -0
  140. package/dist/scope-provenance/digest.d.ts +18 -1
  141. package/dist/scope-provenance/digest.js +16 -11
  142. package/dist/scope-provenance/evaluate.d.ts +5 -1
  143. package/dist/scope-provenance/evaluate.js +64 -4
  144. package/dist/scope-provenance/extract-intent.d.ts +43 -0
  145. package/dist/scope-provenance/extract-intent.js +262 -0
  146. package/dist/scope-provenance/index.d.ts +7 -1
  147. package/dist/scope-provenance/index.js +7 -1
  148. package/dist/scope-provenance/intent-digest.d.ts +12 -0
  149. package/dist/scope-provenance/intent-digest.js +33 -0
  150. package/dist/scope-provenance/intent-evaluate.d.ts +43 -0
  151. package/dist/scope-provenance/intent-evaluate.js +149 -0
  152. package/dist/scope-provenance/json-tokenizer.d.ts +19 -0
  153. package/dist/scope-provenance/json-tokenizer.js +225 -0
  154. package/dist/scope-provenance/known-machine.d.ts +38 -0
  155. package/dist/scope-provenance/known-machine.js +157 -0
  156. package/dist/scope-provenance/mint-artifacts.d.ts +102 -0
  157. package/dist/scope-provenance/mint-artifacts.js +425 -0
  158. package/dist/session/ac-pass-banking.d.ts +4 -36
  159. package/dist/session/ac-pass-banking.js +30 -77
  160. package/dist/session/ac-pass-reuse.d.ts +53 -0
  161. package/dist/session/ac-pass-reuse.js +95 -0
  162. package/dist/session/ceremony-dial-evidence.d.ts +42 -0
  163. package/dist/session/ceremony-dial-evidence.js +194 -0
  164. package/dist/session/git.js +19 -4
  165. package/dist/session/index.d.ts +5 -0
  166. package/dist/session/index.js +5 -0
  167. package/dist/session/occupancy-stress-worker.d.ts +7 -0
  168. package/dist/session/occupancy-stress-worker.js +36 -0
  169. package/dist/session/occupancy.d.ts +97 -0
  170. package/dist/session/occupancy.js +422 -0
  171. package/dist/session/openclaw-soft-rebind-deposit.js +6 -6
  172. package/dist/session/product-state-hash.d.ts +26 -0
  173. package/dist/session/product-state-hash.js +683 -0
  174. package/dist/session/session-ready.d.ts +2 -0
  175. package/dist/session/session-ready.js +52 -30
  176. package/dist/session/session-start.d.ts +7 -0
  177. package/dist/session/session-start.js +88 -5
  178. package/dist/session/verify-ac-session-cache.d.ts +42 -0
  179. package/dist/session/verify-ac-session-cache.js +126 -0
  180. package/dist/session/verify-session-ritual.d.ts +5 -0
  181. package/dist/session/verify-session-ritual.js +9 -0
  182. package/dist/slash/openclaw-deposit.js +13 -9
  183. package/dist/slice/constants.d.ts +2 -2
  184. package/dist/slice/constants.js +2 -2
  185. package/dist/slice/existing.js +21 -4
  186. package/dist/slice/lock.d.ts +16 -1
  187. package/dist/slice/lock.js +137 -11
  188. package/dist/slice/record.d.ts +8 -5
  189. package/dist/slice/record.js +21 -13
  190. package/dist/story-ready/evaluate.d.ts +2 -0
  191. package/dist/story-ready/evaluate.js +13 -0
  192. package/dist/swarm/complete-cohort.d.ts +3 -0
  193. package/dist/swarm/complete-cohort.js +59 -11
  194. package/dist/swarm/finalize-cohort.js +33 -2
  195. package/dist/swarm/launch.d.ts +22 -0
  196. package/dist/swarm/launch.js +155 -0
  197. package/dist/swarm/routing-set-cli.d.ts +11 -1
  198. package/dist/swarm/routing-set-cli.js +13 -3
  199. package/dist/swarm/routing-verify.d.ts +12 -0
  200. package/dist/swarm/routing-verify.js +59 -20
  201. package/dist/swarm/routing.d.ts +14 -0
  202. package/dist/swarm/routing.js +53 -3
  203. package/dist/telemetry-coverage/evaluate.d.ts +42 -0
  204. package/dist/telemetry-coverage/evaluate.js +148 -0
  205. package/dist/telemetry-coverage/fake-trial.d.ts +45 -0
  206. package/dist/telemetry-coverage/fake-trial.js +204 -0
  207. package/dist/telemetry-coverage/index.d.ts +5 -0
  208. package/dist/telemetry-coverage/index.js +5 -0
  209. package/dist/telemetry-coverage/kinds.d.ts +19 -0
  210. package/dist/telemetry-coverage/kinds.js +54 -0
  211. package/dist/telemetry-coverage/scan-callers.d.ts +25 -0
  212. package/dist/telemetry-coverage/scan-callers.js +168 -0
  213. package/dist/triage/actions/index.js +4 -1
  214. package/dist/ts-check-lane/index.d.ts +3 -0
  215. package/dist/ts-check-lane/index.js +2 -0
  216. package/dist/ts-check-lane/progress-reporter.d.ts +17 -0
  217. package/dist/ts-check-lane/progress-reporter.js +38 -0
  218. package/dist/ts-check-lane/progress.d.ts +32 -0
  219. package/dist/ts-check-lane/progress.js +57 -0
  220. package/dist/ts-check-lane/run-lane.d.ts +3 -1
  221. package/dist/ts-check-lane/run-lane.js +8 -6
  222. package/dist/validate-content/validate-links.d.ts +8 -0
  223. package/dist/validate-content/validate-links.js +9 -15
  224. package/dist/value/feedback-file.js +12 -2
  225. package/dist/vbrief-build/index.d.ts +1 -0
  226. package/dist/vbrief-build/index.js +1 -0
  227. package/dist/vbrief-build/project-invariants-io.d.ts +16 -0
  228. package/dist/vbrief-build/project-invariants-io.js +41 -0
  229. package/dist/vbrief-reconcile/graph.js +18 -1
  230. package/dist/vbrief-reconcile/index.d.ts +3 -1
  231. package/dist/vbrief-reconcile/index.js +2 -1
  232. package/dist/vbrief-reconcile/origin-freshness.d.ts +44 -0
  233. package/dist/vbrief-reconcile/origin-freshness.js +221 -0
  234. package/dist/vbrief-reconcile/types.d.ts +13 -0
  235. package/dist/vbrief-reconcile/umbrellas.d.ts +14 -0
  236. package/dist/vbrief-reconcile/umbrellas.js +261 -26
  237. package/dist/vbrief-validate/index.d.ts +1 -1
  238. package/dist/vbrief-validate/index.js +1 -1
  239. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  240. package/dist/vbrief-validate/plan-hooks.js +68 -0
  241. package/dist/verify-ac/evaluate.d.ts +7 -1
  242. package/dist/verify-ac/evaluate.js +34 -26
  243. package/dist/verify-ac/flag.d.ts +4 -0
  244. package/dist/verify-ac/flag.js +16 -0
  245. package/dist/verify-ac/index.d.ts +2 -2
  246. package/dist/verify-ac/index.js +2 -2
  247. package/dist/verify-source/contained-writes.js +2 -0
  248. package/dist/verify-source/index.d.ts +1 -0
  249. package/dist/verify-source/index.js +1 -0
  250. package/dist/verify-source/verify-stubs.d.ts +15 -1
  251. package/dist/verify-source/verify-stubs.js +20 -16
  252. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +5 -6
  253. package/dist/vitest-runner/win32-coverage-tmp-setup.js +5 -6
  254. package/dist/xbrief-migrate/migrate-project.js +2 -5
  255. package/package.json +3 -3
@@ -15,10 +15,25 @@
15
15
  * onto this API over bespoke per-sink checks when equivalent. See
16
16
  * `docs/reference/contained-write.md`.
17
17
  *
18
- * Refs #2951.
18
+ * Delete coverage (#3392 / #3456): real dest deletes MUST go through
19
+ * {@link containedRemove} so a bound ledger (and ADR-004 record mode) records
20
+ * them. Raw `rmSync` on a dest path is a chokepoint bypass.
21
+ *
22
+ * Plan-mode zero-mutation (#3456 / ADR-004): `deft update --dry-run` binds
23
+ * this port in record mode (`runInPortRecordMode`). Dest write / remove /
24
+ * chmod / dest-mutating exec record and skip dest IO. Temp-file cleanups
25
+ * either use this chokepoint or live outside the project root.
26
+ *
27
+ * `tryCleanupLegacyDeftTree` is not on the update/dry-run path (classify
28
+ * refuses `migration-required` first). Its default removeDir still routes
29
+ * through containedRemove so the site is not a silent exception.
30
+ *
31
+ * Refs #2951 / #3392 / #3456.
19
32
  */
20
- import { closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, realpathSync, writeSync, } from "node:fs";
21
- import { dirname, isAbsolute, relative, resolve } from "node:path";
33
+ import { execFileSync } from "node:child_process";
34
+ import { chmodSync, closeSync, constants, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, realpathSync, renameSync, rmSync, unlinkSync, writeSync, } from "node:fs";
35
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
36
+ import { isAtomicWriteTemp, isPortRecordMode, recordActiveMutation, } from "./mutation-ledger.js";
22
37
  import { assertWriteTargetSafe, ProjectionContainmentError } from "./projection-containment.js";
23
38
  /** Stable machine-readable error codes for contained-write refusals. */
24
39
  export const ContainedWriteErrorCode = {
@@ -133,6 +148,68 @@ function ensureParents(rootAbs, targetAbs) {
133
148
  function toBuffer(data, encoding) {
134
149
  return Buffer.isBuffer(data) ? data : Buffer.from(data, encoding);
135
150
  }
151
+ /**
152
+ * Remove resolver: lexical nest + refuse parent-path symlinks. The leaf may be
153
+ * a symlink — unlink the link itself (never follow, never abort).
154
+ */
155
+ function resolveExistingRootForRemove(root, target) {
156
+ const rootAbs = resolve(root);
157
+ try {
158
+ realpathSync(rootAbs);
159
+ }
160
+ catch {
161
+ throw new ContainedWriteError(`contained write refused: root ${rootAbs} does not exist`, {
162
+ code: ContainedWriteErrorCode.ROOT_MISSING,
163
+ root: rootAbs,
164
+ target: String(target),
165
+ offendingPath: rootAbs,
166
+ });
167
+ }
168
+ const targetAbs = resolveContainedTarget(rootAbs, target);
169
+ const parentAbs = dirname(targetAbs);
170
+ if (parentAbs !== rootAbs && parentAbs !== targetAbs) {
171
+ const rel = relative(rootAbs, parentAbs);
172
+ if (rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel)) {
173
+ let current = rootAbs;
174
+ for (const segment of rel.split(/[\\/]+/).filter((part) => part.length > 0)) {
175
+ current = join(current, segment);
176
+ let info;
177
+ try {
178
+ info = lstatSync(current);
179
+ }
180
+ catch {
181
+ break;
182
+ }
183
+ if (info.isSymbolicLink()) {
184
+ throw new ContainedWriteError(`contained write refused: ${current} is a symlink on the remove parent path`, {
185
+ code: ContainedWriteErrorCode.SYMLINK,
186
+ root: rootAbs,
187
+ target: targetAbs,
188
+ offendingPath: current,
189
+ });
190
+ }
191
+ }
192
+ }
193
+ }
194
+ return { rootAbs, targetAbs };
195
+ }
196
+ function recordWriteMutation(targetAbs, mutation) {
197
+ if (mutation === false)
198
+ return;
199
+ const kind = mutation?.kind ?? "wrote";
200
+ const path = mutation?.path ?? targetAbs;
201
+ if (mutation?.path === undefined && isAtomicWriteTemp(path))
202
+ return;
203
+ recordActiveMutation(kind, path);
204
+ }
205
+ function recordRemoveMutation(targetAbs, mutation) {
206
+ if (mutation === false)
207
+ return;
208
+ const path = mutation?.path ?? targetAbs;
209
+ if (mutation?.path === undefined && isAtomicWriteTemp(path))
210
+ return;
211
+ recordActiveMutation("deleted", path);
212
+ }
136
213
  function writeNoFollow(targetAbs, buf, flags) {
137
214
  const fd = openSync(targetAbs, flags, 0o644);
138
215
  try {
@@ -144,6 +221,7 @@ function writeNoFollow(targetAbs, buf, flags) {
144
221
  }
145
222
  offset += n;
146
223
  }
224
+ fsyncSync(fd);
147
225
  return offset;
148
226
  }
149
227
  finally {
@@ -193,6 +271,27 @@ export function containedWrite(input) {
193
271
  }
194
272
  throw err;
195
273
  }
274
+ const encoding = input.encoding ?? "utf8";
275
+ const buf = toBuffer(input.data, encoding);
276
+ if (isPortRecordMode()) {
277
+ if (input.mode === "create") {
278
+ try {
279
+ lstatSync(targetAbs);
280
+ throw new ContainedWriteError(`contained write refused: target ${targetAbs} already exists (mode=create)`, {
281
+ code: ContainedWriteErrorCode.EXISTS,
282
+ root: rootAbs,
283
+ target: targetAbs,
284
+ offendingPath: targetAbs,
285
+ });
286
+ }
287
+ catch (err) {
288
+ if (err instanceof ContainedWriteError)
289
+ throw err;
290
+ }
291
+ }
292
+ recordWriteMutation(targetAbs, input.mutation);
293
+ return { path: targetAbs, bytesWritten: buf.length, mode: input.mode };
294
+ }
196
295
  const doMkdir = input.mkdir !== false;
197
296
  if (doMkdir) {
198
297
  ensureParents(rootAbs, targetAbs);
@@ -207,8 +306,6 @@ export function containedWrite(input) {
207
306
  }
208
307
  throw err;
209
308
  }
210
- const encoding = input.encoding ?? "utf8";
211
- const buf = toBuffer(input.data, encoding);
212
309
  let exists = false;
213
310
  try {
214
311
  lstatSync(targetAbs);
@@ -238,6 +335,7 @@ export function containedWrite(input) {
238
335
  // replace
239
336
  bytesWritten = writeNoFollow(targetAbs, buf, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW);
240
337
  }
338
+ recordWriteMutation(targetAbs, input.mutation);
241
339
  return { path: targetAbs, bytesWritten, mode: input.mode };
242
340
  }
243
341
  catch (err) {
@@ -278,4 +376,143 @@ export function containedWrite(input) {
278
376
  });
279
377
  }
280
378
  }
379
+ /**
380
+ * Contained remove: resolve under root, refuse parent-path symlink / out-of-root,
381
+ * then delete. An in-root leaf symlink is unlinked (not followed, not refused).
382
+ * Missing targets are a no-op (not ledgered).
383
+ *
384
+ * @throws {ContainedWriteError} on containment refusal
385
+ */
386
+ export function containedRemove(input) {
387
+ const { rootAbs, targetAbs } = resolveExistingRootForRemove(input.root, input.target);
388
+ let info;
389
+ try {
390
+ info = lstatSync(targetAbs);
391
+ }
392
+ catch (err) {
393
+ const code = typeof err === "object" && err !== null && "code" in err
394
+ ? err.code
395
+ : undefined;
396
+ if (code === "ENOENT") {
397
+ return { path: targetAbs, removed: false };
398
+ }
399
+ const msg = err instanceof Error ? err.message : String(err);
400
+ throw new ContainedWriteError(`contained write I/O failed: ${msg}`, {
401
+ code: ContainedWriteErrorCode.IO,
402
+ root: rootAbs,
403
+ target: targetAbs,
404
+ offendingPath: targetAbs,
405
+ });
406
+ }
407
+ if (isPortRecordMode()) {
408
+ recordRemoveMutation(targetAbs, input.mutation);
409
+ return { path: targetAbs, removed: true };
410
+ }
411
+ try {
412
+ if (info.isSymbolicLink()) {
413
+ unlinkSync(targetAbs);
414
+ }
415
+ else {
416
+ rmSync(targetAbs, { force: true, recursive: input.recursive === true });
417
+ }
418
+ }
419
+ catch (err) {
420
+ const msg = err instanceof Error ? err.message : String(err);
421
+ throw new ContainedWriteError(`contained write I/O failed: ${msg}`, {
422
+ code: ContainedWriteErrorCode.IO,
423
+ root: rootAbs,
424
+ target: targetAbs,
425
+ offendingPath: targetAbs,
426
+ });
427
+ }
428
+ recordRemoveMutation(targetAbs, input.mutation);
429
+ return { path: targetAbs, removed: true };
430
+ }
431
+ /**
432
+ * Contained mkdir: nest under root. Record mode skips dest IO (dirs are not
433
+ * dest-file membership). Live path creates recursively.
434
+ */
435
+ export function containedMkdir(input) {
436
+ const targetAbs = resolveContainedTarget(input.root, input.target);
437
+ if (isPortRecordMode()) {
438
+ return { path: targetAbs };
439
+ }
440
+ mkdirSync(targetAbs, { recursive: true });
441
+ return { path: targetAbs };
442
+ }
443
+ /**
444
+ * Contained chmod: resolve under root, refuse parent-path symlink / out-of-root,
445
+ * then set mode. Record mode records `chmod` and skips dest IO.
446
+ */
447
+ export function containedChmod(input) {
448
+ const { rootAbs, targetAbs } = resolveExistingRootForRemove(input.root, input.target);
449
+ if (input.mutation !== false) {
450
+ recordActiveMutation("chmod", input.mutation?.path ?? targetAbs);
451
+ }
452
+ if (isPortRecordMode()) {
453
+ return { path: targetAbs };
454
+ }
455
+ try {
456
+ chmodSync(targetAbs, input.mode);
457
+ }
458
+ catch (err) {
459
+ const msg = err instanceof Error ? err.message : String(err);
460
+ throw new ContainedWriteError(`contained write I/O failed: ${msg}`, {
461
+ code: ContainedWriteErrorCode.IO,
462
+ root: rootAbs,
463
+ target: targetAbs,
464
+ offendingPath: targetAbs,
465
+ });
466
+ }
467
+ return { path: targetAbs };
468
+ }
469
+ /**
470
+ * Contained rename: both paths must nest under root. Record mode records
471
+ * `wrote` on `to` (unless `mutation: false`) and skips dest IO.
472
+ */
473
+ export function containedRename(input) {
474
+ const fromAbs = resolveContainedTarget(input.root, input.from);
475
+ const toAbs = resolveContainedTarget(input.root, input.to);
476
+ if (input.mutation !== false) {
477
+ const kind = input.mutation?.kind ?? "wrote";
478
+ recordActiveMutation(kind, input.mutation?.path ?? toAbs);
479
+ }
480
+ if (isPortRecordMode()) {
481
+ return { from: fromAbs, to: toAbs };
482
+ }
483
+ try {
484
+ renameSync(fromAbs, toAbs);
485
+ }
486
+ catch (err) {
487
+ const msg = err instanceof Error ? err.message : String(err);
488
+ throw new ContainedWriteError(`contained write I/O failed: ${msg}`, {
489
+ code: ContainedWriteErrorCode.IO,
490
+ root: resolve(input.root),
491
+ target: toAbs,
492
+ offendingPath: toAbs,
493
+ });
494
+ }
495
+ return { from: fromAbs, to: toAbs };
496
+ }
497
+ /**
498
+ * Dest-mutating exec (git config, git add, …). Record mode records `exec` on
499
+ * `destTarget` and skips the child process. Allowlisted as the port impl.
500
+ */
501
+ export function containedDestExec(input) {
502
+ const targetAbs = resolveContainedTarget(input.root, input.destTarget);
503
+ recordActiveMutation("exec", targetAbs);
504
+ if (isPortRecordMode()) {
505
+ return { ok: true, stdout: "" };
506
+ }
507
+ try {
508
+ const stdout = execFileSync(input.file, [...input.args], {
509
+ encoding: "utf8",
510
+ cwd: resolve(input.root),
511
+ });
512
+ return { ok: true, stdout };
513
+ }
514
+ catch {
515
+ return { ok: false, stdout: "" };
516
+ }
517
+ }
281
518
  //# sourceMappingURL=contained-write.js.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * mutation-ledger.ts — typed deposit mutation log at the write chokepoint (#3392).
3
+ *
4
+ * A bound ledger records wrote / stripped / deleted as a side effect of
5
+ * containedWrite / containedRemove. Callers cannot discard mutations by
6
+ * ignoring a writer return value.
7
+ *
8
+ * Refs #3392 / #3378.
9
+ */
10
+ export declare const MUTATION_KINDS: readonly ["wrote", "stripped", "deleted", "chmod", "exec"];
11
+ export type MutationKind = (typeof MUTATION_KINDS)[number];
12
+ export interface MutationEntry {
13
+ readonly kind: MutationKind;
14
+ readonly path: string;
15
+ }
16
+ export interface MutationSummary {
17
+ readonly wrote: readonly string[];
18
+ readonly stripped: readonly string[];
19
+ readonly deleted: readonly string[];
20
+ readonly chmod: readonly string[];
21
+ readonly exec: readonly string[];
22
+ readonly mutations: readonly MutationEntry[];
23
+ }
24
+ export declare function isAtomicWriteTemp(path: string): boolean;
25
+ /** Posix-relative path under `root`, or empty when the target is not nested. */
26
+ export declare function toLedgerPath(root: string, target: string): string;
27
+ export declare function emptyMutationSummary(): MutationSummary;
28
+ /** In-memory ledger for one deposit/refresh run. */
29
+ export declare class MutationLedger {
30
+ readonly root: string;
31
+ private readonly recorded;
32
+ constructor(root: string);
33
+ record(kind: MutationKind, target: string): void;
34
+ entries(): readonly MutationEntry[];
35
+ summarize(): MutationSummary;
36
+ }
37
+ export declare function activeMutationLedger(): MutationLedger | undefined;
38
+ /** ADR-004: dest-mutating port calls record and skip dest IO. */
39
+ export declare function runInPortRecordMode<T>(fn: () => T): T;
40
+ export declare function isPortRecordMode(): boolean;
41
+ /** Bind a ledger for the duration of `fn` (including awaited continuations). */
42
+ export declare function runWithMutationLedger<T>(root: string, fn: () => T): T;
43
+ export declare function recordActiveMutation(kind: MutationKind, target: string): void;
44
+ export declare function snapshotMutationSummary(): MutationSummary;
45
+ /** Human summary: `Removed:` plus wrote/stripped. Empty when nothing mutated. */
46
+ export declare function formatMutationSummary(summary: MutationSummary): string;
47
+ export declare function mutationSummaryJson(summary: MutationSummary): {
48
+ wrote: string[];
49
+ stripped: string[];
50
+ deleted: string[];
51
+ chmod: string[];
52
+ exec: string[];
53
+ };
54
+ //# sourceMappingURL=mutation-ledger.d.ts.map
@@ -0,0 +1,133 @@
1
+ /**
2
+ * mutation-ledger.ts — typed deposit mutation log at the write chokepoint (#3392).
3
+ *
4
+ * A bound ledger records wrote / stripped / deleted as a side effect of
5
+ * containedWrite / containedRemove. Callers cannot discard mutations by
6
+ * ignoring a writer return value.
7
+ *
8
+ * Refs #3392 / #3378.
9
+ */
10
+ import { AsyncLocalStorage } from "node:async_hooks";
11
+ import { isAbsolute, relative, resolve } from "node:path";
12
+ export const MUTATION_KINDS = ["wrote", "stripped", "deleted", "chmod", "exec"];
13
+ /** Atomic-replace temps (`name.deft-<pid>.tmp`) are not logical mutations. */
14
+ const ATOMIC_TMP = /\.deft-\d+\.tmp$/u;
15
+ export function isAtomicWriteTemp(path) {
16
+ return ATOMIC_TMP.test(path.replace(/\\/g, "/"));
17
+ }
18
+ /** Posix-relative path under `root`, or empty when the target is not nested. */
19
+ export function toLedgerPath(root, target) {
20
+ const rootAbs = resolve(root);
21
+ const targetAbs = isAbsolute(target) ? resolve(target) : resolve(rootAbs, target);
22
+ const rel = relative(rootAbs, targetAbs);
23
+ if (rel.length === 0 || rel.startsWith("..") || isAbsolute(rel))
24
+ return "";
25
+ return rel.split("\\").join("/");
26
+ }
27
+ export function emptyMutationSummary() {
28
+ return { wrote: [], stripped: [], deleted: [], chmod: [], exec: [], mutations: [] };
29
+ }
30
+ function summarizeEntries(entries) {
31
+ const latest = new Map();
32
+ const order = [];
33
+ for (const entry of entries) {
34
+ if (!latest.has(entry.path))
35
+ order.push(entry.path);
36
+ latest.set(entry.path, entry.kind);
37
+ }
38
+ const wrote = [];
39
+ const stripped = [];
40
+ const deleted = [];
41
+ const chmod = [];
42
+ const exec = [];
43
+ const mutations = [];
44
+ for (const path of order) {
45
+ const kind = latest.get(path);
46
+ if (kind === undefined)
47
+ continue;
48
+ mutations.push({ kind, path });
49
+ if (kind === "wrote")
50
+ wrote.push(path);
51
+ else if (kind === "stripped")
52
+ stripped.push(path);
53
+ else if (kind === "chmod")
54
+ chmod.push(path);
55
+ else if (kind === "exec")
56
+ exec.push(path);
57
+ else
58
+ deleted.push(path);
59
+ }
60
+ return { wrote, stripped, deleted, chmod, exec, mutations };
61
+ }
62
+ /** In-memory ledger for one deposit/refresh run. */
63
+ export class MutationLedger {
64
+ root;
65
+ recorded = [];
66
+ constructor(root) {
67
+ this.root = resolve(root);
68
+ }
69
+ record(kind, target) {
70
+ const path = toLedgerPath(this.root, target);
71
+ if (path.length === 0)
72
+ return;
73
+ this.recorded.push({ kind, path });
74
+ }
75
+ entries() {
76
+ return this.recorded;
77
+ }
78
+ summarize() {
79
+ return summarizeEntries(this.recorded);
80
+ }
81
+ }
82
+ const storage = new AsyncLocalStorage();
83
+ const recordModeStorage = new AsyncLocalStorage();
84
+ export function activeMutationLedger() {
85
+ return storage.getStore();
86
+ }
87
+ /** ADR-004: dest-mutating port calls record and skip dest IO. */
88
+ export function runInPortRecordMode(fn) {
89
+ return recordModeStorage.run(true, fn);
90
+ }
91
+ export function isPortRecordMode() {
92
+ return recordModeStorage.getStore() === true;
93
+ }
94
+ /** Bind a ledger for the duration of `fn` (including awaited continuations). */
95
+ export function runWithMutationLedger(root, fn) {
96
+ return storage.run(new MutationLedger(root), fn);
97
+ }
98
+ export function recordActiveMutation(kind, target) {
99
+ activeMutationLedger()?.record(kind, target);
100
+ }
101
+ export function snapshotMutationSummary() {
102
+ return activeMutationLedger()?.summarize() ?? emptyMutationSummary();
103
+ }
104
+ /** Human summary: `Removed:` plus wrote/stripped. Empty when nothing mutated. */
105
+ export function formatMutationSummary(summary) {
106
+ const lines = [];
107
+ if (summary.deleted.length > 0) {
108
+ lines.push(`Removed: ${summary.deleted.join(", ")}`);
109
+ }
110
+ if (summary.wrote.length > 0) {
111
+ lines.push(`wrote: ${summary.wrote.join(", ")}`);
112
+ }
113
+ if (summary.stripped.length > 0) {
114
+ lines.push(`stripped: ${summary.stripped.join(", ")}`);
115
+ }
116
+ if (summary.chmod.length > 0) {
117
+ lines.push(`chmod: ${summary.chmod.join(", ")}`);
118
+ }
119
+ if (summary.exec.length > 0) {
120
+ lines.push(`exec: ${summary.exec.join(", ")}`);
121
+ }
122
+ return lines.length > 0 ? `${lines.join("\n")}\n` : "";
123
+ }
124
+ export function mutationSummaryJson(summary) {
125
+ return {
126
+ wrote: [...summary.wrote],
127
+ stripped: [...summary.stripped],
128
+ deleted: [...summary.deleted],
129
+ chmod: [...summary.chmod],
130
+ exec: [...summary.exec],
131
+ };
132
+ }
133
+ //# sourceMappingURL=mutation-ledger.js.map
@@ -0,0 +1,79 @@
1
+ /**
2
+ * non-product-dirs.ts — one source of truth for the "not product source"
3
+ * directory basenames shared by every Directive filesystem walk (#3487).
4
+ *
5
+ * Directive shipped four independently hand-maintained exclusion sets:
6
+ *
7
+ * - `EXCLUDE_DIRS` — `verify-source/verify-stubs.ts`
8
+ * - `EXCLUDE_DIRS` — `validate-content/validate-links.ts`
9
+ * - `SKIP_DIRS` — `codebase/default-extractor.ts`
10
+ * - `DEFAULT_EXCLUDES` — `release/build-dist.ts`
11
+ *
12
+ * They drifted from each other, which is exactly how #2953 added
13
+ * `.deft-scratch` / `swarm-worktrees` to some walks and missed the
14
+ * `verify-stubs` walk (#3481), and how none of them ever learned about agent
15
+ * working directories even though Directive ships first-class support for
16
+ * several agent hosts. Because `@deftai/directive-core` is published, a
17
+ * consumer cannot patch any of the four.
18
+ *
19
+ * `NON_PRODUCT_DIRS` is the common core each walk now extends. It carries only
20
+ * basenames that are never product source in any repo: VCS metadata,
21
+ * dependency trees, build output, tool caches, and agent working directories.
22
+ * Walk-specific entries (`tests`, `specs`, `.planning`, `htmlcov`, …) stay with
23
+ * the walk that needs them — this constant is a floor, not a ceiling.
24
+ *
25
+ * Adding a directory here is a one-line change all four walks pick up, and
26
+ * `non-product-dirs.test.ts` fails if any of them stops agreeing.
27
+ *
28
+ * Refs #3487, #3481, #2953, #2954, #1656.
29
+ */
30
+ /**
31
+ * Swarm / agent scratch worktree roots (#2953, #1656).
32
+ *
33
+ * `.deft-scratch` is the current layout (`.deft-scratch/worktrees/<story-id>`,
34
+ * built by `swarm/launch.ts`); `swarm-worktrees` is the pre-`.deft-scratch`
35
+ * name, kept for repos that still carry one.
36
+ */
37
+ export declare const AGENT_SCRATCH_DIRS: readonly string[];
38
+ /**
39
+ * Agent-host directories that hold host *working* state rather than product
40
+ * source (#3487).
41
+ *
42
+ * Only names with evidence of a real working directory belong here. Directive
43
+ * knows several host directories (`.claude`, `.cursor`, `.grok`, `.codex`,
44
+ * `.github/skills`, `.agents`), but most of them hold nothing except
45
+ * Directive's own committed deposits — `commands.md` § Native multi-host
46
+ * registration (LockedDecision L8) says to *commit* those, so blanket-excluding
47
+ * them would hide committed team surface for no measured gain.
48
+ *
49
+ * `.claude` is different in kind: the Claude Code host keeps live session state
50
+ * in the same directory it keeps config — `settings.local.json`, and agent
51
+ * worktrees under `.claude/worktrees/<agent-id>`, each a full nested checkout.
52
+ * Three such worktrees added ~11,000 walkable entries to this repo
53
+ * (9,128 → 20,393) that no Directive walk skipped. Its Directive-managed
54
+ * contents (`.claude/settings.json`, `.claude/commands/*.md`,
55
+ * `.claude/skills/`) are deposits generated from `content/`, which every walk
56
+ * still visits at the authoring source.
57
+ *
58
+ * Deliberately **not** listed:
59
+ * - `.openclaw` — there is no project-level `.openclaw/`; the host state dir
60
+ * is `~/.openclaw` in `$HOME` (see `doctor/openclaw-skills.ts`), and
61
+ * inventing a project one is explicitly forbidden by
62
+ * `content/docs/openclaw-agent-host.md` and asserted against in
63
+ * `slash/openclaw-adapter.test.ts`.
64
+ * - `.cursor`, `.grok`, `.codex`, `.github` — committed Directive deposits
65
+ * (`.cursor/hooks.json`, `.grok/commands/`, `.codex/prompts/`,
66
+ * `.github/skills/`) with no known working/worktree subtree. No evidence
67
+ * of a scratch convention was found for any of them.
68
+ */
69
+ export declare const AGENT_HOST_WORKING_DIRS: readonly string[];
70
+ /**
71
+ * Directory basenames no Directive walk should descend into.
72
+ *
73
+ * Every walk extends this set; none may shrink it. Keep entries to basenames
74
+ * that cannot be product source in any repo — a name that is merely *usually*
75
+ * uninteresting to one gate (`tests`, `scripts`, `specs`) belongs to that
76
+ * walk's own set, not here.
77
+ */
78
+ export declare const NON_PRODUCT_DIRS: ReadonlySet<string>;
79
+ //# sourceMappingURL=non-product-dirs.d.ts.map
@@ -0,0 +1,96 @@
1
+ /**
2
+ * non-product-dirs.ts — one source of truth for the "not product source"
3
+ * directory basenames shared by every Directive filesystem walk (#3487).
4
+ *
5
+ * Directive shipped four independently hand-maintained exclusion sets:
6
+ *
7
+ * - `EXCLUDE_DIRS` — `verify-source/verify-stubs.ts`
8
+ * - `EXCLUDE_DIRS` — `validate-content/validate-links.ts`
9
+ * - `SKIP_DIRS` — `codebase/default-extractor.ts`
10
+ * - `DEFAULT_EXCLUDES` — `release/build-dist.ts`
11
+ *
12
+ * They drifted from each other, which is exactly how #2953 added
13
+ * `.deft-scratch` / `swarm-worktrees` to some walks and missed the
14
+ * `verify-stubs` walk (#3481), and how none of them ever learned about agent
15
+ * working directories even though Directive ships first-class support for
16
+ * several agent hosts. Because `@deftai/directive-core` is published, a
17
+ * consumer cannot patch any of the four.
18
+ *
19
+ * `NON_PRODUCT_DIRS` is the common core each walk now extends. It carries only
20
+ * basenames that are never product source in any repo: VCS metadata,
21
+ * dependency trees, build output, tool caches, and agent working directories.
22
+ * Walk-specific entries (`tests`, `specs`, `.planning`, `htmlcov`, …) stay with
23
+ * the walk that needs them — this constant is a floor, not a ceiling.
24
+ *
25
+ * Adding a directory here is a one-line change all four walks pick up, and
26
+ * `non-product-dirs.test.ts` fails if any of them stops agreeing.
27
+ *
28
+ * Refs #3487, #3481, #2953, #2954, #1656.
29
+ */
30
+ /**
31
+ * Swarm / agent scratch worktree roots (#2953, #1656).
32
+ *
33
+ * `.deft-scratch` is the current layout (`.deft-scratch/worktrees/<story-id>`,
34
+ * built by `swarm/launch.ts`); `swarm-worktrees` is the pre-`.deft-scratch`
35
+ * name, kept for repos that still carry one.
36
+ */
37
+ export const AGENT_SCRATCH_DIRS = [".deft-scratch", "swarm-worktrees"];
38
+ /**
39
+ * Agent-host directories that hold host *working* state rather than product
40
+ * source (#3487).
41
+ *
42
+ * Only names with evidence of a real working directory belong here. Directive
43
+ * knows several host directories (`.claude`, `.cursor`, `.grok`, `.codex`,
44
+ * `.github/skills`, `.agents`), but most of them hold nothing except
45
+ * Directive's own committed deposits — `commands.md` § Native multi-host
46
+ * registration (LockedDecision L8) says to *commit* those, so blanket-excluding
47
+ * them would hide committed team surface for no measured gain.
48
+ *
49
+ * `.claude` is different in kind: the Claude Code host keeps live session state
50
+ * in the same directory it keeps config — `settings.local.json`, and agent
51
+ * worktrees under `.claude/worktrees/<agent-id>`, each a full nested checkout.
52
+ * Three such worktrees added ~11,000 walkable entries to this repo
53
+ * (9,128 → 20,393) that no Directive walk skipped. Its Directive-managed
54
+ * contents (`.claude/settings.json`, `.claude/commands/*.md`,
55
+ * `.claude/skills/`) are deposits generated from `content/`, which every walk
56
+ * still visits at the authoring source.
57
+ *
58
+ * Deliberately **not** listed:
59
+ * - `.openclaw` — there is no project-level `.openclaw/`; the host state dir
60
+ * is `~/.openclaw` in `$HOME` (see `doctor/openclaw-skills.ts`), and
61
+ * inventing a project one is explicitly forbidden by
62
+ * `content/docs/openclaw-agent-host.md` and asserted against in
63
+ * `slash/openclaw-adapter.test.ts`.
64
+ * - `.cursor`, `.grok`, `.codex`, `.github` — committed Directive deposits
65
+ * (`.cursor/hooks.json`, `.grok/commands/`, `.codex/prompts/`,
66
+ * `.github/skills/`) with no known working/worktree subtree. No evidence
67
+ * of a scratch convention was found for any of them.
68
+ */
69
+ export const AGENT_HOST_WORKING_DIRS = [".claude"];
70
+ /**
71
+ * Directory basenames no Directive walk should descend into.
72
+ *
73
+ * Every walk extends this set; none may shrink it. Keep entries to basenames
74
+ * that cannot be product source in any repo — a name that is merely *usually*
75
+ * uninteresting to one gate (`tests`, `scripts`, `specs`) belongs to that
76
+ * walk's own set, not here.
77
+ */
78
+ export const NON_PRODUCT_DIRS = new Set([
79
+ // Version-control metadata.
80
+ ".git",
81
+ // Dependency trees and language virtualenvs.
82
+ "node_modules",
83
+ ".venv",
84
+ // Build output and operator backups.
85
+ "dist",
86
+ "backup",
87
+ // Tool caches.
88
+ "__pycache__",
89
+ ".mypy_cache",
90
+ ".pytest_cache",
91
+ ".ruff_cache",
92
+ // Agent working directories.
93
+ ...AGENT_SCRATCH_DIRS,
94
+ ...AGENT_HOST_WORKING_DIRS,
95
+ ]);
96
+ //# sourceMappingURL=non-product-dirs.js.map
@@ -21,7 +21,9 @@ export type HookDecisionCode = "session-start" | "session-start-disabled" | "ses
21
21
  /** Enforcement skipped: root `.deft-directive-disable` test kill-switch (#3039). */
22
22
  | "directive-disabled" | "session-compact-rearm" | "session-compact-rearm-degraded" | "session-compact-noop" | "not-direct-write" | "invalid-input"
23
23
  /** Host closed stdin with zero bytes — integration failure, not a policy gate (#2864). */
24
- | "stdin-empty" | "ritual-not-ready" | "scope-not-ready" | "write-propose-ready"
24
+ | "stdin-empty" | "ritual-not-ready"
25
+ /** Product-path write while another live session occupies this worktree (#3433). */
26
+ | "occupancy-occupied" | "scope-not-ready" | "write-propose-ready"
25
27
  /** Allowlisted assist/scratch write without active xBRIEF (#1802). */
26
28
  | "write-assist-scratch-ready" | "write-ready" | "read-only-deny" | "spawn-explore-ready"
27
29
  /** Non-lifecycle assist/docs spawn allowed without active xBRIEF (#3080). */