@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
@@ -1,5 +1,7 @@
1
+ import { ContainedWriteError } from "../fs/contained-write.js";
1
2
  import { call as scmCall } from "../scm/call.js";
2
3
  import { pyRepr } from "../scm/py-format.js";
4
+ import { triageCacheRelPath } from "../triage/cache-path.js";
3
5
  import { DEFAULT_ACTOR, DEFAULT_EXPECTED_CLOSE_SIGNAL, DEFAULT_ROLE, WAVE_FLAG_RE, } from "./constants.js";
4
6
  import { IssueValidationError, SliceRecordError } from "./errors.js";
5
7
  import { pythonJsonPretty } from "./json.js";
@@ -170,6 +172,10 @@ export function summariseWaves(waveMap, totalChildren) {
170
172
  .map((waveN) => `wave-${waveN}=${placedByWave.get(waveN)}`);
171
173
  return `${parts.length} wave(s): ${parts.join(", ")}`;
172
174
  }
175
+ /** Layout-aware display path for the slices log actually read or written (#3386). */
176
+ function slicesLogDisplayPath(projectRoot) {
177
+ return triageCacheRelPath(projectRoot, "slices.jsonl");
178
+ }
173
179
  function duplicateMessage(umbrella, duplicate) {
174
180
  return (`slice:record-existing: umbrella #${umbrella} already has a matching record (slice_id=${String(duplicate.slice_id)}, ` +
175
181
  `actor=${String(duplicate.actor)}). Re-run with --force to write a second record.`);
@@ -278,7 +284,10 @@ export function runRecordExisting(args, waveMap, deps = {}) {
278
284
  if (authoritativeDup !== null && !args.force) {
279
285
  return { kind: "duplicate", duplicate: authoritativeDup };
280
286
  }
281
- const id = writeSliceUnlocked(record, { path: logPath });
287
+ const id = writeSliceUnlocked(record, {
288
+ path: logPath,
289
+ projectRoot: resolved.projectRoot,
290
+ });
282
291
  return { kind: "written", sliceId: id };
283
292
  });
