@deftai/directive-core 0.107.0 → 0.109.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 (271) hide show
  1. package/dist/architecture/sor-preflight.js +2 -0
  2. package/dist/cache/operations.js +1 -1
  3. package/dist/check/cached-orchestrator.js +28 -3
  4. package/dist/check/gate-lists.d.ts +14 -0
  5. package/dist/check/gate-lists.js +26 -3
  6. package/dist/check/named-cause.js +18 -3
  7. package/dist/check/session-completed-ac.d.ts +1 -1
  8. package/dist/check/session-completed-ac.js +1 -1
  9. package/dist/codebase/provider.js +7 -1
  10. package/dist/consumer-check-contract/evaluate.d.ts +47 -0
  11. package/dist/consumer-check-contract/evaluate.js +181 -15
  12. package/dist/delivery-attempt/evaluate.d.ts +9 -1
  13. package/dist/delivery-attempt/evaluate.js +69 -0
  14. package/dist/delivery-attempt/handoff.js +1 -1
  15. package/dist/delivery-attempt/index.d.ts +1 -1
  16. package/dist/delivery-attempt/index.js +1 -1
  17. package/dist/deposit/live-procedure-exclusions.d.ts +18 -0
  18. package/dist/deposit/live-procedure-exclusions.js +110 -0
  19. package/dist/deposit/live-procedure-targets.d.ts +45 -0
  20. package/dist/deposit/live-procedure-targets.js +274 -0
  21. package/dist/deposit/python-free.d.ts +6 -0
  22. package/dist/deposit/python-free.js +15 -0
  23. package/dist/deposit/rewrite-deposit-links.d.ts +42 -0
  24. package/dist/deposit/rewrite-deposit-links.js +148 -0
  25. package/dist/deposit/run-stage-content-pack.d.ts +2 -0
  26. package/dist/deposit/run-stage-content-pack.js +3 -0
  27. package/dist/deposit/stage-content-pack.d.ts +17 -0
  28. package/dist/deposit/stage-content-pack.js +91 -0
  29. package/dist/design-critique/citation-grammar.d.ts +60 -0
  30. package/dist/design-critique/citation-grammar.js +303 -0
  31. package/dist/design-critique/completed-arc-record.d.ts +62 -0
  32. package/dist/design-critique/completed-arc-record.js +277 -0
  33. package/dist/freshness/bind.d.ts +32 -3
  34. package/dist/freshness/bind.js +58 -12
  35. package/dist/hooks/classify/host-session-identity.d.ts +89 -0
  36. package/dist/hooks/classify/host-session-identity.js +467 -0
  37. package/dist/hooks/classify/index.d.ts +3 -2
  38. package/dist/hooks/classify/index.js +3 -2
  39. package/dist/hooks/classify/paths.d.ts +9 -0
  40. package/dist/hooks/classify/paths.js +39 -0
  41. package/dist/hooks/classify/stdin.d.ts +5 -0
  42. package/dist/hooks/classify/stdin.js +94 -1
  43. package/dist/hooks/dest-form.d.ts +20 -1
  44. package/dist/hooks/dest-form.js +158 -21
  45. package/dist/hooks/dispatcher.d.ts +58 -3
  46. package/dist/hooks/dispatcher.js +762 -73
  47. package/dist/hooks/fixtures/cases.d.ts +22 -2
  48. package/dist/hooks/fixtures/cases.js +228 -0
  49. package/dist/hooks/git-destructive-log.d.ts +32 -0
  50. package/dist/hooks/git-destructive-log.js +46 -0
  51. package/dist/hooks/index.d.ts +2 -0
  52. package/dist/hooks/index.js +2 -0
  53. package/dist/hooks/scope.js +3 -1
  54. package/dist/hooks/shell-write-targets.d.ts +10 -0
  55. package/dist/hooks/shell-write-targets.js +274 -0
  56. package/dist/hooks/tools.d.ts +20 -2
  57. package/dist/hooks/tools.js +30 -1
  58. package/dist/init-deposit/agent-hooks.d.ts +2 -1
  59. package/dist/init-deposit/agent-hooks.js +8 -1
  60. package/dist/init-deposit/gitignore.d.ts +7 -0
  61. package/dist/init-deposit/gitignore.js +24 -0
  62. package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
  63. package/dist/init-deposit/hook-runtime-travel.js +269 -0
  64. package/dist/init-deposit/init-deposit.js +3 -0
  65. package/dist/init-deposit/refresh.js +7 -0
  66. package/dist/init-deposit/runtime-writers.d.ts +14 -0
  67. package/dist/init-deposit/runtime-writers.js +33 -0
  68. package/dist/intake/clause-derivation.js +9 -3
  69. package/dist/intake/issue-ingest.d.ts +6 -1
  70. package/dist/intake/issue-ingest.js +83 -4
  71. package/dist/intake/platform-capabilities.d.ts +9 -2
  72. package/dist/intake/platform-capabilities.js +38 -9
  73. package/dist/intake/reconcile-issues.js +13 -13
  74. package/dist/lifecycle/brief-envelope.d.ts +25 -0
  75. package/dist/lifecycle/brief-envelope.js +42 -0
  76. package/dist/lifecycle/index.d.ts +1 -0
  77. package/dist/lifecycle/index.js +1 -0
  78. package/dist/literal-acceptance/evaluate.js +14 -5
  79. package/dist/literal-acceptance/index.d.ts +1 -1
  80. package/dist/literal-acceptance/index.js +1 -1
  81. package/dist/literal-acceptance/run.d.ts +2 -0
  82. package/dist/literal-acceptance/run.js +19 -1
  83. package/dist/orphan-active/candidate-scope.d.ts +53 -0
  84. package/dist/orphan-active/candidate-scope.js +157 -0
  85. package/dist/orphan-active/evaluate.d.ts +52 -0
  86. package/dist/orphan-active/evaluate.js +250 -128
  87. package/dist/orphan-active/index.d.ts +2 -0
  88. package/dist/orphan-active/index.js +2 -0
  89. package/dist/orphan-active/issue-state.d.ts +121 -0
  90. package/dist/orphan-active/issue-state.js +278 -0
  91. package/dist/platform/cursor-managed-runtime.d.ts +95 -0
  92. package/dist/platform/cursor-managed-runtime.js +241 -0
  93. package/dist/platform/index.d.ts +1 -0
  94. package/dist/platform/index.js +1 -0
  95. package/dist/platform/platform-capabilities.d.ts +23 -1
  96. package/dist/platform/platform-capabilities.js +55 -15
  97. package/dist/policy/ceremony-dial.js +5 -9
  98. package/dist/policy/host-hooks.js +4 -9
  99. package/dist/policy/merge-approval-head.js +7 -6
  100. package/dist/policy/org-force-on-migration.js +5 -10
  101. package/dist/policy/plan-extensions.d.ts +14 -2
  102. package/dist/policy/plan-extensions.js +24 -3
  103. package/dist/policy/product-signal.js +5 -10
  104. package/dist/policy/require-human-merge.js +5 -9
  105. package/dist/policy/resolve.js +77 -16
  106. package/dist/policy/value-feedback.js +9 -18
  107. package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
  108. package/dist/pr-closeout-attestable/evaluate.js +306 -0
  109. package/dist/pr-closeout-attestable/index.d.ts +2 -0
  110. package/dist/pr-closeout-attestable/index.js +2 -0
  111. package/dist/pr-closing-keywords/gh.js +32 -9
  112. package/dist/pr-closing-keywords/main.d.ts +1 -0
  113. package/dist/pr-closing-keywords/main.js +68 -3
  114. package/dist/pr-closing-keywords/types.d.ts +2 -0
  115. package/dist/pr-merge-readiness/gh.js +32 -9
  116. package/dist/pr-protected-issues/gh.d.ts +6 -2
  117. package/dist/pr-protected-issues/gh.js +42 -11
  118. package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
  119. package/dist/pr-wait-mergeable/cascade.js +21 -1
  120. package/dist/pr-wait-mergeable/types.d.ts +2 -0
  121. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  122. package/dist/pr-wait-mergeable/wrappers.js +14 -3
  123. package/dist/preflight/evaluate.d.ts +15 -0
  124. package/dist/preflight/evaluate.js +29 -3
  125. package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
  126. package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
  127. package/dist/product-first-done-gate/evaluate.js +23 -9
  128. package/dist/product-first-done-gate/types.js +2 -0
  129. package/dist/release/cli-drift-report.d.ts +68 -0
  130. package/dist/release/cli-drift-report.js +189 -0
  131. package/dist/release/consumer-hard-stops.d.ts +48 -0
  132. package/dist/release/consumer-hard-stops.js +140 -0
  133. package/dist/release/consumer-readiness-disclosure.d.ts +18 -0
  134. package/dist/release/consumer-readiness-disclosure.js +51 -0
  135. package/dist/release/index.d.ts +3 -0
  136. package/dist/release/index.js +3 -0
  137. package/dist/release/issue-state-fetch.d.ts +5 -3
  138. package/dist/release/issue-state-fetch.js +86 -21
  139. package/dist/release/native-steps.js +1 -0
  140. package/dist/release/pipeline.js +74 -0
  141. package/dist/release/run-consumer-readiness.d.ts +15 -0
  142. package/dist/release/run-consumer-readiness.js +24 -0
  143. package/dist/release/types.d.ts +20 -0
  144. package/dist/render/constants.d.ts +4 -0
  145. package/dist/render/constants.js +11 -8
  146. package/dist/render/export-spec.js +31 -5
  147. package/dist/render/index.d.ts +1 -1
  148. package/dist/render/index.js +1 -1
  149. package/dist/render/prd-render.d.ts +4 -1
  150. package/dist/render/prd-render.js +82 -23
  151. package/dist/render/project-render.js +14 -7
  152. package/dist/render/scope-outlook.d.ts +2 -0
  153. package/dist/render/scope-outlook.js +3 -0
  154. package/dist/render/spec-render.js +2 -2
  155. package/dist/resolution/package-manager.d.ts +33 -4
  156. package/dist/resolution/package-manager.js +210 -9
  157. package/dist/review-monitor/constants.d.ts +7 -0
  158. package/dist/review-monitor/constants.js +7 -0
  159. package/dist/review-monitor/github-lease.d.ts +98 -1
  160. package/dist/review-monitor/github-lease.js +186 -4
  161. package/dist/review-monitor/lease-comment.d.ts +36 -0
  162. package/dist/review-monitor/lease-comment.js +103 -2
  163. package/dist/run-summary/types.d.ts +2 -2
  164. package/dist/scm/build-command.d.ts +2 -2
  165. package/dist/scm/build-command.js +2 -2
  166. package/dist/scm/call-shape.d.ts +25 -0
  167. package/dist/scm/call-shape.js +59 -0
  168. package/dist/scm/call.d.ts +7 -4
  169. package/dist/scm/call.js +48 -9
  170. package/dist/scm/design-critique-chip.d.ts +1 -0
  171. package/dist/scm/design-critique-chip.js +22 -6
  172. package/dist/scm/gh-rest.d.ts +34 -13
  173. package/dist/scm/gh-rest.js +168 -15
  174. package/dist/scm/index.d.ts +2 -0
  175. package/dist/scm/index.js +2 -0
  176. package/dist/scm/readiness.d.ts +2 -0
  177. package/dist/scm/readiness.js +28 -4
  178. package/dist/scm/spawn-status.d.ts +33 -0
  179. package/dist/scm/spawn-status.js +53 -0
  180. package/dist/scope/acceptance-evidence.d.ts +1 -1
  181. package/dist/scope/project-definition-sync.js +14 -7
  182. package/dist/scope/transition.js +4 -14
  183. package/dist/session/ac-pass-banking.d.ts +2 -2
  184. package/dist/session/ac-pass-banking.js +2 -2
  185. package/dist/session/git.d.ts +24 -0
  186. package/dist/session/git.js +138 -2
  187. package/dist/session/host-session-owner.d.ts +53 -0
  188. package/dist/session/host-session-owner.js +89 -0
  189. package/dist/session/index.d.ts +1 -0
  190. package/dist/session/index.js +1 -0
  191. package/dist/session/occupancy.d.ts +259 -13
  192. package/dist/session/occupancy.js +877 -33
  193. package/dist/session/ritual-sentinel.d.ts +5 -0
  194. package/dist/session/ritual-sentinel.js +5 -0
  195. package/dist/session/session-ready.d.ts +5 -1
  196. package/dist/session/session-ready.js +98 -8
  197. package/dist/session/session-start.d.ts +5 -1
  198. package/dist/session/session-start.js +32 -17
  199. package/dist/session/toolchain-preflight.d.ts +13 -3
  200. package/dist/session/toolchain-preflight.js +93 -18
  201. package/dist/session/verify-ac-session-cache.d.ts +2 -2
  202. package/dist/session/verify-ac-session-cache.js +2 -2
  203. package/dist/session/verify-session-ritual.d.ts +11 -0
  204. package/dist/session/verify-session-ritual.js +60 -15
  205. package/dist/slice/constants.d.ts +1 -1
  206. package/dist/spec-authority/constants.d.ts +10 -2
  207. package/dist/spec-authority/constants.js +53 -8
  208. package/dist/spec-authority/resolver.d.ts +3 -0
  209. package/dist/spec-authority/resolver.js +55 -7
  210. package/dist/subprocess/max-buffer.d.ts +14 -0
  211. package/dist/subprocess/max-buffer.js +15 -0
  212. package/dist/swarm/complete-cohort.d.ts +2 -0
  213. package/dist/swarm/complete-cohort.js +21 -13
  214. package/dist/swarm/index.d.ts +1 -0
  215. package/dist/swarm/index.js +1 -0
  216. package/dist/swarm/launch-cli.js +53 -33
  217. package/dist/swarm/launch.d.ts +8 -0
  218. package/dist/swarm/launch.js +63 -43
  219. package/dist/swarm/pre-dispatch-cli.js +2 -1
  220. package/dist/swarm/pre-dispatch.js +10 -1
  221. package/dist/swarm/subagent-status-dir.d.ts +27 -0
  222. package/dist/swarm/subagent-status-dir.js +42 -0
  223. package/dist/swarm/worktrees.d.ts +17 -3
  224. package/dist/swarm/worktrees.js +48 -4
  225. package/dist/triage/evaluate/worktrees.js +153 -6
  226. package/dist/triage/scope/mutations-core.d.ts +2 -6
  227. package/dist/triage/scope/mutations-core.js +23 -55
  228. package/dist/triage/scope-drift/add-ignore.js +5 -39
  229. package/dist/triage/subscribe/index.d.ts +2 -6
  230. package/dist/triage/subscribe/index.js +15 -65
  231. package/dist/triage/welcome/writers.js +15 -74
  232. package/dist/umbrella-current-shape/index.d.ts +51 -3
  233. package/dist/umbrella-current-shape/index.js +106 -18
  234. package/dist/validate-content/deposit-required.d.ts +39 -0
  235. package/dist/validate-content/deposit-required.js +147 -0
  236. package/dist/validate-content/index.d.ts +1 -0
  237. package/dist/validate-content/index.js +1 -0
  238. package/dist/validate-content/validate-links.d.ts +2 -3
  239. package/dist/validate-content/validate-links.js +29 -2
  240. package/dist/vbrief-activate/activate.d.ts +7 -2
  241. package/dist/vbrief-activate/activate.js +26 -13
  242. package/dist/vbrief-build/index.d.ts +2 -1
  243. package/dist/vbrief-build/index.js +2 -1
  244. package/dist/vbrief-build/parity-scenarios.js +5 -5
  245. package/dist/vbrief-build/project-definition-io.d.ts +63 -3
  246. package/dist/vbrief-build/project-definition-io.js +357 -31
  247. package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
  248. package/dist/vbrief-build/project-definition-mutation.js +43 -0
  249. package/dist/vbrief-validate/conformance.d.ts +1 -0
  250. package/dist/vbrief-validate/conformance.js +85 -8
  251. package/dist/vbrief-validate/main.js +2 -0
  252. package/dist/vbrief-validate/precutover.js +5 -10
  253. package/dist/verify-ac/clauses.d.ts +62 -2
  254. package/dist/verify-ac/clauses.js +183 -111
  255. package/dist/verify-ac/evaluate.d.ts +9 -0
  256. package/dist/verify-ac/evaluate.js +32 -9
  257. package/dist/verify-ac/index.d.ts +1 -1
  258. package/dist/verify-ac/index.js +1 -1
  259. package/dist/verify-env/node-runtime.d.ts +8 -4
  260. package/dist/verify-env/node-runtime.js +9 -6
  261. package/dist/verify-env/toolchain-check.d.ts +35 -3
  262. package/dist/verify-env/toolchain-check.js +155 -36
  263. package/dist/verify-source/deposit-closure.d.ts +23 -0
  264. package/dist/verify-source/deposit-closure.js +162 -0
  265. package/dist/verify-source/index.d.ts +2 -0
  266. package/dist/verify-source/index.js +2 -0
  267. package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
  268. package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
  269. package/dist/verify-source/semantic-single-source.d.ts +36 -0
  270. package/dist/verify-source/semantic-single-source.js +349 -0
  271. package/package.json +7 -3
