@deftai/directive-core 0.104.0 → 0.106.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 (287) hide show
  1. package/dist/authz/classify.js +1108 -67
  2. package/dist/check/cached-orchestrator.d.ts +1 -1
  3. package/dist/check/cached-orchestrator.js +2 -2
  4. package/dist/check/named-cause.d.ts +1 -1
  5. package/dist/check/named-cause.js +34 -4
  6. package/dist/codebase/default-extractor.d.ts +5 -0
  7. package/dist/codebase/default-extractor.js +7 -15
  8. package/dist/deposit/copy-tree.js +60 -0
  9. package/dist/deposit/python-free.js +26 -18
  10. package/dist/doctor/checks.d.ts +8 -0
  11. package/dist/doctor/checks.js +37 -25
  12. package/dist/doctor/doctor-state.d.ts +4 -0
  13. package/dist/doctor/doctor-state.js +7 -1
  14. package/dist/doctor/main.js +9 -19
  15. package/dist/doctor/types.d.ts +9 -0
  16. package/dist/forward-coverage/diff-coverage.d.ts +39 -0
  17. package/dist/forward-coverage/diff-coverage.js +112 -0
  18. package/dist/forward-coverage/diff-lines.d.ts +19 -0
  19. package/dist/forward-coverage/diff-lines.js +173 -0
  20. package/dist/forward-coverage/evaluate.d.ts +24 -9
  21. package/dist/forward-coverage/evaluate.js +125 -19
  22. package/dist/fs/contained-write.d.ts +115 -1
  23. package/dist/fs/contained-write.js +241 -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 +1 -0
  31. package/dist/index.js +2 -0
  32. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  33. package/dist/init-deposit/agent-hooks.js +18 -16
  34. package/dist/init-deposit/core-guard-pin-content.d.ts +7 -0
  35. package/dist/init-deposit/core-guard-pin-content.js +393 -0
  36. package/dist/init-deposit/gitignore.js +9 -0
  37. package/dist/init-deposit/hygiene.d.ts +47 -11
  38. package/dist/init-deposit/hygiene.js +172 -37
  39. package/dist/init-deposit/legacy-detect.js +6 -2
  40. package/dist/init-deposit/refresh.d.ts +21 -1
  41. package/dist/init-deposit/refresh.js +106 -9
  42. package/dist/init-deposit/scaffold.js +28 -279
  43. package/dist/init-deposit/slash-deposit.js +13 -7
  44. package/dist/init-deposit/xbrief-projections.d.ts +8 -1
  45. package/dist/init-deposit/xbrief-projections.js +24 -11
  46. package/dist/intake/clause-derivation.d.ts +47 -1
  47. package/dist/intake/clause-derivation.js +238 -9
  48. package/dist/intake/issue-ingest.d.ts +3 -1
  49. package/dist/intake/issue-ingest.js +35 -6
  50. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +5 -0
  51. package/dist/lifecycle/completed-tracked-on-delivery.js +85 -18
  52. package/dist/lifecycle-visible/evaluate.d.ts +98 -0
  53. package/dist/lifecycle-visible/evaluate.js +710 -0
  54. package/dist/lifecycle-visible/index.d.ts +2 -0
  55. package/dist/lifecycle-visible/index.js +2 -0
  56. package/dist/literal-acceptance/capture.d.ts +10 -0
  57. package/dist/literal-acceptance/capture.js +226 -77
  58. package/dist/literal-acceptance/evaluate.d.ts +29 -4
  59. package/dist/literal-acceptance/evaluate.js +70 -2
  60. package/dist/literal-acceptance/index.d.ts +3 -3
  61. package/dist/literal-acceptance/index.js +3 -3
  62. package/dist/literal-acceptance/safety.d.ts +41 -0
  63. package/dist/literal-acceptance/safety.js +126 -2
  64. package/dist/literal-acceptance/types.d.ts +8 -0
  65. package/dist/orchestration/judgment-policy.d.ts +13 -0
  66. package/dist/orchestration/judgment-policy.js +15 -0
  67. package/dist/orchestration/probe-session.js +13 -8
  68. package/dist/orchestration/verify-judgment-gates.d.ts +4 -0
  69. package/dist/orchestration/verify-judgment-gates.js +43 -11
  70. package/dist/orphan-active/evaluate.d.ts +7 -1
  71. package/dist/orphan-active/evaluate.js +102 -23
  72. package/dist/platform/platform-capabilities.js +2 -3
  73. package/dist/policy/base-branch.d.ts +37 -0
  74. package/dist/policy/base-branch.js +114 -0
  75. package/dist/policy/branch-sync.d.ts +80 -0
  76. package/dist/policy/branch-sync.js +211 -0
  77. package/dist/policy/ceremony-dial-escalation.js +1 -0
  78. package/dist/policy/ceremony-dial.js +11 -21
  79. package/dist/policy/delivery-branch.d.ts +5 -3
  80. package/dist/policy/delivery-branch.js +7 -3
  81. package/dist/policy/file-size-thresholds.d.ts +24 -0
  82. package/dist/policy/file-size-thresholds.js +23 -0
  83. package/dist/policy/forge-outage-retry.d.ts +47 -0
  84. package/dist/policy/forge-outage-retry.js +157 -0
  85. package/dist/policy/host-hooks.d.ts +20 -0
  86. package/dist/policy/host-hooks.js +121 -2
  87. package/dist/policy/index.d.ts +7 -0
  88. package/dist/policy/index.js +66 -7
  89. package/dist/policy/org-force-on-migration.js +2 -1
  90. package/dist/policy/plan-extensions.d.ts +1 -1
  91. package/dist/policy/plan-extensions.js +2 -0
  92. package/dist/policy/product-signal.js +6 -7
  93. package/dist/policy/project-invariants.d.ts +50 -0
  94. package/dist/policy/project-invariants.js +205 -0
  95. package/dist/policy/require-human-merge.js +8 -6
  96. package/dist/policy/resolve.d.ts +7 -2
  97. package/dist/policy/resolve.js +25 -8
  98. package/dist/policy/sync-default.d.ts +129 -0
  99. package/dist/policy/sync-default.js +452 -0
  100. package/dist/policy/sync-max-files.d.ts +93 -0
  101. package/dist/policy/sync-max-files.js +192 -0
  102. package/dist/policy/value-feedback.js +7 -8
  103. package/dist/pr-monitor/absent-required.d.ts +11 -0
  104. package/dist/pr-monitor/absent-required.js +61 -0
  105. package/dist/pr-monitor/constants.d.ts +7 -0
  106. package/dist/pr-monitor/constants.js +7 -0
  107. package/dist/pr-monitor/index.d.ts +2 -1
  108. package/dist/pr-monitor/index.js +2 -1
  109. package/dist/pr-monitor/main.js +4 -0
  110. package/dist/pr-monitor/monitor.js +24 -1
  111. package/dist/pr-wait-mergeable/classify.js +6 -0
  112. package/dist/preflight/evaluate.d.ts +4 -0
  113. package/dist/preflight/evaluate.js +35 -1
  114. package/dist/preflight/index.d.ts +4 -0
  115. package/dist/preflight/index.js +2 -0
  116. package/dist/preflight/intended-placement.d.ts +62 -0
  117. package/dist/preflight/intended-placement.js +275 -0
  118. package/dist/preflight/project-invariants-gate.d.ts +29 -0
  119. package/dist/preflight/project-invariants-gate.js +82 -0
  120. package/dist/preflight-cache/evaluate.d.ts +12 -0
  121. package/dist/preflight-cache/evaluate.js +15 -1
  122. package/dist/preflight-cache/index.d.ts +1 -1
  123. package/dist/preflight-cache/index.js +1 -1
  124. package/dist/product-first-done-gate/acceptance-resolver.d.ts +128 -0
  125. package/dist/product-first-done-gate/acceptance-resolver.js +144 -0
  126. package/dist/product-first-done-gate/acceptance.d.ts +7 -4
  127. package/dist/product-first-done-gate/acceptance.js +79 -8
  128. package/dist/product-first-done-gate/empty-resolution.d.ts +4 -1
  129. package/dist/product-first-done-gate/empty-resolution.js +12 -1
  130. package/dist/product-first-done-gate/evaluate.d.ts +16 -0
  131. package/dist/product-first-done-gate/evaluate.js +298 -34
  132. package/dist/product-first-done-gate/index.d.ts +2 -0
  133. package/dist/product-first-done-gate/index.js +1 -0
  134. package/dist/release/build-dist.d.ts +101 -0
  135. package/dist/release/build-dist.js +428 -46
  136. package/dist/release/closed-verb-gate.d.ts +12 -0
  137. package/dist/release/closed-verb-gate.js +51 -0
  138. package/dist/release/index.d.ts +1 -0
  139. package/dist/release/index.js +1 -0
  140. package/dist/release/pipeline.js +7 -0
  141. package/dist/release/types.d.ts +12 -0
  142. package/dist/render/constants.d.ts +2 -1
  143. package/dist/render/constants.js +2 -1
  144. package/dist/render/framework-commands.js +4 -0
  145. package/dist/render/spec-validate.js +23 -12
  146. package/dist/run-summary/emit.d.ts +16 -10
  147. package/dist/run-summary/emit.js +26 -15
  148. package/dist/run-summary/index.d.ts +3 -2
  149. package/dist/run-summary/index.js +2 -1
  150. package/dist/run-summary/session-id.d.ts +21 -0
  151. package/dist/run-summary/session-id.js +92 -0
  152. package/dist/run-summary/types.d.ts +33 -4
  153. package/dist/run-summary/types.js +1 -0
  154. package/dist/scope/acceptance-evidence.d.ts +19 -7
  155. package/dist/scope/acceptance-evidence.js +38 -11
  156. package/dist/scope/delivery-evidence.d.ts +3 -3
  157. package/dist/scope/delivery-evidence.js +22 -27
  158. package/dist/scope/index.d.ts +1 -0
  159. package/dist/scope/index.js +1 -0
  160. package/dist/scope/main.js +15 -5
  161. package/dist/scope/parent-lineage.js +7 -6
  162. package/dist/scope/project-invariant-coverage.d.ts +54 -0
  163. package/dist/scope/project-invariant-coverage.js +322 -0
  164. package/dist/scope/promote-from-issue.js +5 -5
  165. package/dist/scope/transition.js +1 -1
  166. package/dist/scope-provenance/compare-intent.d.ts +27 -0
  167. package/dist/scope-provenance/compare-intent.js +233 -0
  168. package/dist/scope-provenance/digest.d.ts +18 -1
  169. package/dist/scope-provenance/digest.js +16 -11
  170. package/dist/scope-provenance/evaluate.d.ts +5 -1
  171. package/dist/scope-provenance/evaluate.js +67 -7
  172. package/dist/scope-provenance/extract-intent.d.ts +43 -0
  173. package/dist/scope-provenance/extract-intent.js +262 -0
  174. package/dist/scope-provenance/index.d.ts +7 -1
  175. package/dist/scope-provenance/index.js +7 -1
  176. package/dist/scope-provenance/intent-digest.d.ts +12 -0
  177. package/dist/scope-provenance/intent-digest.js +33 -0
  178. package/dist/scope-provenance/intent-evaluate.d.ts +43 -0
  179. package/dist/scope-provenance/intent-evaluate.js +149 -0
  180. package/dist/scope-provenance/json-tokenizer.d.ts +19 -0
  181. package/dist/scope-provenance/json-tokenizer.js +225 -0
  182. package/dist/scope-provenance/known-machine.d.ts +38 -0
  183. package/dist/scope-provenance/known-machine.js +157 -0
  184. package/dist/scope-provenance/mint-artifacts.d.ts +102 -0
  185. package/dist/scope-provenance/mint-artifacts.js +425 -0
  186. package/dist/session/ac-pass-banking.d.ts +9 -36
  187. package/dist/session/ac-pass-banking.js +33 -79
  188. package/dist/session/ac-pass-reuse.d.ts +53 -0
  189. package/dist/session/ac-pass-reuse.js +95 -0
  190. package/dist/session/git.js +19 -4
  191. package/dist/session/index.d.ts +4 -0
  192. package/dist/session/index.js +4 -0
  193. package/dist/session/occupancy-stress-worker.d.ts +7 -0
  194. package/dist/session/occupancy-stress-worker.js +36 -0
  195. package/dist/session/occupancy.d.ts +97 -0
  196. package/dist/session/occupancy.js +422 -0
  197. package/dist/session/openclaw-soft-rebind-deposit.js +6 -6
  198. package/dist/session/process-cost.d.ts +32 -10
  199. package/dist/session/process-cost.js +189 -1
  200. package/dist/session/product-state-hash.d.ts +26 -0
  201. package/dist/session/product-state-hash.js +700 -0
  202. package/dist/session/ritual-entrypoint.js +16 -1
  203. package/dist/session/session-ready.d.ts +2 -0
  204. package/dist/session/session-ready.js +52 -30
  205. package/dist/session/session-start.d.ts +13 -0
  206. package/dist/session/session-start.js +103 -9
  207. package/dist/session/verify-ac-session-cache.d.ts +42 -0
  208. package/dist/session/verify-ac-session-cache.js +126 -0
  209. package/dist/session/verify-session-ritual.d.ts +28 -0
  210. package/dist/session/verify-session-ritual.js +97 -5
  211. package/dist/slash/openclaw-deposit.js +13 -9
  212. package/dist/slice/constants.d.ts +2 -2
  213. package/dist/slice/constants.js +2 -2
  214. package/dist/slice/existing.js +9 -3
  215. package/dist/slice/lock.d.ts +16 -1
  216. package/dist/slice/lock.js +137 -11
  217. package/dist/story-ready/evaluate.d.ts +2 -0
  218. package/dist/story-ready/evaluate.js +13 -0
  219. package/dist/swarm/complete-cohort.d.ts +3 -0
  220. package/dist/swarm/complete-cohort.js +59 -11
  221. package/dist/swarm/finalize-cohort-cli.d.ts +7 -2
  222. package/dist/swarm/finalize-cohort-cli.js +165 -35
  223. package/dist/swarm/finalize-cohort.d.ts +1 -0
  224. package/dist/swarm/finalize-cohort.js +47 -4
  225. package/dist/swarm/launch.d.ts +22 -0
  226. package/dist/swarm/launch.js +155 -0
  227. package/dist/swarm/routing-set-cli.d.ts +11 -1
  228. package/dist/swarm/routing-set-cli.js +15 -3
  229. package/dist/swarm/routing-verify.d.ts +13 -1
  230. package/dist/swarm/routing-verify.js +60 -21
  231. package/dist/swarm/routing.d.ts +14 -0
  232. package/dist/swarm/routing.js +53 -3
  233. package/dist/telemetry-coverage/fake-trial.js +10 -0
  234. package/dist/telemetry-coverage/kinds.js +2 -0
  235. package/dist/triage/actions/index.js +4 -1
  236. package/dist/triage/bootstrap/index.js +6 -6
  237. package/dist/triage/help/registry-data.d.ts +1 -1
  238. package/dist/triage/help/registry-data.js +1 -1
  239. package/dist/triage/welcome/writers.d.ts +1 -1
  240. package/dist/triage/welcome/writers.js +10 -5
  241. package/dist/ts-check-lane/index.d.ts +3 -0
  242. package/dist/ts-check-lane/index.js +2 -0
  243. package/dist/ts-check-lane/progress-reporter.d.ts +17 -0
  244. package/dist/ts-check-lane/progress-reporter.js +38 -0
  245. package/dist/ts-check-lane/progress.d.ts +32 -0
  246. package/dist/ts-check-lane/progress.js +57 -0
  247. package/dist/ts-check-lane/run-lane.d.ts +3 -1
  248. package/dist/ts-check-lane/run-lane.js +8 -6
  249. package/dist/validate-content/validate-links.d.ts +8 -0
  250. package/dist/validate-content/validate-links.js +9 -15
  251. package/dist/value/feedback-file.js +12 -2
  252. package/dist/value/readback.d.ts +4 -1
  253. package/dist/value/readback.js +51 -12
  254. package/dist/vbrief-build/index.d.ts +1 -0
  255. package/dist/vbrief-build/index.js +1 -0
  256. package/dist/vbrief-build/project-invariants-io.d.ts +16 -0
  257. package/dist/vbrief-build/project-invariants-io.js +41 -0
  258. package/dist/vbrief-reconcile/graph.js +18 -1
  259. package/dist/vbrief-reconcile/index.d.ts +1 -1
  260. package/dist/vbrief-reconcile/index.js +1 -1
  261. package/dist/vbrief-reconcile/types.d.ts +13 -0
  262. package/dist/vbrief-reconcile/umbrellas.d.ts +14 -0
  263. package/dist/vbrief-reconcile/umbrellas.js +261 -26
  264. package/dist/vbrief-validate/index.d.ts +1 -1
  265. package/dist/vbrief-validate/index.js +1 -1
  266. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  267. package/dist/vbrief-validate/plan-hooks.js +68 -0
  268. package/dist/vbrief-validate/schema.d.ts +4 -0
  269. package/dist/vbrief-validate/schema.js +1 -1
  270. package/dist/verify-ac/evaluate.d.ts +7 -1
  271. package/dist/verify-ac/evaluate.js +34 -26
  272. package/dist/verify-ac/flag.d.ts +4 -0
  273. package/dist/verify-ac/flag.js +16 -0
  274. package/dist/verify-ac/index.d.ts +2 -2
  275. package/dist/verify-ac/index.js +2 -2
  276. package/dist/verify-env/agent-hook-readiness.d.ts +2 -2
  277. package/dist/verify-env/agent-hook-readiness.js +12 -8
  278. package/dist/verify-env/agent-hooks-live-probe.d.ts +5 -1
  279. package/dist/verify-env/agent-hooks-live-probe.js +26 -5
  280. package/dist/verify-env/agent-hooks.js +3 -4
  281. package/dist/verify-source/contained-writes.js +2 -0
  282. package/dist/verify-source/verify-stubs.d.ts +15 -1
  283. package/dist/verify-source/verify-stubs.js +20 -16
  284. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +5 -6
  285. package/dist/vitest-runner/win32-coverage-tmp-setup.js +5 -6
  286. package/dist/xbrief-migrate/migrate-project.js +2 -5
  287. package/package.json +7 -3
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Worktree occupancy lease (#3433).
3
+ *
4
+ * Ritual-state is "this session completed ceremony." Occupancy is "who may
5
+ * mutate this tree right now." Those lifetimes differ; do not overload
6
+ * ritual-state.json. Join negotiation (`occupancy:request`) is out of scope.
7
+ *
8
+ * Concurrency model:
9
+ * - Assumptions: local filesystem; cooperating processes on one machine.
10
+ * - Guarantees: mutual exclusion under crash-free operation; detect-and-abort
11
+ * if the sidecar lock is compromised (fence before rename/unlink).
12
+ * - Non-goals: network filesystems; Byzantine processes; perfect off-Linux
13
+ * PID-reuse detection (hard age cap + fence instead).
14
+ */
15
+ import { randomUUID } from "node:crypto";
16
+ import { existsSync, readFileSync, renameSync, rmSync } from "node:fs";
17
+ import { dirname, join, resolve } from "node:path";
18
+ import { containedRemove, containedWrite } from "../fs/contained-write.js";
19
+ import { assertWriteTargetSafe } from "../fs/projection-containment.js";
20
+ import { assertAppendLockOwned, withAppendLock } from "../slice/lock.js";
21
+ import { stableJson } from "./json.js";
22
+ import { parseTimestamp, timestampIso } from "./time.js";
23
+ export const OCCUPANCY_SCHEMA_VERSION = 1;
24
+ export const OCCUPANCY_RELPATH = [".deft", "occupancy.json"];
25
+ /** Crash recovery TTL: 20 minutes without heartbeat (15–30 window). */
26
+ export const OCCUPANCY_TTL_MS = 20 * 60 * 1000;
27
+ export const OCCUPANCY_INTENTS = ["mutation", "swarm", "review"];
28
+ export const OCCUPANCY_JOIN_PROTOCOLS = ["none", "heartbeat-file", "parent-message"];
29
+ export function occupancyPath(projectRoot) {
30
+ return join(resolve(projectRoot), ...OCCUPANCY_RELPATH);
31
+ }
32
+ export function heartbeatAgeSeconds(record, now = new Date()) {
33
+ return Math.max(0, Math.round((now.getTime() - record.heartbeatAt.getTime()) / 1000));
34
+ }
35
+ export function isOccupancyExpired(record, now = new Date(), ttlMs = OCCUPANCY_TTL_MS) {
36
+ return now.getTime() - record.heartbeatAt.getTime() > ttlMs;
37
+ }
38
+ export function formatOccupancyRemediation(record, now = new Date()) {
39
+ const age = heartbeatAgeSeconds(record, now);
40
+ return (`Worktree occupied by session ${record.sessionId} (intent=${record.intent}, heartbeat ${age}s ago).\n` +
41
+ "Stay read-only (`session:start --read-only`), use another worktree,\n" +
42
+ "queue a join (`occupancy:request`), or steal (`occupancy:steal --confirm`).");
43
+ }
44
+ export function resolveOccupancySessionId(input = {}) {
45
+ const explicit = input.sessionId?.trim();
46
+ if (explicit)
47
+ return explicit;
48
+ const envId = (input.env ?? process.env).DEFT_SESSION_ID?.trim();
49
+ if (envId)
50
+ return envId;
51
+ return (input.newSessionId ?? randomUUID)();
52
+ }
53
+ export function readOccupancy(projectRoot) {
54
+ const path = occupancyPath(projectRoot);
55
+ try {
56
+ if (!existsSync(path))
57
+ return null;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ let payload;
63
+ try {
64
+ payload = JSON.parse(readFileSync(path, { encoding: "utf8" }));
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ return parseOccupancy(payload, resolve(projectRoot));
70
+ }
71
+ export function liveOccupant(projectRoot, now = new Date(), ttlMs = OCCUPANCY_TTL_MS) {
72
+ const record = readOccupancy(projectRoot);
73
+ if (record === null || isOccupancyExpired(record, now, ttlMs))
74
+ return null;
75
+ return record;
76
+ }
77
+ export function applyWorktreeOccupancy(projectRoot, input = {}) {
78
+ const now = input.now ?? new Date();
79
+ const path = occupancyPath(projectRoot);
80
+ const incoming = resolveOccupancySessionId(input);
81
+ const existing = readOccupancy(projectRoot);
82
+ const live = existing !== null && !isOccupancyExpired(existing, now) ? existing : null;
83
+ if (input.steal === true) {
84
+ return stealOccupancy(projectRoot, { ...input, sessionId: incoming, now });
85
+ }
86
+ if (live !== null && live.sessionId !== incoming) {
87
+ return {
88
+ action: "denied",
89
+ sessionId: incoming,
90
+ record: live,
91
+ path,
92
+ message: formatOccupancyRemediation(live, now),
93
+ code: 1,
94
+ };
95
+ }
96
+ if (input.write === false) {
97
+ return {
98
+ action: live !== null ? "heartbeat" : "claimed",
99
+ sessionId: incoming,
100
+ record: live,
101
+ path,
102
+ message: live !== null
103
+ ? `occupancy heartbeat session ${incoming} (intent=${live.intent})`
104
+ : `occupancy claimed session ${incoming} (intent=${input.intent ?? "mutation"})`,
105
+ code: 0,
106
+ };
107
+ }
108
+ return withOccupancyLock(projectRoot, (fence) => {
109
+ const existingLocked = readOccupancy(projectRoot);
110
+ const liveLocked = existingLocked !== null && !isOccupancyExpired(existingLocked, now) ? existingLocked : null;
111
+ if (liveLocked !== null && liveLocked.sessionId !== incoming) {
112
+ return {
113
+ action: "denied",
114
+ sessionId: incoming,
115
+ record: liveLocked,
116
+ path,
117
+ message: formatOccupancyRemediation(liveLocked, now),
118
+ code: 1,
119
+ };
120
+ }
121
+ const record = writeOccupancyRecord(projectRoot, {
122
+ sessionId: incoming,
123
+ worktreePath: resolve(projectRoot),
124
+ intent: input.intent ?? liveLocked?.intent ?? "mutation",
125
+ claimedAt: liveLocked?.claimedAt ?? now,
126
+ heartbeatAt: now,
127
+ host: input.host ?? liveLocked?.host ?? occupancyHost(input.env),
128
+ address: input.address ?? liveLocked?.address ?? occupancyAddress(input.env),
129
+ retainCapable: input.retainCapable ?? liveLocked?.retainCapable ?? false,
130
+ joinProtocol: input.joinProtocol ?? liveLocked?.joinProtocol ?? "none",
131
+ }, fence);
132
+ const action = liveLocked !== null ? "heartbeat" : "claimed";
133
+ return {
134
+ action,
135
+ sessionId: record.sessionId,
136
+ record,
137
+ path,
138
+ message: action === "heartbeat"
139
+ ? `occupancy heartbeat session ${record.sessionId} (intent=${record.intent})`
140
+ : `occupancy claimed session ${record.sessionId} (intent=${record.intent})`,
141
+ code: 0,
142
+ };
143
+ }, input.lockDeps);
144
+ }
145
+ export function stealOccupancy(projectRoot, input = {}) {
146
+ const now = input.now ?? new Date();
147
+ const path = occupancyPath(projectRoot);
148
+ if (input.confirm !== true) {
149
+ return {
150
+ action: "denied",
151
+ sessionId: resolveOccupancySessionId(input),
152
+ record: readOccupancy(projectRoot),
153
+ path,
154
+ message: "occupancy:steal requires --confirm after naming the occupant.",
155
+ code: 2,
156
+ };
157
+ }
158
+ const named = input.occupant?.trim() ?? "";
159
+ if (named.length === 0) {
160
+ return {
161
+ action: "denied",
162
+ sessionId: resolveOccupancySessionId(input),
163
+ record: readOccupancy(projectRoot),
164
+ path,
165
+ message: "occupancy:steal requires --occupant <session-id> to name the current occupant.",
166
+ code: 2,
167
+ };
168
+ }
169
+ const existing = readOccupancy(projectRoot);
170
+ const live = existing !== null && !isOccupancyExpired(existing, now) ? existing : null;
171
+ if (live !== null && live.sessionId !== named) {
172
+ return {
173
+ action: "denied",
174
+ sessionId: resolveOccupancySessionId(input),
175
+ record: live,
176
+ path,
177
+ message: `occupancy:steal named occupant ${named} does not match live occupant ${live.sessionId}.\n` +
178
+ formatOccupancyRemediation(live, now),
179
+ code: 1,
180
+ };
181
+ }
182
+ return withOccupancyLock(projectRoot, (fence) => {
183
+ const existingLocked = readOccupancy(projectRoot);
184
+ const liveLocked = existingLocked !== null && !isOccupancyExpired(existingLocked, now) ? existingLocked : null;
185
+ if (liveLocked !== null && liveLocked.sessionId !== named) {
186
+ return {
187
+ action: "denied",
188
+ sessionId: resolveOccupancySessionId(input),
189
+ record: liveLocked,
190
+ path,
191
+ message: `occupancy:steal named occupant ${named} does not match live occupant ${liveLocked.sessionId}.\n` +
192
+ formatOccupancyRemediation(liveLocked, now),
193
+ code: 1,
194
+ };
195
+ }
196
+ const incoming = resolveOccupancySessionId(input);
197
+ const record = writeOccupancyRecord(projectRoot, {
198
+ sessionId: incoming,
199
+ worktreePath: resolve(projectRoot),
200
+ intent: input.intent ?? "mutation",
201
+ claimedAt: now,
202
+ heartbeatAt: now,
203
+ host: input.host ?? occupancyHost(input.env),
204
+ address: input.address ?? occupancyAddress(input.env),
205
+ retainCapable: input.retainCapable ?? false,
206
+ joinProtocol: input.joinProtocol ?? "none",
207
+ }, fence);
208
+ return {
209
+ action: "stolen",
210
+ sessionId: record.sessionId,
211
+ record,
212
+ path,
213
+ message: `occupancy stolen from ${named}; writer is now session ${record.sessionId}`,
214
+ code: 0,
215
+ };
216
+ }, input.lockDeps);
217
+ }
218
+ export function releaseOccupancy(projectRoot, input = {}) {
219
+ const now = input.now ?? new Date();
220
+ const path = occupancyPath(projectRoot);
221
+ const caller = input.sessionId?.trim() || (input.env ?? process.env).DEFT_SESSION_ID?.trim() || "";
222
+ return withOccupancyLock(projectRoot, (fence) => {
223
+ const existing = readOccupancy(projectRoot);
224
+ if (existing === null) {
225
+ return {
226
+ action: "released",
227
+ sessionId: caller,
228
+ record: null,
229
+ path,
230
+ message: "occupancy already free",
231
+ code: 0,
232
+ };
233
+ }
234
+ const expired = isOccupancyExpired(existing, now);
235
+ const owns = caller.length > 0 && caller === existing.sessionId;
236
+ if (!expired && !owns) {
237
+ return {
238
+ action: "denied",
239
+ sessionId: caller,
240
+ record: existing,
241
+ path,
242
+ message: formatOccupancyRemediation(existing, now),
243
+ code: 1,
244
+ };
245
+ }
246
+ fence();
247
+ const still = readOccupancy(projectRoot);
248
+ if (still === null) {
249
+ return {
250
+ action: "released",
251
+ sessionId: caller,
252
+ record: null,
253
+ path,
254
+ message: "occupancy already free",
255
+ code: 0,
256
+ };
257
+ }
258
+ if (still.sessionId !== existing.sessionId) {
259
+ throw new Error("lock compromised: occupancy session changed before release");
260
+ }
261
+ if (!expired && caller !== still.sessionId) {
262
+ return {
263
+ action: "denied",
264
+ sessionId: caller,
265
+ record: still,
266
+ path,
267
+ message: formatOccupancyRemediation(still, now),
268
+ code: 1,
269
+ };
270
+ }
271
+ removeOccupancyFile(projectRoot, fence);
272
+ return {
273
+ action: "released",
274
+ sessionId: still.sessionId,
275
+ record: null,
276
+ path,
277
+ message: `occupancy released session ${still.sessionId}`,
278
+ code: 0,
279
+ };
280
+ }, input.lockDeps);
281
+ }
282
+ export function evaluateOccupancyWriteGate(projectRoot, input = {}) {
283
+ const now = input.now ?? new Date();
284
+ const live = liveOccupant(projectRoot, now);
285
+ if (live === null)
286
+ return { allow: true, message: null, occupant: null };
287
+ const incoming = input.sessionId?.trim() || (input.env ?? process.env).DEFT_SESSION_ID?.trim() || "";
288
+ if (incoming.length > 0 && incoming === live.sessionId) {
289
+ return { allow: true, message: null, occupant: live };
290
+ }
291
+ return {
292
+ allow: false,
293
+ message: formatOccupancyRemediation(live, now),
294
+ occupant: live,
295
+ };
296
+ }
297
+ /** Close-out identity comes from the launch manifest or DEFT_SESSION_ID — never occupancy.json. */
298
+ export function releaseSwarmOccupancy(projectRoot, input = {}) {
299
+ const env = input.env ?? process.env;
300
+ const sessionId = input.sessionId?.trim() || env.DEFT_SESSION_ID?.trim() || "";
301
+ if (sessionId.length === 0) {
302
+ const occupant = readOccupancy(projectRoot);
303
+ return {
304
+ action: "denied",
305
+ sessionId: "",
306
+ record: occupant,
307
+ path: occupancyPath(projectRoot),
308
+ message: "swarm close-out has no occupancy_session_id (manifest missing or predates the field) " +
309
+ "and DEFT_SESSION_ID is unset. Steal with occupancy:steal --confirm --occupant <id>.",
310
+ code: 1,
311
+ };
312
+ }
313
+ return releaseOccupancy(projectRoot, {
314
+ env,
315
+ now: input.now,
316
+ lockDeps: input.lockDeps,
317
+ sessionId,
318
+ });
319
+ }
320
+ export function runOccupancySteal(projectRoot, input = {}) {
321
+ return stealOccupancy(projectRoot, { ...input, steal: true });
322
+ }
323
+ function occupancyHost(env) {
324
+ const value = (env ?? process.env).DEFT_AGENT_ID?.trim();
325
+ return value && value.length > 0 ? value : "none";
326
+ }
327
+ function occupancyAddress(env) {
328
+ const value = (env ?? process.env).DEFT_SESSION_NAME?.trim();
329
+ return value && value.length > 0 ? value : "none";
330
+ }
331
+ function parseOccupancy(payload, fallbackWorktree) {
332
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload))
333
+ return null;
334
+ const obj = payload;
335
+ const sessionId = typeof obj.session_id === "string" ? obj.session_id.trim() : "";
336
+ if (sessionId.length === 0)
337
+ return null;
338
+ const intentRaw = typeof obj.intent === "string" ? obj.intent : "mutation";
339
+ const intent = OCCUPANCY_INTENTS.includes(intentRaw)
340
+ ? intentRaw
341
+ : "mutation";
342
+ const claimedAt = parseTimestamp(obj.claimed_at) ?? parseTimestamp(obj.heartbeat_at);
343
+ const heartbeatAt = parseTimestamp(obj.heartbeat_at) ?? claimedAt;
344
+ if (claimedAt === null || heartbeatAt === null)
345
+ return null;
346
+ const joinRaw = typeof obj.join_protocol === "string" ? obj.join_protocol : "none";
347
+ const joinProtocol = OCCUPANCY_JOIN_PROTOCOLS.includes(joinRaw)
348
+ ? joinRaw
349
+ : "none";
350
+ const worktreePath = typeof obj.worktree_path === "string" && obj.worktree_path.trim().length > 0
351
+ ? obj.worktree_path
352
+ : fallbackWorktree;
353
+ return {
354
+ schemaVersion: typeof obj.schemaVersion === "number" ? obj.schemaVersion : OCCUPANCY_SCHEMA_VERSION,
355
+ sessionId,
356
+ worktreePath,
357
+ intent,
358
+ claimedAt,
359
+ heartbeatAt,
360
+ host: typeof obj.host === "string" && obj.host.length > 0 ? obj.host : "none",
361
+ address: typeof obj.address === "string" && obj.address.length > 0 ? obj.address : "none",
362
+ retainCapable: obj.retain_capable === true,
363
+ joinProtocol,
364
+ raw: { ...obj },
365
+ };
366
+ }
367
+ function occupancyPayload(record) {
368
+ return {
369
+ schemaVersion: OCCUPANCY_SCHEMA_VERSION,
370
+ session_id: record.sessionId,
371
+ worktree_path: record.worktreePath,
372
+ intent: record.intent,
373
+ claimed_at: timestampIso(record.claimedAt),
374
+ heartbeat_at: timestampIso(record.heartbeatAt),
375
+ host: record.host,
376
+ address: record.address,
377
+ retain_capable: record.retainCapable,
378
+ join_protocol: record.joinProtocol,
379
+ };
380
+ }
381
+ function writeOccupancyRecord(projectRoot, record, fence) {
382
+ const root = resolve(projectRoot);
383
+ const target = occupancyPath(root);
384
+ assertWriteTargetSafe(root, target);
385
+ const dir = dirname(target);
386
+ const tmpName = join(dir, `.occupancy.${process.pid}.occupancy.json.tmp`);
387
+ const payload = occupancyPayload(record);
388
+ const text = `${stableJson(payload, 2)}\n`;
389
+ try {
390
+ containedWrite({ root, target: tmpName, data: text, mode: "create" });
391
+ fence();
392
+ renameSync(tmpName, target);
393
+ }
394
+ catch (err) {
395
+ try {
396
+ rmSync(tmpName, { force: true });
397
+ }
398
+ catch {
399
+ /* best-effort cleanup */
400
+ }
401
+ throw err;
402
+ }
403
+ const parsed = parseOccupancy(payload, record.worktreePath);
404
+ if (parsed === null) {
405
+ throw new Error("occupancy write produced an unreadable record");
406
+ }
407
+ return parsed;
408
+ }
409
+ function removeOccupancyFile(projectRoot, fence) {
410
+ const root = resolve(projectRoot);
411
+ fence();
412
+ containedRemove({ root, target: occupancyPath(root) });
413
+ }
414
+ function withOccupancyLock(projectRoot, fn, deps = {}) {
415
+ return withAppendLock(occupancyPath(projectRoot), (held) => {
416
+ const fence = () => {
417
+ assertAppendLockOwned(held);
418
+ };
419
+ return fn(fence);
420
+ }, deps);
421
+ }
422
+ //# sourceMappingURL=occupancy.js.map
@@ -5,11 +5,11 @@
5
5
  * in compact-ritual.ts. Fail-closed when OpenClaw is not detected (unless
6
6
  * forceDeposit for tests). Real directory copy — not symlink-escape into npm.
7
7
  */
8
- import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, } from "node:fs";
8
+ import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
9
9
  import { homedir } from "node:os";