284
293
  if (outcome.kind === "duplicate") {
@@ -291,7 +300,7 @@ export function runRecordExisting(args, waveMap, deps = {}) {
291
300
  const waveSummary = summariseWaves(waveMap, children.length);
292
301
  return {
293
302
  exitCode: 0,
294
- stdout: `Wrote vbrief/.eval/slices.jsonl entry for umbrella ` +
303
+ stdout: `Wrote ${slicesLogDisplayPath(resolved.projectRoot)} entry for umbrella ` +
295
304
  `#${args.umbrella} (${children.length} children, ${waveSummary}). ` +
296
305
  `slice_id=${outcome.sliceId}\n`,
297
306
  stderr: "",
@@ -301,6 +310,13 @@ export function runRecordExisting(args, waveMap, deps = {}) {
301
310
  if (err instanceof SliceRecordError) {
302
311
  return { exitCode: 1, stdout: "", stderr: `error: invalid record -- ${err.message}\n` };
303
312
  }
313
+ if (err instanceof ContainedWriteError) {
314
+ return {
315
+ exitCode: 1,
316
+ stdout: "",
317
+ stderr: `error: refused slices.jsonl write -- ${err.message}\n`,
318
+ };
319
+ }
304
320
  throw err;
305
321
  }
306
322
  }
@@ -309,6 +325,7 @@ export function runList(args) {
309
325
  if (resolved.exitCode !== 0) {
310
326
  return { exitCode: 2, stdout: "", stderr: `${formatMissingRootError()}\n` };
311
327
  }
328
+ const logDisplay = slicesLogDisplayPath(resolved.projectRoot);
312
329
  const records = readAll({ path: slicesPath(resolved.projectRoot) });
313
330
  if (args.asJson) {
314
331
  return { exitCode: 0, stdout: `${pythonJsonPretty(records)}\n`, stderr: "" };
@@ -316,11 +333,11 @@ export function runList(args) {
316
333
  if (records.length === 0) {
317
334
  return {
318
335
  exitCode: 0,
319
- stdout: "slice:list: no records found in vbrief/.eval/slices.jsonl (file absent or empty).\n",
336
+ stdout: `slice:list: no records found in ${logDisplay} (file absent or empty).\n`,
320
337
  stderr: "",
321
338
  };
322
339
  }
323
- const lines = [`slice:list: ${records.length} record(s) in vbrief/.eval/slices.jsonl`];
340
+ const lines = [`slice:list: ${records.length} record(s) in ${logDisplay}`];
324
341
  for (const record of records) {
325
342
  const umbrella = record.umbrella ?? "?";
326
343
  const actor = record.actor ?? "?";
@@ -1,9 +1,24 @@
1
+ /** 10× occupancy heartbeat TTL. Off-Linux PID reuse / stalled holder reclaim. */
2
+ export declare const STALE_LOCK_HARD_CAP_MS: number;
1
3
  export interface LockDeps {
2
4
  readonly sleepMs?: (ms: number) => void;
3
5
  readonly now?: () => number;
4
6
  }
7
+ export interface HeldLock {
8
+ readonly lockPath: string;
9
+ readonly pid: number;
10
+ readonly token: string;
11
+ readonly acquiredAt: number;
12
+ }
13
+ export interface LockRecord {
14
+ readonly pid: number | null;
15
+ readonly token: string;
16
+ readonly acquiredAt: number | null;
17
+ }
18
+ export declare function parseLockRecord(lockPath: string): LockRecord;
19
+ export declare function assertAppendLockOwned(held: HeldLock): void;
5
20
  /** Serialise appenders across threads AND processes (sidecar lock file). */
6
- export declare function withAppendLock<T>(logPath: string, fn: () => T, deps?: LockDeps): T;
21
+ export declare function withAppendLock<T>(logPath: string, fn: (held: HeldLock) => T, deps?: LockDeps): T;
7
22
  /** Public alias mirroring Python append_lock. */
8
23
  export declare const appendLock: typeof withAppendLock;
9
24
  //# sourceMappingURL=lock.d.ts.map
@@ -1,12 +1,127 @@
1
- import { closeSync, existsSync, mkdirSync, openSync, unlinkSync, writeSync } from "node:fs";
1
+ import { randomUUID } from "node:crypto";
2
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeSync, } from "node:fs";
2
3
  import { dirname } from "node:path";
3
4
  const threadLocked = { held: false };
5
+ /** 10× occupancy heartbeat TTL. Off-Linux PID reuse / stalled holder reclaim. */
6
+ export const STALE_LOCK_HARD_CAP_MS = 20 * 60 * 1000 * 10;
4
7
  function defaultSleep(ms) {
5
8
  const end = Date.now() + ms;
6
9
  while (Date.now() < end) {
7
10
  /* spin */
8
11
  }
9
12
  }
13
+ export function parseLockRecord(lockPath) {
14
+ try {
15
+ const lines = readFileSync(lockPath, { encoding: "utf8" }).trim().split(/\r?\n/);
16
+ const pidRaw = Number(lines[0]);
17
+ const pid = Number.isInteger(pidRaw) && pidRaw > 0 ? pidRaw : null;
18
+ if (lines.length >= 3) {
19
+ const acquiredRaw = Number(lines[2]);
20
+ return {
21
+ pid,
22
+ token: lines[1] ?? "",
23
+ acquiredAt: Number.isInteger(acquiredRaw) && acquiredRaw > 0 ? acquiredRaw : null,
24
+ };
25
+ }
26
+ const acquiredRaw = Number(lines[1]);
27
+ return {
28
+ pid,
29
+ token: "",
30
+ acquiredAt: Number.isInteger(acquiredRaw) && acquiredRaw > 0 ? acquiredRaw : null,
31
+ };
32
+ }
33
+ catch {
34
+ return { pid: null, token: "", acquiredAt: null };
35
+ }
36
+ }
37
+ export function assertAppendLockOwned(held) {
38
+ const rec = parseLockRecord(held.lockPath);
39
+ if (rec.pid !== held.pid || rec.token !== held.token || rec.token.length === 0) {
40
+ throw new Error(`lock compromised: ${held.lockPath}`);
41
+ }
42
+ }
43
+ function processExists(pid) {
44
+ try {
45
+ process.kill(pid, 0);
46
+ return true;
47
+ }
48
+ catch (err) {
49
+ return err.code !== "ESRCH";
50
+ }
51
+ }
52
+ function linuxPidStartedAfter(pid, acquiredAt) {
53
+ try {
54
+ const stat = readFileSync(`/proc/${pid}/stat`, { encoding: "utf8" });
55
+ const closeParen = stat.lastIndexOf(")");
56
+ if (closeParen < 0)
57
+ return false;
58
+ const startTicks = Number(stat.slice(closeParen + 2).split(" ")[19]);
59
+ if (!Number.isFinite(startTicks))
60
+ return false;
61
+ const btime = /(?:^|\n)btime (\d+)/.exec(readFileSync("/proc/stat", { encoding: "utf8" }));
62
+ if (btime === null)
63
+ return false;
64
+ const startMs = Number(btime[1]) * 1000 + (startTicks * 1000) / 100;
65
+ return startMs > acquiredAt + 1000;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ function lockMtimeMs(lockPath) {
72
+ try {
73
+ return statSync(lockPath).mtimeMs;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ function isAbandoned(rec, now, lockPath) {
80
+ if (rec.pid === null) {
81
+ const mtime = lockMtimeMs(lockPath);
82
+ return mtime !== null && now - mtime > STALE_LOCK_HARD_CAP_MS;
83
+ }
84
+ if (!processExists(rec.pid))
85
+ return true;
86
+ if (rec.acquiredAt !== null && linuxPidStartedAfter(rec.pid, rec.acquiredAt))
87
+ return true;
88
+ if (rec.acquiredAt !== null && now - rec.acquiredAt > STALE_LOCK_HARD_CAP_MS)
89
+ return true;
90
+ return false;
91
+ }
92
+ function recordsMatch(a, b) {
93
+ return a.pid === b.pid && a.token === b.token && a.acquiredAt === b.acquiredAt;
94
+ }
95
+ /** Atomic rename reclaim: one waiter wins; losers see ENOENT. */
96
+ function tryReclaimAbandonedOwner(lockPath, now) {
97
+ const observed = parseLockRecord(lockPath);
98
+ if (!isAbandoned(observed, now, lockPath))
99
+ return false;
100
+ const quarantine = `${lockPath}.reclaim.${process.pid}.${randomUUID()}`;
101
+ try {
102
+ renameSync(lockPath, quarantine);
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ const quarantined = parseLockRecord(quarantine);
108
+ if (recordsMatch(observed, quarantined)) {
109
+ try {
110
+ unlinkSync(quarantine);
111
+ }
112
+ catch {
113
+ /* already gone */
114
+ }
115
+ return true;
116
+ }
117
+ try {
118
+ renameSync(quarantine, lockPath);
119
+ }
120
+ catch {
121
+ /* best-effort restore */
122
+ }
123
+ return false;
124
+ }
10
125
  /** Serialise appenders across threads AND processes (sidecar lock file). */
11
126
  export function withAppendLock(logPath, fn, deps = {}) {
12
127
  const sleepMs = deps.sleepMs ?? defaultSleep;
@@ -18,13 +133,18 @@ export function withAppendLock(logPath, fn, deps = {}) {
18
133
  }
19
134
  threadLocked.held = true;
20
135
  let fd;
136
+ let held;
137
+ let reclaimedStale = false;
21
138
  try {
22
139
  const deadline = now() + 30_000;
23
140
  while (true) {
24
141
  try {
25
142
  fd = openSync(lockPath, "wx");
26
- writeSync(fd, Buffer.from("\0"));
27
- break;
143
+ const token = randomUUID();
144
+ const acquiredAt = now();
145
+ writeSync(fd, Buffer.from(`${process.pid}\n${token}\n${acquiredAt}\n`));
146
+ held = { lockPath, pid: process.pid, token, acquiredAt };
147
+ return fn(held);
28
148
  }
29
149
  catch (err) {
30
150
  const code = err.code;
@@ -32,26 +152,32 @@ export function withAppendLock(logPath, fn, deps = {}) {
32
152
  throw err;
33
153
  }
34
154
  if (now() > deadline) {
155
+ if (!reclaimedStale && tryReclaimAbandonedOwner(lockPath, now())) {
156
+ reclaimedStale = true;
157
+ continue;
158
+ }
35
159
  throw new Error(`timed out acquiring lock for ${logPath}`);
36
160
  }
37
161
  sleepMs(20);
38
162
  }
39
163
  }
40
- return fn();
41
164
  }
42
165
  finally {
43
166
  if (fd !== undefined) {
44
167
  closeSync(fd);
45
168
  }
46
- threadLocked.held = false;
47
- try {
48
- if (existsSync(lockPath)) {
49
- unlinkSync(lockPath);
169
+ if (held !== undefined) {
170
+ try {
171
+ assertAppendLockOwned(held);
172
+ if (existsSync(lockPath)) {
173
+ unlinkSync(lockPath);
174
+ }
175
+ }
176
+ catch {
177
+ /* not ours anymore */
50
178
  }
51
179
  }
52
- catch {
53
- /* best-effort */
54
- }
180
+ threadLocked.held = false;
55
181
  }
56
182
  }
57
183
  /** Public alias mirroring Python append_lock. */
@@ -1,4 +1,4 @@
1
- import { appendFileSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
2
  import { withAppendLock } from "./lock.js";
3
3
  export interface WriteSliceOptions {
4
4
  readonly umbrellaUrl: string;
@@ -14,13 +14,15 @@ export interface WriteSliceOptions {
14
14
  export interface RecordModuleDeps {
15
15
  readonly readFile?: typeof readFileSync;
16
16
  readonly exists?: typeof existsSync;
17
- readonly append?: typeof appendFileSync;
18
- readonly fsync?: typeof fsyncSync;
19
- readonly open?: typeof openSync;
20
- readonly close?: typeof closeSync;
21
17
  readonly mkdir?: typeof mkdirSync;
22
18
  readonly withLock?: typeof withAppendLock;
23
19
  }
20
+ /**
21
+ * Containment root for slices.jsonl append (#3354 / #3288).
22
+ * Prefer `projectRoot` when the log is nested under it so out-of-tree dests refuse.
23
+ * Otherwise use the log parent (still refuses leaf symlink follow).
24
+ */
25
+ export declare function containmentRootForSliceLog(logPath: string, projectRoot?: string): string;
24
26
  export declare function newSliceId(): string;
25
27
  /** Return the current UTC time in canonical ISO-8601 form with Z suffix (no fractional seconds). */
26
28
  export declare function nowIso(): string;
@@ -29,6 +31,7 @@ export declare function writeSliceUnlocked(record: Record<string, unknown>, opti
29
31
  path?: string;
30
32
  deps?: RecordModuleDeps;
31
33
  warn?: (message: string) => void;
34
+ projectRoot?: string;
32
35
  }): string;
33
36
  /** Validate and atomically append a cohort record to slices.jsonl. */
34
37
  export declare function writeSlice(umbrella: number, children: Iterable<Record<string, unknown>>, options: WriteSliceOptions, deps?: RecordModuleDeps): string;
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { appendFileSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
3
3
  import { dirname, resolve } from "node:path";
4
+ import { containedWrite } from "../fs/contained-write.js";
4
5
  import { resolveTriageCachePath } from "../triage/cache-path.js";
5
6
  import { pythonJsonStringify } from "./json.js";
6
7
  import { withAppendLock } from "./lock.js";
@@ -8,13 +9,20 @@ import { validateRecord } from "./validate.js";
8
9
  const defaultDeps = {
9
10
  readFile: readFileSync,
10
11
  exists: existsSync,
11
- append: appendFileSync,
12
- fsync: fsyncSync,
13
- open: openSync,
14
- close: closeSync,
15
12
  mkdir: mkdirSync,
16
13
  withLock: withAppendLock,
17
14
  };
15
+ /**
16
+ * Containment root for slices.jsonl append (#3354 / #3288).
17
+ * Prefer `projectRoot` when the log is nested under it so out-of-tree dests refuse.
18
+ * Otherwise use the log parent (still refuses leaf symlink follow).
19
+ */
20
+ export function containmentRootForSliceLog(logPath, projectRoot) {
21
+ if (projectRoot !== undefined) {
22
+ return resolve(projectRoot);
23
+ }
24
+ return resolve(dirname(logPath));
25
+ }
18
26
  function resolvePath(path) {
19
27
  return path !== undefined ? resolve(path) : resolveTriageCachePath(process.cwd(), "slices.jsonl");
20
28
  }
@@ -64,14 +72,14 @@ export function writeSliceUnlocked(record, options = {}) {
64
72
  return resolvedId;
65
73
  }
66
74
  const line = `${pythonJsonStringify(record)}\n`;
67
- deps.append(logPath, line, { encoding: "utf8" });
68
- const fd = deps.open(logPath, "a");
69
- try {
70
- deps.fsync(fd);
71
- }
72
- finally {
73
- deps.close(fd);
74
- }
75
+ const root = containmentRootForSliceLog(logPath, options.projectRoot);
76
+ deps.mkdir(root, { recursive: true });
77
+ containedWrite({
78
+ root,
79
+ target: logPath,
80
+ data: line,
81
+ mode: "append",
82
+ });
75
83
  return resolvedId;
76
84
  }
77
85
  /** Validate and atomically append a cohort record to slices.jsonl. */
@@ -20,6 +20,8 @@ export interface EvaluateOptions {
20
20
  readonly projectRoot?: string;
21
21
  /** Skip parent-lineage check (tests / opt-out). Default false. */
22
22
  readonly skipParentLineage?: boolean;
23
+ /** Skip project-invariant coverage (#3425). Default false. */
24
+ readonly skipProjectInvariants?: boolean;
23
25
  }
24
26
  /**
25
27
  * Pure evaluator — returns exit code + human message. Faithful to
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
+ import { evaluateProjectInvariantsGate, resolveProjectRootForInvariants, } from "../preflight/project-invariants-gate.js";
3
4
  import { evaluateParentLineage, formatParentLineageLine, } from "../scope/parent-lineage.js";
4
5
  import { parseAllocationSection, SOLO_KIND, SWARM_COHORT_KIND, VALID_DISPATCH_KINDS, } from "./allocation.js";
5
6
  export const ACTIVE_FOLDER = "active";
@@ -168,6 +169,18 @@ export function evaluate(vbriefPath, options = {}) {
168
169
  `\n${formatParentLineageLine(lineage)}`,
169
170
  };
170
171
  }
172
+ const invariants = evaluateProjectInvariantsGate(vbriefCheck.payload, {
173
+ projectRoot: resolveProjectRootForInvariants(vbriefCheck.path, options.projectRoot),
174
+ skip: options.skipProjectInvariants === true,
175
+ });
176
+ if (!invariants.ok) {
177
+ return {
178
+ exitCode: 1,
179
+ dispatchKind: null,
180
+ parentLineage: lineage,
181
+ message: `not ready: ${invariants.message}`,
182
+ };
183
+ }
171
184
  const lineageNote = lineage.applicable
172
185
  ? `parent lineage OK (${lineage.parent_requirement_ids.length} req IDs` +
173
186
  (lineage.negative_invariant_ids.length > 0
@@ -45,9 +45,12 @@ export declare function completeCohort(args: {
45
45
  emitJson?: boolean;
46
46
  /** Per-story delivery evidence; without it code-bearing stories fail closed (#3041). */
47
47
  delivery?: CohortDeliveryContext | null;
48
+ /** Optional launch allocation plan id so close-out reads this cohort's occupancy slot. */
49
+ allocationPlanId?: string | null;
48
50
  }): {
49
51
  exitCode: number;
50
52
  stdout: string;
51
53
  stderr: string;
54
+ sweep: SweepResult | null;
52
55
  };
53
56
  //# sourceMappingURL=complete-cohort.d.ts.map
@@ -1,11 +1,13 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
3
- import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
3
+ import { hasArtifactSuffix, resolveLifecycleRoot, stripArtifactSuffix } from "../layout/resolve.js";
4
4
  import { detectLifecycleFolder } from "../scope/decomposed-refs.js";
5
5
  import { classifyStoredDeliveryDisposition, } from "../scope/delivery-evidence.js";
6
6
  import { runTransition } from "../scope/transition.js";
7
7
  import { collectChildUris, collectPlanRefs, resolveVbriefRef } from "../scope/vbrief-ref.js";
8
+ import { readOccupancy, releaseSwarmOccupancy } from "../session/occupancy.js";
8
9
  import { MAX_FIXPOINT_PASSES, TERMINAL_FOLDERS } from "./constants.js";
10
+ import { resolveLaunchOccupancySessionId } from "./launch.js";
9
11
  function loadPlan(path) {
10
12
  try {
11
13
  const data = JSON.parse(readFileSync(path, "utf8"));
@@ -21,6 +23,14 @@ function loadPlan(path) {
21
23
  return null;
22
24
  }
23
25
  }
26
+ function storyIdFromPath(path) {
27
+ const plan = loadPlan(path);
28
+ const id = plan !== null && typeof plan.id === "string" ? plan.id.trim() : "";
29
+ if (id.length > 0)
30
+ return id;
31
+ const name = basename(path);
32
+ return hasArtifactSuffix(name) ? stripArtifactSuffix(name) : name.replace(/\.[^.]+$/, "");
33
+ }
24
34
  function rel(path, projectRoot) {
25
35
  try {
26
36
  return resolve(path)
@@ -226,6 +236,7 @@ function completeParent(args) {
226
236
  }
227
237
  let current = parentPath;
228
238
  let action = "complete";
239
+ let activateNotice = "";
229
240
  if (folder === "pending") {
230
241
  action = "activate+complete";
231
242
  const activateResult = runTransition("activate", current);
@@ -238,18 +249,22 @@ function completeParent(args) {
238
249
  detail: `activate failed: ${activateResult.message}`,
239
250
  };
240
251
  }
252
+ activateNotice = activateResult.message.trim();
241
253
  current = join(vbriefDir, "active", parentPath.split(/[/\\]/).pop() ?? "");
242
254
  }
243
255
  const completeResult = runTransition("complete", current, new Date(), transitionOptionsFor(current, delivery));
244
256
  if (completeResult.ok) {
245
257
  settled.add(resolve(join(vbriefDir, "completed", parentPath.split(/[/\\]/).pop() ?? "")));
246
258
  }
259
+ const detail = activateNotice.length > 0
260
+ ? `${activateNotice}\n${completeResult.message}`
261
+ : completeResult.message;
247
262
  return {
248
263
  kind: "epic",
249
264
  path: relpath,
250
265
  action: completeResult.ok ? action : "failed",
251
266
  ok: completeResult.ok,
252
- detail: completeResult.message,
267
+ detail,
253
268
  };
254
269
  }
255
270
  export function sweepCohort(storyPaths, projectRoot, dryRun, delivery) {
@@ -412,6 +427,7 @@ export function completeCohort(args) {
412
427
  exitCode: 2,
413
428
  stdout: "",
414
429
  stderr: `Error: project root does not exist: ${projectRoot}\n`,
430
+ sweep: null,
415
431
  };
416
432
  }
417
433
  if (!existsSync(resolveLifecycleRoot(projectRoot))) {
@@ -419,31 +435,33 @@ export function completeCohort(args) {
419
435
  exitCode: 2,
420
436
  stdout: "",
421
437
  stderr: `Error: no vbrief/ directory under project root: ${projectRoot}\n`,
438
+ sweep: null,
422
439
  };
423
440
  }
424
441
  const { paths, errors } = resolveCohortPaths(args.stories ?? [], args.cohortGlobs ?? [], projectRoot);
425
442
  if (paths.length === 0) {
426
443
  const msg = "Error: empty cohort. Pass one or more story vBRIEF paths as positional arguments and/or --cohort <glob>.";
444
+ const empty = {
445
+ project_root: projectRoot,
446
+ dry_run: args.dryRun ?? false,
447
+ stories: [],
448
+ parents: [],
449
+ errors: errors.length > 0 ? errors : [msg],
450
+ ok: false,
451
+ };
427
452
  if (args.emitJson) {
428
- const empty = {
429
- project_root: projectRoot,
430
- dry_run: args.dryRun ?? false,
431
- stories: [],
432
- parents: [],
433
- errors: errors.length > 0 ? errors : [msg],
434
- ok: false,
435
- };
436
453
  return {
437
454
  exitCode: 2,
438
455
  stdout: `${JSON.stringify(sweepResultToDict(empty), null, 2)}\n`,
439
456
  stderr: "",
457
+ sweep: empty,
440
458
  };
441
459
  }
442
460
  let stderr = `${msg}\n`;
443
461
  for (const err of errors) {
444
462
  stderr += ` - ${err}\n`;
445
463
  }
446
- return { exitCode: 2, stdout: "", stderr };
464
+ return { exitCode: 2, stdout: "", stderr, sweep: empty };
447
465
  }
448
466
  const result = sweepCohortWithArgs({
449
467
  storyPaths: paths,
@@ -452,17 +470,47 @@ export function completeCohort(args) {
452
470
  delivery: args.delivery ?? null,
453
471
  });
454
472
  result.errors.push(...errors);
473
+ if (result.ok && args.dryRun !== true) {
474
+ const envSession = process.env.DEFT_SESSION_ID?.trim() ?? "";
475
+ let sessionId = envSession;
476
+ if (sessionId.length === 0) {
477
+ const resolved = resolveLaunchOccupancySessionId(projectRoot, {
478
+ allocationPlanId: args.allocationPlanId ?? null,
479
+ storyIds: paths.map(storyIdFromPath),
480
+ });
481
+ if (resolved.reason === "ok" && resolved.sessionId.length > 0) {
482
+ sessionId = resolved.sessionId;
483
+ }
484
+ else if (readOccupancy(projectRoot) !== null) {
485
+ result.ok = false;
486
+ result.errors.push("swarm close-out occupancy record is missing or belongs to a different cohort. " +
487
+ "Steal with occupancy:steal --confirm --occupant <id>.");
488
+ }
489
+ }
490
+ if (result.ok && sessionId.length > 0) {
491
+ const released = releaseSwarmOccupancy(projectRoot, {
492
+ env: process.env,
493
+ sessionId,
494
+ });
495
+ if (released.code !== 0) {
496
+ result.ok = false;
497
+ result.errors.push(released.message);
498
+ }
499
+ }
500
+ }
455
501
  if (args.emitJson) {
456
502
  return {
457
503
  exitCode: result.ok ? 0 : 1,
458
504
  stdout: `${JSON.stringify(sweepResultToDict(result), null, 2)}\n`,
459
505
  stderr: "",
506
+ sweep: result,
460
507
  };
461
508
  }
462
509
  return {
463
510
  exitCode: result.ok ? 0 : 1,
464
511
  stdout: `${renderSweepText(result)}\n`,
465
512
  stderr: "",
513
+ sweep: result,
466
514
  };
467
515
  }
468
516
  //# sourceMappingURL=complete-cohort.js.map
@@ -51,6 +51,18 @@ function safeSegment(text) {
51
51
  function oneLine(text) {
52
52
  return text.replace(/\r?\n/g, " ");
53
53
  }
54
+ /** Existing sweep records already carry refused-stamp remediations (#3398). */
55
+ function sweepDetailLines(sweep) {
56
+ const lines = [];
57
+ for (const rec of [...sweep.stories, ...sweep.parents]) {
58
+ const detail = rec.detail.trim();
59
+ if (detail.length > 0) {
60
+ lines.push(`${rec.path}: ${detail}`);
61
+ }
62
+ }
63
+ lines.push(...sweep.errors);
64
+ return lines;
65
+ }
54
66
  function parseRepo(repo) {
55
67
  const slash = repo.indexOf("/");
56
68
  if (slash <= 0 || slash >= repo.length - 1) {
@@ -640,8 +652,15 @@ export function finalizeCohort(args) {
640
652
  }
641
653
  : null,
642
654
  });
655
+ const sweep = sweepResult.sweep;
643
656
  if (sweepResult.exitCode !== 0) {
644
657
  errors.push("cohort completion sweep failed.");
658
+ if (sweep !== null) {
659
+ errors.push(...sweepDetailLines(sweep));
660
+ }
661
+ else if (sweepResult.stdout.trim().length > 0) {
662
+ errors.push(sweepResult.stdout.trim());
663
+ }
645
664
  return buildResponse({
646
665
  projectRoot,
647
666
  dryRun,
@@ -649,7 +668,7 @@ export function finalizeCohort(args) {
649
668
  prNumbers,
650
669
  storyPaths,
651
670
  closingIssues: [...closingIssues],
652
- sweep: null,
671
+ sweep,
653
672
  commitSha: null,
654
673
  branch: null,
655
674
  prUrl: null,
@@ -688,7 +707,7 @@ export function finalizeCohort(args) {
688
707
  prNumbers,
689
708
  storyPaths,
690
709
  closingIssues: [...closingIssues],
691
- sweep: null,
710
+ sweep,
692
711
  commitSha,
693
712
  branch,
694
713
  prUrl,
@@ -747,6 +766,18 @@ function buildResponse(input) {
747
766
  lines.push(` - ${oneLine(path)}`);
748
767
  }
749
768
  }
769
+ if (input.sweep !== null) {
770
+ const records = [...input.sweep.stories, ...input.sweep.parents];
771
+ if (records.length > 0 || input.sweep.errors.length > 0) {
772
+ lines.push(" Sweep:");
773
+ for (const rec of records) {
774
+ lines.push(` [${rec.ok ? "ok" : "FAILED"}] ${rec.action} ${rec.path} -- ${oneLine(rec.detail)}`);
775
+ }
776
+ for (const err of input.sweep.errors) {
777
+ lines.push(` - ${oneLine(err)}`);
778
+ }
779
+ }
780
+ }
750
781
  if (input.branch !== null) {
751
782
  lines.push(` Branch: ${input.branch}`);
752
783
  }