@@ -56,6 +56,11 @@ export interface MarkRitualStaleAfterCompactResult {
56
56
  * Invalidate an existing mutation ritual after host context compaction/resume (#2113).
57
57
  * Reuses ritual-state age semantics — no parallel policy stack.
58
58
  * Marks rearm_needed so recovery messaging prefers session:start --rearm when cold is unnecessary (#2992).
59
+ *
60
+ * Fail-open on an unbindable actor (#3769, operator amend 2026-08-28): compact
61
+ * carries no acting identity and runs inside a hook budget, so a skipped write
62
+ * has no surface a human reliably sees. The accepted cost is that an
63
+ * unidentified compact re-arms whoever owns the record.
59
64
  */
60
65
  export declare function markRitualStaleAfterCompact(projectRoot: string, input?: {
61
66
  now?: Date;
@@ -209,6 +209,11 @@ export const RITUAL_STALE_EPOCH = new Date("1970-01-01T00:00:00Z");
209
209
  * Invalidate an existing mutation ritual after host context compaction/resume (#2113).
210
210
  * Reuses ritual-state age semantics — no parallel policy stack.
211
211
  * Marks rearm_needed so recovery messaging prefers session:start --rearm when cold is unnecessary (#2992).
212
+ *
213
+ * Fail-open on an unbindable actor (#3769, operator amend 2026-08-28): compact
214
+ * carries no acting identity and runs inside a hook budget, so a skipped write
215
+ * has no surface a human reliably sees. The accepted cost is that an
216
+ * unidentified compact re-arms whoever owns the record.
212
217
  */
213
218
  export function markRitualStaleAfterCompact(projectRoot, input = {}) {
214
219
  const now = input.now ?? new Date();
@@ -22,6 +22,8 @@ export declare const SESSION_READY_FAILED = "failed";
22
22
  export type SessionReadyPath = typeof SESSION_READY_FAST_PATH | typeof SESSION_READY_RECOVERED | typeof SESSION_READY_VERIFIED | typeof SESSION_READY_FAILED;
23
23
  export interface SessionReadyResult {
24
24
  readonly code: number;
25
+ /** One resolved owner carried through preview, verification, and claim (#3611). */
26
+ readonly sessionId: string;
25
27
  readonly message: string;
26
28
  readonly path: SessionReadyPath;
27
29
  readonly lines: readonly string[];
@@ -40,9 +42,11 @@ export interface SessionReadyOptions {
40
42
  readonly runGit?: GitRunner;
41
43
  readonly runner?: RitualRunner;
42
44
  readonly env?: NodeJS.ProcessEnv;
45
+ /** Explicit lifecycle owner resolved by the host/session bridge (#3611). */
46
+ readonly sessionId?: string;
43
47
  /** When set, overrides DEFT_TRIAGE_REPO / git remote inference for cache recovery. */
44
48
  readonly repo?: string | null;
45
- readonly sessionStartOptions?: Omit<SessionStartOptions, "now" | "runGit" | "env">;
49
+ readonly sessionStartOptions?: Omit<SessionStartOptions, "now" | "runGit" | "env" | "sessionId">;
46
50
  readonly inspectRitual?: (projectRoot: string, options: InspectSessionRitualOptions) => VerifyResult;
47
51
  readonly verifyRitual?: (projectRoot: string, options: VerifySessionRitualOptions) => VerifyResult;
48
52
  readonly runStart?: (projectRoot: string, options: SessionStartOptions) => SessionStartResult;
@@ -15,7 +15,7 @@ import { execFileSync } from "node:child_process";
15
15
  import { join } from "node:path";
16
16
  import { cacheFetchAll } from "../cache/fetch.js";
17
17
  import { formatFrameworkCommand } from "../render/framework-commands.js";
18
- import { applyWorktreeOccupancy, } from "./occupancy.js";
18
+ import { applyWorktreeOccupancy, resolveOccupancySessionId, } from "./occupancy.js";
19
19
  import { runSessionStart, } from "./session-start.js";
20
20
  import { inspectSessionRitual, verifySessionRitual, } from "./verify-session-ritual.js";
21
21
  export const SESSION_READY_FAST_PATH = "fast";
@@ -91,28 +91,39 @@ export function runSessionReady(projectRoot, options = {}) {
91
91
  const env = options.env ?? process.env;
92
92
  const lines = [];
93
93
  const steps = [];
94
+ // #3611: resolve once before any occupancy preview or nested lifecycle call.
95
+ // A nested start must report the same persisted owner; adopting a different
96
+ // result would silently split the invocation's identity.
97
+ const sessionId = resolveOccupancySessionId({
98
+ sessionId: options.sessionId,
99
+ env,
100
+ newSessionId: options.sessionStartOptions?.newSessionId,
101
+ });
102
+ let nestedStartClaimedOccupancy = false;
94
103
  const inspect = options.inspectRitual ?? inspectSessionRitual;
95
104
  const verify = options.verifyRitual ?? verifySessionRitual;
96
105
  const start = options.runStart ?? runSessionStart;
97
106
  const fetchAll = options.fetchAll ?? cacheFetchAll;
98
107
  const inferRepo = options.inferRepo ?? ((root) => inferSessionReadyRepo(root, env));
99
108
  const applyOccupancy = options.applyOccupancy ?? applyWorktreeOccupancy;
100
- const occupancyInput = (write) => ({
109
+ const occupancyInput = (write, steal) => ({
110
+ sessionId,
101
111
  env,
102
112
  now,
103
- steal: options.sessionStartOptions?.steal,
113
+ steal,
104
114
  confirm: options.sessionStartOptions?.confirm,
105
115
  occupant: options.sessionStartOptions?.occupant,
106
- newSessionId: options.sessionStartOptions?.newSessionId,
107
116
  intent: options.sessionStartOptions?.occupancyIntent ?? "mutation",
108
117
  write,
109
118
  });
110
- const previewOccupancy = applyOccupancy(projectRoot, occupancyInput(false));
119
+ const requestedSteal = options.sessionStartOptions?.steal === true;
120
+ const previewOccupancy = applyOccupancy(projectRoot, occupancyInput(false, requestedSteal));
111
121
  if (previewOccupancy.code !== 0) {
112
122
  const message = previewOccupancy.message;
113
123
  lines.push(message);
114
124
  return {
115
125
  code: previewOccupancy.code,
126
+ sessionId,
116
127
  message,
117
128
  path: SESSION_READY_FAILED,
118
129
  lines,
@@ -120,13 +131,14 @@ export function runSessionReady(projectRoot, options = {}) {
120
131
  duration_ms: elapsedMs(started),
121
132
  };
122
133
  }
123
- const claimOnSuccess = () => applyOccupancy(projectRoot, occupancyInput(true));
134
+ const claimOnSuccess = () => applyOccupancy(projectRoot, occupancyInput(true, nestedStartClaimedOccupancy ? false : requestedSteal));
124
135
  const finishReady = (path, message) => {
125
136
  const claimed = claimOnSuccess();
126
137
  if (claimed.code !== 0) {
127
138
  lines.push(claimed.message);
128
139
  return {
129
140
  code: claimed.code,
141
+ sessionId,
130
142
  message: claimed.message,
131
143
  path: SESSION_READY_FAILED,
132
144
  lines,
@@ -134,9 +146,39 @@ export function runSessionReady(projectRoot, options = {}) {
134
146
  duration_ms: elapsedMs(started),
135
147
  };
136
148
  }
149
+ // The verified ritual can change while the final occupancy claim is being
150
+ // persisted. Re-read only the exact bound ritual owner after the claim;
151
+ // never return success with a lease/ritual split (#3611). Files remain a
152
+ // cooperative gate, so this narrows rather than eliminates TOCTOU.
153
+ const postClaimRitual = inspect(projectRoot, {
154
+ tier: "gated",
155
+ posture: "mutation",
156
+ now,
157
+ runGit: options.runGit,
158
+ });
159
+ if (postClaimRitual.code !== 0 || postClaimRitual.boundSessionId !== sessionId) {
160
+ const actual = postClaimRitual.boundSessionId ?? "<unbound>";
161
+ const detail = postClaimRitual.code === 0
162
+ ? `post-claim ritual owner ${actual}`
163
+ : `post-claim ritual inspection failed: ${postClaimRitual.message}`;
164
+ const failure = `session:ready ${detail}, but occupancy is held by ${sessionId}. ` +
165
+ "Readiness remains fail-closed. Re-run session:start with the same explicit " +
166
+ "--session-id to align ritual and occupancy state.";
167
+ lines.push(failure);
168
+ return {
169
+ code: postClaimRitual.code === 0 ? 1 : postClaimRitual.code,
170
+ sessionId,
171
+ message: failure,
172
+ path: SESSION_READY_FAILED,
173
+ lines,
174
+ steps,
175
+ duration_ms: elapsedMs(started),
176
+ };
177
+ }
137
178
  lines.push(message);
138
179
  return {
139
180
  code: 0,
181
+ sessionId,
140
182
  message,
141
183
  path,
142
184
  lines,
@@ -144,6 +186,22 @@ export function runSessionReady(projectRoot, options = {}) {
144
186
  duration_ms: elapsedMs(started),
145
187
  };
146
188
  };
189
+ const ownerAlignmentFailure = (actualOwner, context = "verified ritual") => {
190
+ const actual = actualOwner ?? "<unbound>";
191
+ const message = `session:ready ${context} owner ${actual}, but the resolved occupancy owner is ` +
192
+ `${sessionId}. Refusing to claim a mismatched lease. Re-run session:start with the ` +
193
+ "same explicit --session-id to align ritual and occupancy state.";
194
+ lines.push(message);
195
+ return {
196
+ code: 1,
197
+ sessionId,
198
+ message,
199
+ path: SESSION_READY_FAILED,
200
+ lines,
201
+ steps,
202
+ duration_ms: elapsedMs(started),
203
+ };
204
+ };
147
205
  const verifyOpts = {
148
206
  tier: "gated",
149
207
  posture: "mutation",
@@ -159,7 +217,10 @@ export function runSessionReady(projectRoot, options = {}) {
159
217
  now,
160
218
  runGit: options.runGit,
161
219
  });
162
- if (gatedInspect.code === 0) {
220
+ // A confirmed owner transition must also rewrite ritual state. Even a green
221
+ // legacy ritual cannot take the fast path, because stealing only in the
222
+ // final occupancy claim would leave lease and ritual owners mismatched.
223
+ if (gatedInspect.code === 0 && gatedInspect.boundSessionId === sessionId && !requestedSteal) {
163
224
  steps.push("verify:session-ritual:gated");
164
225
  const refreshed = verify(projectRoot, verifyOpts);
165
226
  if (!isGatedVerifyActuallyReady(refreshed)) {
@@ -169,6 +230,7 @@ export function runSessionReady(projectRoot, options = {}) {
169
230
  lines.push(message);
170
231
  return {
171
232
  code: refreshed.code === 0 ? 1 : refreshed.code,
233
+ sessionId,
172
234
  message,
173
235
  path: SESSION_READY_FAILED,
174
236
  lines,
@@ -176,6 +238,9 @@ export function runSessionReady(projectRoot, options = {}) {
176
238
  duration_ms: elapsedMs(started),
177
239
  };
178
240
  }
241
+ if (refreshed.boundSessionId !== sessionId) {
242
+ return ownerAlignmentFailure(refreshed.boundSessionId);
243
+ }
179
244
  return finishReady(SESSION_READY_FAST_PATH, "OK session ready (gated ritual already fresh).");
180
245
  }
181
246
  // --- Ensure quick-tier ritual state when missing / stale / drifted ---
@@ -185,13 +250,14 @@ export function runSessionReady(projectRoot, options = {}) {
185
250
  now,
186
251
  runGit: options.runGit,
187
252
  });
188
- if (quickInspect.code !== 0) {
253
+ if (quickInspect.code !== 0 || quickInspect.boundSessionId !== sessionId || requestedSteal) {
189
254
  steps.push("session:start");
190
255
  const startResult = start(projectRoot, {
191
256
  ...options.sessionStartOptions,
192
257
  now,
193
258
  runGit: options.runGit,
194
259
  env,
260
+ sessionId,
195
261
  writeHistory: options.sessionStartOptions?.writeHistory ?? false,
196
262
  });
197
263
  for (const line of startResult.lines) {
@@ -203,6 +269,7 @@ export function runSessionReady(projectRoot, options = {}) {
203
269
  lines.push(message);
204
270
  return {
205
271
  code: startResult.code,
272
+ sessionId,
206
273
  message,
207
274
  path: SESSION_READY_FAILED,
208
275
  lines,
@@ -210,11 +277,26 @@ export function runSessionReady(projectRoot, options = {}) {
210
277
  duration_ms: elapsedMs(started),
211
278
  };
212
279
  }
280
+ const startedOccupancy = startResult.payload.occupancy;
281
+ if (startedOccupancy !== null &&
282
+ typeof startedOccupancy === "object" &&
283
+ !Array.isArray(startedOccupancy)) {
284
+ const persistedSessionId = startedOccupancy.session_id;
285
+ if (typeof persistedSessionId === "string" && persistedSessionId.trim().length > 0) {
286
+ if (persistedSessionId.trim() !== sessionId) {
287
+ return ownerAlignmentFailure(persistedSessionId.trim(), "nested session:start");
288
+ }
289
+ nestedStartClaimedOccupancy = true;
290
+ }
291
+ }
213
292
  }
214
293
  // --- Gated verify (lazy doctor + cache_fresh) ---
215
294
  steps.push("verify:session-ritual:gated");
216
295
  let verifyResult = verify(projectRoot, verifyOpts);
217
296
  if (isGatedVerifyActuallyReady(verifyResult)) {
297
+ if (verifyResult.boundSessionId !== sessionId) {
298
+ return ownerAlignmentFailure(verifyResult.boundSessionId);
299
+ }
218
300
  return finishReady(SESSION_READY_VERIFIED, "OK session ready (gated ritual verified).");
219
301
  }
220
302
  if (verifyResult.code === 0 && verifyResult.bypassed) {
@@ -222,6 +304,7 @@ export function runSessionReady(projectRoot, options = {}) {
222
304
  lines.push(message);
223
305
  return {
224
306
  code: 1,
307
+ sessionId,
225
308
  message,
226
309
  path: SESSION_READY_FAILED,
227
310
  lines,
@@ -238,6 +321,7 @@ export function runSessionReady(projectRoot, options = {}) {
238
321
  lines.push(message);
239
322
  return {
240
323
  code: 1,
324
+ sessionId,
241
325
  message,
242
326
  path: SESSION_READY_FAILED,
243
327
  lines,
@@ -262,6 +346,7 @@ export function runSessionReady(projectRoot, options = {}) {
262
346
  lines.push(message);
263
347
  return {
264
348
  code: 1,
349
+ sessionId,
265
350
  message,
266
351
  path: SESSION_READY_FAILED,
267
352
  lines,
@@ -272,6 +357,9 @@ export function runSessionReady(projectRoot, options = {}) {
272
357
  steps.push("verify:session-ritual:gated:retry");
273
358
  verifyResult = verify(projectRoot, { ...verifyOpts, forceGatedSteps: [] });
274
359
  if (isGatedVerifyActuallyReady(verifyResult)) {
360
+ if (verifyResult.boundSessionId !== sessionId) {
361
+ return ownerAlignmentFailure(verifyResult.boundSessionId);
362
+ }
275
363
  return finishReady(SESSION_READY_RECOVERED, "OK session ready (recovered via cache refresh).");
276
364
  }
277
365
  if (verifyResult.code === 0 && verifyResult.bypassed) {
@@ -279,6 +367,7 @@ export function runSessionReady(projectRoot, options = {}) {
279
367
  lines.push(message);
280
368
  return {
281
369
  code: 1,
370
+ sessionId,
282
371
  message,
283
372
  path: SESSION_READY_FAILED,
284
373
  lines,
@@ -293,6 +382,7 @@ export function runSessionReady(projectRoot, options = {}) {
293
382
  lines.push(message);
294
383
  return {
295
384
  code: verifyResult.code,
385
+ sessionId,
296
386
  message,
297
387
  path: SESSION_READY_FAILED,
298
388
  lines,
@@ -10,7 +10,7 @@ import type { GitRunner } from "./git.js";
10
10
  import { type ApplyOccupancyInput, type OccupancyDecision } from "./occupancy.js";
11
11
  import { type OrientationBundle, type RunOrientationOptions } from "./orientation-compression.js";
12
12
  import { type ReleaseAvailabilityProbeOptions } from "./release-availability.js";
13
- import { type RitualState, ritualStatePath } from "./ritual-sentinel.js";
13
+ import { type RitualState, ritualStatePath, writeRitualState } from "./ritual-sentinel.js";
14
14
  import { type ToolchainPreflightOptions, type ToolchainPreflightResult } from "./toolchain-preflight.js";
15
15
  export declare const SESSION_POSTURES: readonly ["read-only", "mutation"];
16
16
  export type SessionPosture = (typeof SESSION_POSTURES)[number];
@@ -76,6 +76,10 @@ export interface SessionStartOptions {
76
76
  readonly now?: Date;
77
77
  readonly writeHistory?: boolean;
78
78
  readonly runGit?: GitRunner;
79
+ /** Test seam for deterministic ritual persistence failures. */
80
+ readonly writeRitualState?: typeof writeRitualState;
81
+ /** Explicit lifecycle owner resolved by the host/session bridge (#3611). */
82
+ readonly sessionId?: string;
79
83
  readonly newSessionId?: () => string;
80
84
  /** #3433: steal the worktree occupancy lease. Requires confirm + occupant. */
81
85
  readonly steal?: boolean;
@@ -26,7 +26,7 @@ import { verifyRequiredTools } from "../verify-env/verify-tools.js";
26
26
  import { ceremonyDialEvidenceToDict, collectCeremonyDialConsumerEvidence, formatCeremonyDialEvidenceLine, mergeCeremonyDialInputsWithConsumerEvidence, } from "./ceremony-dial-evidence.js";
27
27
  import { effortBudgetToDict, maybeFormatEffortBudgetLines, resolveProductionHostEffortDescriptor, } from "./effort-budget.js";
28
28
  import { defaultGitRunner, gitHead, gitIsAncestor, worktreePath } from "./git.js";
29
- import { applyWorktreeOccupancy, } from "./occupancy.js";
29
+ import { applyWorktreeOccupancy, resolveOccupancySessionId, } from "./occupancy.js";
30
30
  import { resolveSessionCompact, runOrientationCompression, } from "./orientation-compression.js";
31
31
  import { emitSessionStartProcessCost, formatSessionStartCeremonyCostLine } from "./process-cost.js";
32
32
  import { probeSessionReleaseAvailability, } from "./release-availability.js";
@@ -110,6 +110,13 @@ const STEP_ALIASES = {
110
110
  triage: "triage_welcome",
111
111
  "triage-welcome": "triage_welcome",
112
112
  };
113
+ function ritualPersistenceTransitionError(cause, sessionId) {
114
+ const detail = cause instanceof Error ? cause.message : String(cause);
115
+ return new Error(`Occupancy is now held by session ${sessionId}, but ritual-state persistence failed: ${detail}. ` +
116
+ "Direct writes remain fail-closed until both owners align. Re-run " +
117
+ "`deft session:start --rearm --session-id=<same-session-id>` when re-arm is eligible; " +
118
+ "otherwise run `deft session:start --session-id=<same-session-id>` for a cold ceremony.");
119
+ }
113
120
  /** Format preferred `session:start` recovery command for cold vs re-arm (#2992). */
114
121
  export function formatSessionStartRecoveryCommand(tier = "cold") {
115
122
  if (tier === "rearm") {
@@ -386,6 +393,7 @@ function resolveSessionScmReadiness(options, allowOptionalNetwork) {
386
393
  authState: "unknown",
387
394
  githubAuthMode: "host-gh",
388
395
  runtimeMode: "local-unsandboxed",
396
+ runtimeModeReason: null,
389
397
  injectedTokenPresent: false,
390
398
  depth: allowOptionalNetwork ? "deep" : "shallow",
391
399
  detail: "SCM readiness probe failed unexpectedly; treating SCM-dependent gates as skipped",
@@ -463,11 +471,11 @@ function resolveEffortBudget(options) {
463
471
  };
464
472
  }
465
473
  }
466
- function occupancyInput(options, now, write) {
474
+ function occupancyInput(options, sessionId, now, write) {
467
475
  return {
476
+ sessionId,
468
477
  env: options.env,
469
478
  now,
470
- newSessionId: options.newSessionId,
471
479
  steal: options.steal,
472
480
  confirm: options.confirm,
473
481
  occupant: options.occupant,
@@ -475,9 +483,9 @@ function occupancyInput(options, now, write) {
475
483
  write,
476
484
  };
477
485
  }
478
- function runOccupancy(projectRoot, options, now, write) {
486
+ function runOccupancy(projectRoot, options, sessionId, now, write) {
479
487
  const apply = options.applyOccupancy ?? applyWorktreeOccupancy;
480
- return apply(projectRoot, occupancyInput(options, now, write));
488
+ return apply(projectRoot, occupancyInput(options, sessionId, now, write));
481
489
  }
482
490
  function occupancyDeniedResult(occupancy, environment) {
483
491
  return {
@@ -497,8 +505,8 @@ function occupancyDeniedResult(occupancy, environment) {
497
505
  lines: occupancy.message.split("\n"),
498
506
  };
499
507
  }
500
- function persistOccupancyOrDeny(projectRoot, options, now, environment) {
501
- const occupancy = runOccupancy(projectRoot, options, now, true);
508
+ function persistOccupancyOrDeny(projectRoot, options, sessionId, now, environment) {
509
+ const occupancy = runOccupancy(projectRoot, options, sessionId, now, true);
502
510
  if (occupancy.code !== 0)
503
511
  return occupancyDeniedResult(occupancy, environment);
504
512
  return occupancy;
@@ -553,7 +561,7 @@ function runReadOnlySessionStart(projectRoot, options, instant, environment) {
553
561
  };
554
562
  return { code: 0, payload: resultPayload, lines };
555
563
  }
556
- function runSessionRearm(projectRoot, options, instant, environment) {
564
+ function runSessionRearm(projectRoot, options, sessionId, instant, environment) {
557
565
  const overallStarted = performance.now();
558
566
  const runGit = options.runGit ?? defaultGitRunner;
559
567
  const eligibility = assessRearmEligibility(projectRoot, { runGit });
@@ -581,7 +589,7 @@ function runSessionRearm(projectRoot, options, instant, environment) {
581
589
  lines: [formatEnvironmentContext(environment), message],
582
590
  };
583
591
  }
584
- const plannedOccupancy = runOccupancy(projectRoot, options, instant, options.steal === true);
592
+ const plannedOccupancy = runOccupancy(projectRoot, options, sessionId, instant, false);
585
593
  if (plannedOccupancy.code !== 0) {
586
594
  return occupancyDeniedResult(plannedOccupancy, environment);
587
595
  }
@@ -683,7 +691,7 @@ function runSessionRearm(projectRoot, options, instant, environment) {
683
691
  };
684
692
  const gatedSteps = restampSteps(eligibility.state.gatedSteps, instant);
685
693
  const writeStarted = performance.now();
686
- const persistedOccupancy = persistOccupancyOrDeny(projectRoot, options, instant, environment);
694
+ const persistedOccupancy = persistOccupancyOrDeny(projectRoot, options, sessionId, instant, environment);
687
695
  if ("payload" in persistedOccupancy) {
688
696
  return persistedOccupancy;
689
697
  }
@@ -703,7 +711,7 @@ function runSessionRearm(projectRoot, options, instant, environment) {
703
711
  };
704
712
  let statePath;
705
713
  try {
706
- statePath = writeRitualState(projectRoot, writePayload);
714
+ statePath = (options.writeRitualState ?? writeRitualState)(projectRoot, writePayload);
707
715
  }
708
716
  catch (cause) {
709
717
  // #2994: still record failed attempt when ritual-state write throws.
@@ -722,7 +730,7 @@ function runSessionRearm(projectRoot, options, instant, environment) {
722
730
  { name: "ritual_write", duration_ms: elapsedMs(writeStarted) },
723
731
  ],
724
732
  }, { projectRoot });
725
- throw cause;
733
+ throw ritualPersistenceTransitionError(cause, rearmSessionId);
726
734
  }
727
735
  const stepTimings = [
728
736
  { name: "alignment", duration_ms: 0 },
@@ -878,9 +886,16 @@ export function runSessionStart(projectRoot, options = {}) {
878
886
  if (posture === READ_ONLY_POSTURE) {
879
887
  return runReadOnlySessionStart(projectRoot, options, instant, environment);
880
888
  }
889
+ // #3611: resolve once per mutation invocation. Every occupancy evaluation,
890
+ // persistence write, and ritual-state payload below receives this exact ID.
891
+ const sessionId = resolveOccupancySessionId({
892
+ sessionId: options.sessionId,
893
+ env: options.env,
894
+ newSessionId: options.newSessionId,
895
+ });
881
896
  // #2992: re-arm path refreshes clock/bind without fat cold ceremony.
882
897
  if (ceremonyTier === REARM_CEREMONY_TIER) {
883
- return runSessionRearm(projectRoot, options, instant, environment);
898
+ return runSessionRearm(projectRoot, options, sessionId, instant, environment);
884
899
  }
885
900
  const overallStarted = performance.now();
886
901
  const stepTimings = [];
@@ -937,7 +952,7 @@ export function runSessionStart(projectRoot, options = {}) {
937
952
  lines: [formatEnvironmentContext(environment), payload.message],
938
953
  };
939
954
  }
940
- const plannedOccupancy = runOccupancy(projectRoot, options, instant, options.steal === true);
955
+ const plannedOccupancy = runOccupancy(projectRoot, options, sessionId, instant, false);
941
956
  if (plannedOccupancy.code !== 0) {
942
957
  return occupancyDeniedResult(plannedOccupancy, environment);
943
958
  }
@@ -1314,7 +1329,7 @@ export function runSessionStart(projectRoot, options = {}) {
1314
1329
  lines.push(consentPrompt.trimEnd());
1315
1330
  }
1316
1331
  const writeStarted = performance.now();
1317
- const persistedOccupancy = persistOccupancyOrDeny(projectRoot, options, instant, environment);
1332
+ const persistedOccupancy = persistOccupancyOrDeny(projectRoot, options, sessionId, instant, environment);
1318
1333
  if ("payload" in persistedOccupancy) {
1319
1334
  return persistedOccupancy;
1320
1335
  }
@@ -1348,7 +1363,7 @@ export function runSessionStart(projectRoot, options = {}) {
1348
1363
  };
1349
1364
  let statePath;
1350
1365
  try {
1351
- statePath = writeRitualState(projectRoot, payload);
1366
+ statePath = (options.writeRitualState ?? writeRitualState)(projectRoot, payload);
1352
1367
  }
1353
1368
  catch (cause) {
1354
1369
  // #2994: still record failed attempt when ritual-state write throws.
@@ -1361,7 +1376,7 @@ export function runSessionStart(projectRoot, options = {}) {
1361
1376
  optionalNetwork: allowOptionalNetwork,
1362
1377
  steps: stepTimings,
1363
1378
  }, { projectRoot });
1364
- throw cause;
1379
+ throw ritualPersistenceTransitionError(cause, coldSessionId);
1365
1380
  }
1366
1381
  stepTimings.push({ name: "ritual_write", duration_ms: elapsedMs(writeStarted) });
1367
1382
  const failed = Object.entries(quickSteps)
@@ -9,13 +9,15 @@
9
9
  * remedy without embedding env values.
10
10
  */
11
11
  import { type CheckGateSpec } from "../check/gate-lists.js";
12
+ import { type PackageManager, type PackageManagerResolutionSource } from "../resolution/package-manager.js";
13
+ /** Maintainer toolchain remains pnpm-based. Consumer tools are selected dynamically. */
12
14
  export declare const TOOLCHAIN_PREFLIGHT_TOOLS: readonly ["task", "pnpm", "node", "git"];
13
- export type ToolchainPreflightTool = (typeof TOOLCHAIN_PREFLIGHT_TOOLS)[number];
15
+ export type ToolchainPreflightTool = "task" | PackageManager | "node" | "git";
14
16
  export type ToolchainPreflightStatus = "ok" | "missing" | "degraded";
15
17
  /** How a missing tool affects check dispatch (#3335). */
16
18
  export type ToolchainPreflightImpact = "none" | "degraded";
17
19
  export interface ToolchainPreflightFinding {
18
- readonly tool: ToolchainPreflightTool | "cli_dist";
20
+ readonly tool: ToolchainPreflightTool | "package_manager" | "cli_dist";
19
21
  readonly present: boolean;
20
22
  /** Machine-stable cause code (never embeds env values). */
21
23
  readonly cause: string | null;
@@ -40,6 +42,10 @@ export interface ToolchainPreflightResult {
40
42
  * Product-ordering of remaining gates is owned by #3284.
41
43
  */
42
44
  readonly skipGateIds: readonly string[];
45
+ /** Closed manager selected for this project, or null on strict selection failure. */
46
+ readonly packageManager?: PackageManager | null;
47
+ /** Selection provenance; maintainer source is deliberately fixed to pnpm. */
48
+ readonly packageManagerSource?: PackageManagerResolutionSource | "maintainer" | null;
43
49
  }
44
50
  export interface ToolchainPreflightOptions {
45
51
  readonly projectRoot?: string;
@@ -61,17 +67,21 @@ export interface ToolchainPreflightOptions {
61
67
  * frameworkRoot vs projectRoot (#3335 / #3324).
62
68
  */
63
69
  readonly consumerDeposit?: boolean;
70
+ /** Environment map used for consumer package-manager precedence. */
71
+ readonly env?: NodeJS.ProcessEnv;
64
72
  }
65
73
  /**
66
74
  * Gates that specifically need a package manager / built dist.
67
75
  * When task/node is missing, the orchestrator skips *all* scheduled gates
68
76
  * (dynamic from gate-lists), not this static list — avoids hardcode drift.
69
77
  */
78
+ export declare const PACKAGE_MANAGER_DEPENDENT_GATE_IDS: readonly string[];
79
+ /** @deprecated use PACKAGE_MANAGER_DEPENDENT_GATE_IDS. */
70
80
  export declare const PNPM_DEPENDENT_GATE_IDS: readonly string[];
71
81
  /** Sentinel skip id meaning "every gate in the active check composition". */
72
82
  export declare const SKIP_ALL_GATES = "*";
73
83
  /**
74
- * Run done-gate toolchain preflight. Pure probe — never installs or builds.
84
+ * Run done-gate toolchain preflight. Read-only probe — never installs or builds.
75
85
  */
76
86
  export declare function runToolchainPreflight(options?: ToolchainPreflightOptions): ToolchainPreflightResult;
77
87
  /** Serialize preflight for run-summary / ritual-state (no env values). */