10
10
  import { join } from "node:path";
11
11
  import { detectOpenClaw, isEscapingSkillSymlink, listInScopeSkillsDirs, } from "../doctor/openclaw-skills.js";
12
- import { containedWrite } from "../fs/contained-write.js";
12
+ import { containedMkdir, containedRemove, containedRename, containedWrite, } from "../fs/contained-write.js";
13
13
  import { formatOpenClawSoftRebindSkillMarkdown, isManagedOpenClawSoftRebindSkill, OPENCLAW_SOFT_REBIND_SKILL_ID, } from "./compact-ritual.js";
14
14
  function defaultIsDir(path) {
15
15
  try {
@@ -98,7 +98,7 @@ export function depositOpenClawSoftRebindSkill(options = {}) {
98
98
  if (kind === "symlink" && isEscapingSkillSymlink(skillsDir, skillDir)) {
99
99
  // Replace unusable escaping symlink with a real copy.
100
100
  try {
101
- rmSync(skillDir, { recursive: true, force: true });
101
+ containedRemove({ root: skillsDir, target: skillDir, recursive: true });
102
102
  }
103
103
  catch {
104
104
  // best-effort
@@ -121,7 +121,7 @@ export function depositOpenClawSoftRebindSkill(options = {}) {
121
121
  continue;
122
122
  }
123
123
  }
124
- mkdirSync(skillDir, { recursive: true });
124
+ containedMkdir({ root: skillsDir, target: skillDir });
125
125
  const tmpName = `SKILL.md.deft-${process.pid}-${Date.now().toString(36)}.tmp`;
126
126
  const staging = join(skillDir, tmpName);
127
127
  try {
@@ -132,11 +132,11 @@ export function depositOpenClawSoftRebindSkill(options = {}) {
132
132
  data: expected,
133
133
  mode: "replace",
134
134
  });
135
- renameSync(staging, skillFile);
135
+ containedRename({ root: skillsDir, from: staging, to: skillFile, mutation: false });
136
136
  }
137
137
  catch (err) {
138
138
  try {
139
- rmSync(staging, { force: true });
139
+ containedRemove({ root: skillsDir, target: staging, mutation: false });
140
140
  }
141
141
  catch {
142
142
  /* best-effort */
@@ -1,13 +1,3 @@
1
- /**
2
- * Local process-cost ceremony events (#2994).
3
- *
4
- * Always-on, best-effort appends to `.deft-cache/events.jsonl`.
5
- * Not gated on valueFeedback (distinct from #1709 attribution).
6
- * No remote / Product Insights upload (#2603 not required).
7
- *
8
- * Types are declared locally (not imported from session-start) so this module
9
- * does not form an import cycle with session-start call sites.
10
- */
11
1
  import { type BehavioralEventRecord } from "../lifecycle/events.js";
12
2
  export { PROCESS_COST_EVENT_NAMES, PROCESS_COST_REQUIRED_PAYLOAD, type ProcessCostEventName, } from "./process-cost-constants.js";
13
3
  /** Ceremony tier labels mirror session-start cold|rearm (#2992 / #2994). */
@@ -50,4 +40,36 @@ export declare function emitSessionStartProcessCost(input: SessionStartProcessCo
50
40
  * Returns null on any failure (telemetry must not change deny verdict).
51
41
  */
52
42
  export declare function emitSessionRitualBlockedProcessCost(input: SessionRitualBlockedProcessCostInput, options: EmitProcessCostOptions): BehavioralEventRecord | null;
43
+ export interface CeremonyCostRollup {
44
+ /** Discriminator: CLI process time, not agent-turn wall clock (#3500 / #3508). */
45
+ readonly kind: "cli_process_time";
46
+ readonly windowLabel: string;
47
+ readonly lastColdDurationMs: number | null;
48
+ readonly lastRearmDurationMs: number | null;
49
+ readonly lastColdSteps: readonly ProcessCostStepTiming[];
50
+ readonly lastRearmSteps: readonly ProcessCostStepTiming[];
51
+ readonly blockedRitualCount: number;
52
+ readonly recoveryTierDistribution: Readonly<Record<string, number>>;
53
+ }
54
+ /**
55
+ * Roll up ceremony cost from `.deft-cache/events.jsonl` (#3508).
56
+ * Measures CLI process time, not agent-turn wall clock (#3500).
57
+ */
58
+ export declare function computeCeremonyCostRollup(options: {
59
+ projectRoot: string;
60
+ logPath?: string | null;
61
+ windowMs?: number;
62
+ now?: Date;
63
+ windowLabel?: string;
64
+ }): CeremonyCostRollup;
65
+ /**
66
+ * Human rollup for the composed `value:show` reader (#3508).
67
+ * CLI process time, not agent-turn wall clock (#3500).
68
+ */
69
+ export declare function formatCeremonyCostReport(rollup: CeremonyCostRollup): string;
70
+ /**
71
+ * Operator-visible mutation `session:start` line (#3508).
72
+ * CLI process time only. ⊗ not #3286 Later graduation input.
73
+ */
74
+ export declare function formatSessionStartCeremonyCostLine(ceremonyTier: ProcessCostCeremonyTier, durationMs: number): string;
53
75
  //# sourceMappingURL=process-cost.d.ts.map