@deftai/directive-core 0.106.0 → 0.108.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 (249) hide show
  1. package/dist/architecture/sor-preflight.js +2 -0
  2. package/dist/check/cached-orchestrator.js +28 -3
  3. package/dist/check/gate-lists.js +2 -0
  4. package/dist/check/named-cause.js +18 -3
  5. package/dist/codebase/provider.js +7 -1
  6. package/dist/delivery-attempt/handoff.js +1 -1
  7. package/dist/design-critique/citation-grammar.d.ts +53 -0
  8. package/dist/design-critique/citation-grammar.js +303 -0
  9. package/dist/design-critique/completed-arc-record.d.ts +62 -0
  10. package/dist/design-critique/completed-arc-record.js +232 -0
  11. package/dist/design-critique/exclusive-chip.d.ts +33 -0
  12. package/dist/design-critique/exclusive-chip.js +68 -0
  13. package/dist/design-critique/parent-audit.d.ts +59 -0
  14. package/dist/design-critique/parent-audit.js +121 -0
  15. package/dist/doctor/checks.d.ts +6 -0
  16. package/dist/doctor/checks.js +35 -0
  17. package/dist/freshness/bind.d.ts +32 -3
  18. package/dist/freshness/bind.js +58 -12
  19. package/dist/hooks/classify/host-session-identity.d.ts +76 -0
  20. package/dist/hooks/classify/host-session-identity.js +470 -0
  21. package/dist/hooks/classify/index.d.ts +3 -2
  22. package/dist/hooks/classify/index.js +3 -2
  23. package/dist/hooks/classify/paths.d.ts +7 -0
  24. package/dist/hooks/classify/paths.js +35 -0
  25. package/dist/hooks/classify/stdin.d.ts +5 -0
  26. package/dist/hooks/classify/stdin.js +27 -0
  27. package/dist/hooks/dest-form.d.ts +46 -0
  28. package/dist/hooks/dest-form.js +575 -0
  29. package/dist/hooks/dispatcher.d.ts +69 -2
  30. package/dist/hooks/dispatcher.js +609 -45
  31. package/dist/hooks/fixtures/cases.d.ts +16 -2
  32. package/dist/hooks/fixtures/cases.js +175 -0
  33. package/dist/hooks/index.d.ts +1 -0
  34. package/dist/hooks/index.js +1 -0
  35. package/dist/hooks/scope.js +3 -1
  36. package/dist/init-deposit/agent-hooks.d.ts +2 -1
  37. package/dist/init-deposit/agent-hooks.js +8 -1
  38. package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
  39. package/dist/init-deposit/hook-runtime-travel.js +269 -0
  40. package/dist/intake/clause-derivation.js +9 -3
  41. package/dist/intake/github-auth-modes-cli.js +14 -2
  42. package/dist/intake/github-auth-modes.d.ts +54 -9
  43. package/dist/intake/github-auth-modes.js +326 -123
  44. package/dist/intake/issue-ingest.js +62 -2
  45. package/dist/intake/platform-capabilities.d.ts +9 -2
  46. package/dist/intake/platform-capabilities.js +38 -9
  47. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +26 -0
  48. package/dist/lifecycle/completed-tracked-on-delivery.js +37 -14
  49. package/dist/lifecycle/completed-write-guard.d.ts +48 -0
  50. package/dist/lifecycle/completed-write-guard.js +373 -0
  51. package/dist/lifecycle/index.d.ts +1 -0
  52. package/dist/lifecycle/index.js +1 -0
  53. package/dist/literal-acceptance/capture.d.ts +16 -0
  54. package/dist/literal-acceptance/capture.js +75 -7
  55. package/dist/literal-acceptance/evaluate.js +25 -3
  56. package/dist/literal-acceptance/index.d.ts +2 -2
  57. package/dist/literal-acceptance/index.js +2 -2
  58. package/dist/literal-acceptance/run.js +9 -3
  59. package/dist/literal-acceptance/types.d.ts +6 -0
  60. package/dist/literal-acceptance/types.js +6 -0
  61. package/dist/orphan-active/evaluate.d.ts +25 -0
  62. package/dist/orphan-active/evaluate.js +193 -128
  63. package/dist/orphan-active/index.d.ts +1 -0
  64. package/dist/orphan-active/index.js +1 -0
  65. package/dist/orphan-active/issue-state.d.ts +121 -0
  66. package/dist/orphan-active/issue-state.js +278 -0
  67. package/dist/platform/cursor-managed-runtime.d.ts +95 -0
  68. package/dist/platform/cursor-managed-runtime.js +241 -0
  69. package/dist/platform/index.d.ts +1 -0
  70. package/dist/platform/index.js +1 -0
  71. package/dist/platform/platform-capabilities.d.ts +23 -1
  72. package/dist/platform/platform-capabilities.js +55 -15
  73. package/dist/policy/ceremony-dial.js +5 -9
  74. package/dist/policy/host-hooks.js +4 -9
  75. package/dist/policy/org-force-on-migration.js +5 -10
  76. package/dist/policy/plan-extensions.d.ts +14 -2
  77. package/dist/policy/plan-extensions.js +24 -3
  78. package/dist/policy/product-signal.js +5 -10
  79. package/dist/policy/require-human-merge.js +5 -9
  80. package/dist/policy/resolve.js +77 -16
  81. package/dist/policy/runtime-authority.d.ts +18 -0
  82. package/dist/policy/runtime-authority.js +11 -0
  83. package/dist/policy/value-feedback.js +9 -18
  84. package/dist/policy/write-fence.js +3 -0
  85. package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
  86. package/dist/pr-closeout-attestable/evaluate.js +306 -0
  87. package/dist/pr-closeout-attestable/index.d.ts +2 -0
  88. package/dist/pr-closeout-attestable/index.js +2 -0
  89. package/dist/pr-protected-issues/gh.d.ts +6 -2
  90. package/dist/pr-protected-issues/gh.js +7 -3
  91. package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
  92. package/dist/pr-wait-mergeable/cascade.js +21 -1
  93. package/dist/pr-wait-mergeable/types.d.ts +2 -0
  94. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  95. package/dist/pr-wait-mergeable/wrappers.js +11 -0
  96. package/dist/preflight/evaluate.d.ts +15 -0
  97. package/dist/preflight/evaluate.js +29 -3
  98. package/dist/preflight-cache/evaluate.js +9 -3
  99. package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
  100. package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
  101. package/dist/product-first-done-gate/evaluate.js +53 -12
  102. package/dist/release/cli-drift-report.d.ts +68 -0
  103. package/dist/release/cli-drift-report.js +189 -0
  104. package/dist/release/index.d.ts +1 -0
  105. package/dist/release/index.js +1 -0
  106. package/dist/release/issue-state-fetch.d.ts +5 -3
  107. package/dist/release/issue-state-fetch.js +86 -21
  108. package/dist/release/native-steps.js +1 -0
  109. package/dist/release/pipeline.js +19 -0
  110. package/dist/release/types.d.ts +14 -0
  111. package/dist/render/constants.d.ts +4 -0
  112. package/dist/render/constants.js +11 -8
  113. package/dist/render/export-spec.js +31 -5
  114. package/dist/render/index.d.ts +1 -1
  115. package/dist/render/index.js +1 -1
  116. package/dist/render/prd-render.d.ts +4 -1
  117. package/dist/render/prd-render.js +82 -23
  118. package/dist/render/project-render.js +14 -7
  119. package/dist/render/scope-outlook.d.ts +2 -0
  120. package/dist/render/scope-outlook.js +3 -0
  121. package/dist/render/spec-render.js +2 -2
  122. package/dist/resolution/package-manager.d.ts +33 -4
  123. package/dist/resolution/package-manager.js +210 -9
  124. package/dist/review-monitor/constants.d.ts +7 -0
  125. package/dist/review-monitor/constants.js +7 -0
  126. package/dist/review-monitor/github-lease.d.ts +98 -1
  127. package/dist/review-monitor/github-lease.js +182 -1
  128. package/dist/review-monitor/lease-comment.d.ts +36 -0
  129. package/dist/review-monitor/lease-comment.js +103 -2
  130. package/dist/scm/call.js +12 -3
  131. package/dist/scm/design-critique-chip.d.ts +38 -0
  132. package/dist/scm/design-critique-chip.js +187 -0
  133. package/dist/scm/gh-rest.d.ts +9 -1
  134. package/dist/scm/gh-rest.js +103 -0
  135. package/dist/scm/index.d.ts +1 -0
  136. package/dist/scm/index.js +1 -0
  137. package/dist/scm/main.d.ts +3 -0
  138. package/dist/scm/main.js +15 -1
  139. package/dist/scm/readiness-cli.d.ts +2 -0
  140. package/dist/scm/readiness-cli.js +42 -1
  141. package/dist/scm/readiness.d.ts +4 -1
  142. package/dist/scm/readiness.js +35 -5
  143. package/dist/scope/decompose.js +2 -1
  144. package/dist/scope/delivery-evidence.d.ts +27 -1
  145. package/dist/scope/index.d.ts +1 -0
  146. package/dist/scope/index.js +1 -0
  147. package/dist/scope/lifecycle-write.d.ts +24 -0
  148. package/dist/scope/lifecycle-write.js +64 -0
  149. package/dist/scope/main.js +3 -1
  150. package/dist/scope/project-definition-sync.js +14 -7
  151. package/dist/scope/transition.js +90 -11
  152. package/dist/scope-provenance/evaluate.js +5 -1
  153. package/dist/session/git.d.ts +42 -0
  154. package/dist/session/git.js +219 -3
  155. package/dist/session/occupancy.d.ts +239 -12
  156. package/dist/session/occupancy.js +820 -27
  157. package/dist/session/ritual-sentinel.d.ts +5 -0
  158. package/dist/session/ritual-sentinel.js +5 -0
  159. package/dist/session/session-ready.d.ts +5 -1
  160. package/dist/session/session-ready.js +98 -8
  161. package/dist/session/session-start.d.ts +17 -1
  162. package/dist/session/session-start.js +49 -17
  163. package/dist/session/toolchain-preflight.d.ts +13 -3
  164. package/dist/session/toolchain-preflight.js +93 -18
  165. package/dist/session/verify-session-ritual.d.ts +33 -3
  166. package/dist/session/verify-session-ritual.js +89 -19
  167. package/dist/slice/constants.d.ts +1 -1
  168. package/dist/spec-authority/constants.d.ts +10 -2
  169. package/dist/spec-authority/constants.js +53 -8
  170. package/dist/spec-authority/resolver.d.ts +3 -0
  171. package/dist/spec-authority/resolver.js +55 -7
  172. package/dist/subprocess/max-buffer.d.ts +14 -0
  173. package/dist/subprocess/max-buffer.js +15 -0
  174. package/dist/swarm/complete-cohort.d.ts +2 -0
  175. package/dist/swarm/complete-cohort.js +21 -13
  176. package/dist/swarm/index.d.ts +1 -0
  177. package/dist/swarm/index.js +1 -0
  178. package/dist/swarm/launch-cli.js +53 -33
  179. package/dist/swarm/launch.d.ts +77 -0
  180. package/dist/swarm/launch.js +248 -38
  181. package/dist/swarm/pre-dispatch-cli.js +2 -1
  182. package/dist/swarm/pre-dispatch.js +10 -1
  183. package/dist/swarm/subagent-status-dir.d.ts +27 -0
  184. package/dist/swarm/subagent-status-dir.js +42 -0
  185. package/dist/swarm/worktrees.d.ts +17 -3
  186. package/dist/swarm/worktrees.js +48 -4
  187. package/dist/triage/actions/index.d.ts +4 -0
  188. package/dist/triage/actions/index.js +6 -0
  189. package/dist/triage/evaluate/evaluate.d.ts +11 -0
  190. package/dist/triage/evaluate/evaluate.js +169 -0
  191. package/dist/triage/evaluate/github.d.ts +16 -0
  192. package/dist/triage/evaluate/github.js +153 -0
  193. package/dist/triage/evaluate/index.d.ts +9 -0
  194. package/dist/triage/evaluate/index.js +8 -0
  195. package/dist/triage/evaluate/paths.d.ts +7 -0
  196. package/dist/triage/evaluate/paths.js +25 -0
  197. package/dist/triage/evaluate/sink.d.ts +5 -0
  198. package/dist/triage/evaluate/sink.js +52 -0
  199. package/dist/triage/evaluate/types.d.ts +93 -0
  200. package/dist/triage/evaluate/types.js +13 -0
  201. package/dist/triage/evaluate/validity.d.ts +8 -0
  202. package/dist/triage/evaluate/validity.js +87 -0
  203. package/dist/triage/evaluate/value.d.ts +12 -0
  204. package/dist/triage/evaluate/value.js +33 -0
  205. package/dist/triage/evaluate/wip-census.d.ts +5 -0
  206. package/dist/triage/evaluate/wip-census.js +39 -0
  207. package/dist/triage/evaluate/worktrees.d.ts +8 -0
  208. package/dist/triage/evaluate/worktrees.js +68 -0
  209. package/dist/triage/evaluate/xbrief-refs.d.ts +8 -0
  210. package/dist/triage/evaluate/xbrief-refs.js +60 -0
  211. package/dist/triage/help/registry-data.d.ts +21 -7
  212. package/dist/triage/help/registry-data.js +53 -5
  213. package/dist/triage/index.d.ts +1 -0
  214. package/dist/triage/index.js +1 -0
  215. package/dist/triage/scope/mutations-core.d.ts +2 -6
  216. package/dist/triage/scope/mutations-core.js +23 -55
  217. package/dist/triage/scope-drift/add-ignore.js +5 -39
  218. package/dist/triage/subscribe/index.d.ts +2 -6
  219. package/dist/triage/subscribe/index.js +15 -65
  220. package/dist/triage/welcome/writers.js +15 -74
  221. package/dist/value/feedback-file.d.ts +17 -2
  222. package/dist/value/feedback-file.js +80 -6
  223. package/dist/vbrief-build/index.d.ts +2 -1
  224. package/dist/vbrief-build/index.js +2 -1
  225. package/dist/vbrief-build/parity-scenarios.js +5 -5
  226. package/dist/vbrief-build/project-definition-io.d.ts +63 -3
  227. package/dist/vbrief-build/project-definition-io.js +357 -31
  228. package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
  229. package/dist/vbrief-build/project-definition-mutation.js +43 -0
  230. package/dist/vbrief-reconcile/labels.d.ts +1 -0
  231. package/dist/vbrief-reconcile/labels.js +30 -0
  232. package/dist/vbrief-validate/conformance.d.ts +1 -0
  233. package/dist/vbrief-validate/conformance.js +85 -8
  234. package/dist/vbrief-validate/main.js +2 -0
  235. package/dist/vbrief-validate/precutover.js +5 -10
  236. package/dist/vbrief-validation/story-quality.d.ts +7 -0
  237. package/dist/vbrief-validation/story-quality.js +8 -1
  238. package/dist/verify-ac/clauses.d.ts +62 -2
  239. package/dist/verify-ac/clauses.js +183 -111
  240. package/dist/verify-ac/index.d.ts +1 -1
  241. package/dist/verify-ac/index.js +1 -1
  242. package/dist/verify-env/node-runtime.d.ts +8 -4
  243. package/dist/verify-env/node-runtime.js +9 -6
  244. package/dist/verify-env/toolchain-check.d.ts +35 -3
  245. package/dist/verify-env/toolchain-check.js +155 -36
  246. package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
  247. package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
  248. package/dist/xbrief-migrate/agents-header.js +69 -7
  249. 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];
@@ -24,8 +24,20 @@ export type SessionCeremonyTier = (typeof SESSION_CEREMONY_TIERS)[number];
24
24
  export declare const COLD_CEREMONY_TIER: SessionCeremonyTier;
25
25
  export declare const REARM_CEREMONY_TIER: SessionCeremonyTier;
26
26
  export declare const QUICK_STEPS: readonly ["alignment", "branch_policy", "triage_welcome", "verify_tools"];
27
+ /** Session / full gated ritual set. Write dispatch uses a per-surface subset (#3738). */
27
28
  export declare const GATED_STEPS: readonly ["agent_hooks", "doctor", "cache_fresh"];
28
29
  export type GatedStepName = (typeof GATED_STEPS)[number];
30
+ /**
31
+ * Gated steps a write/spawn mutation must prove from recorded ritual state (#3738).
32
+ * `cache_fresh` is a work-selection precondition, not a write-authorization one.
33
+ */
34
+ export declare const WRITE_GATED_REQUIRED_STEPS: readonly ["agent_hooks", "doctor"];
35
+ /**
36
+ * Gated steps the write path may execute (#3738).
37
+ * Hook readiness is deliberately non-cacheable; doctor and cache_fresh stay
38
+ * session-surface only.
39
+ */
40
+ export declare const WRITE_GATED_EXECUTE_STEPS: readonly ["agent_hooks"];
29
41
  /** Env opt-in for optional session:start network (release probe + triage cache hydrate) (#2991). */
30
42
  export declare const ENV_SESSION_START_NETWORK = "DEFT_SESSION_START_NETWORK";
31
43
  /** Human-readable skip notice when optional network is off the hot path (#2991). */
@@ -64,6 +76,10 @@ export interface SessionStartOptions {
64
76
  readonly now?: Date;
65
77
  readonly writeHistory?: boolean;
66
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;
67
83
  readonly newSessionId?: () => string;
68
84
  /** #3433: steal the worktree occupancy lease. Requires confirm + occupant. */
69
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";
@@ -50,7 +50,24 @@ export const QUICK_STEPS = [
50
50
  "triage_welcome",
51
51
  "verify_tools",
52
52
  ];
53
+ /** Session / full gated ritual set. Write dispatch uses a per-surface subset (#3738). */
53
54
  export const GATED_STEPS = ["agent_hooks", "doctor", "cache_fresh"];
55
+ /**
56
+ * Gated steps a write/spawn mutation must prove from recorded ritual state (#3738).
57
+ * `cache_fresh` is a work-selection precondition, not a write-authorization one.
58
+ */
59
+ export const WRITE_GATED_REQUIRED_STEPS = [
60
+ "agent_hooks",
61
+ "doctor",
62
+ ];
63
+ /**
64
+ * Gated steps the write path may execute (#3738).
65
+ * Hook readiness is deliberately non-cacheable; doctor and cache_fresh stay
66
+ * session-surface only.
67
+ */
68
+ export const WRITE_GATED_EXECUTE_STEPS = [
69
+ "agent_hooks",
70
+ ];
54
71
  /** Per-clone ignore/index hide of lifecycle roots — warn-only (#3505). */
55
72
  function pushLifecycleVisibleAdvisory(lines, projectRoot, options, runGit) {
56
73
  try {
@@ -93,6 +110,13 @@ const STEP_ALIASES = {
93
110
  triage: "triage_welcome",
94
111
  "triage-welcome": "triage_welcome",
95
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
+ }
96
120
  /** Format preferred `session:start` recovery command for cold vs re-arm (#2992). */
97
121
  export function formatSessionStartRecoveryCommand(tier = "cold") {
98
122
  if (tier === "rearm") {
@@ -369,6 +393,7 @@ function resolveSessionScmReadiness(options, allowOptionalNetwork) {
369
393
  authState: "unknown",
370
394
  githubAuthMode: "host-gh",
371
395
  runtimeMode: "local-unsandboxed",
396
+ runtimeModeReason: null,
372
397
  injectedTokenPresent: false,
373
398
  depth: allowOptionalNetwork ? "deep" : "shallow",
374
399
  detail: "SCM readiness probe failed unexpectedly; treating SCM-dependent gates as skipped",
@@ -446,11 +471,11 @@ function resolveEffortBudget(options) {
446
471
  };
447
472
  }
448
473
  }
449
- function occupancyInput(options, now, write) {
474
+ function occupancyInput(options, sessionId, now, write) {
450
475
  return {
476
+ sessionId,
451
477
  env: options.env,
452
478
  now,
453
- newSessionId: options.newSessionId,
454
479
  steal: options.steal,
455
480
  confirm: options.confirm,
456
481
  occupant: options.occupant,
@@ -458,9 +483,9 @@ function occupancyInput(options, now, write) {
458
483
  write,
459
484
  };
460
485
  }
461
- function runOccupancy(projectRoot, options, now, write) {
486
+ function runOccupancy(projectRoot, options, sessionId, now, write) {
462
487
  const apply = options.applyOccupancy ?? applyWorktreeOccupancy;
463
- return apply(projectRoot, occupancyInput(options, now, write));
488
+ return apply(projectRoot, occupancyInput(options, sessionId, now, write));
464
489
  }
465
490
  function occupancyDeniedResult(occupancy, environment) {
466
491
  return {
@@ -480,8 +505,8 @@ function occupancyDeniedResult(occupancy, environment) {
480
505
  lines: occupancy.message.split("\n"),
481
506
  };
482
507
  }
483
- function persistOccupancyOrDeny(projectRoot, options, now, environment) {
484
- const occupancy = runOccupancy(projectRoot, options, now, true);
508
+ function persistOccupancyOrDeny(projectRoot, options, sessionId, now, environment) {
509
+ const occupancy = runOccupancy(projectRoot, options, sessionId, now, true);
485
510
  if (occupancy.code !== 0)
486
511
  return occupancyDeniedResult(occupancy, environment);
487
512
  return occupancy;
@@ -536,7 +561,7 @@ function runReadOnlySessionStart(projectRoot, options, instant, environment) {
536
561
  };
537
562
  return { code: 0, payload: resultPayload, lines };
538
563
  }
539
- function runSessionRearm(projectRoot, options, instant, environment) {
564
+ function runSessionRearm(projectRoot, options, sessionId, instant, environment) {
540
565
  const overallStarted = performance.now();
541
566
  const runGit = options.runGit ?? defaultGitRunner;
542
567
  const eligibility = assessRearmEligibility(projectRoot, { runGit });
@@ -564,7 +589,7 @@ function runSessionRearm(projectRoot, options, instant, environment) {
564
589
  lines: [formatEnvironmentContext(environment), message],
565
590
  };
566
591
  }
567
- const plannedOccupancy = runOccupancy(projectRoot, options, instant, options.steal === true);
592
+ const plannedOccupancy = runOccupancy(projectRoot, options, sessionId, instant, false);
568
593
  if (plannedOccupancy.code !== 0) {
569
594
  return occupancyDeniedResult(plannedOccupancy, environment);
570
595
  }
@@ -666,7 +691,7 @@ function runSessionRearm(projectRoot, options, instant, environment) {
666
691
  };
667
692
  const gatedSteps = restampSteps(eligibility.state.gatedSteps, instant);
668
693
  const writeStarted = performance.now();
669
- const persistedOccupancy = persistOccupancyOrDeny(projectRoot, options, instant, environment);
694
+ const persistedOccupancy = persistOccupancyOrDeny(projectRoot, options, sessionId, instant, environment);
670
695
  if ("payload" in persistedOccupancy) {
671
696
  return persistedOccupancy;
672
697
  }
@@ -686,7 +711,7 @@ function runSessionRearm(projectRoot, options, instant, environment) {
686
711
  };
687
712
  let statePath;
688
713
  try {
689
- statePath = writeRitualState(projectRoot, writePayload);
714
+ statePath = (options.writeRitualState ?? writeRitualState)(projectRoot, writePayload);
690
715
  }
691
716
  catch (cause) {
692
717
  // #2994: still record failed attempt when ritual-state write throws.
@@ -705,7 +730,7 @@ function runSessionRearm(projectRoot, options, instant, environment) {
705
730
  { name: "ritual_write", duration_ms: elapsedMs(writeStarted) },
706
731
  ],
707
732
  }, { projectRoot });
708
- throw cause;
733
+ throw ritualPersistenceTransitionError(cause, rearmSessionId);
709
734
  }
710
735
  const stepTimings = [
711
736
  { name: "alignment", duration_ms: 0 },
@@ -861,9 +886,16 @@ export function runSessionStart(projectRoot, options = {}) {
861
886
  if (posture === READ_ONLY_POSTURE) {
862
887
  return runReadOnlySessionStart(projectRoot, options, instant, environment);
863
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
+ });
864
896
  // #2992: re-arm path refreshes clock/bind without fat cold ceremony.
865
897
  if (ceremonyTier === REARM_CEREMONY_TIER) {
866
- return runSessionRearm(projectRoot, options, instant, environment);
898
+ return runSessionRearm(projectRoot, options, sessionId, instant, environment);
867
899
  }
868
900
  const overallStarted = performance.now();
869
901
  const stepTimings = [];
@@ -920,7 +952,7 @@ export function runSessionStart(projectRoot, options = {}) {
920
952
  lines: [formatEnvironmentContext(environment), payload.message],
921
953
  };
922
954
  }
923
- const plannedOccupancy = runOccupancy(projectRoot, options, instant, options.steal === true);
955
+ const plannedOccupancy = runOccupancy(projectRoot, options, sessionId, instant, false);
924
956
  if (plannedOccupancy.code !== 0) {
925
957
  return occupancyDeniedResult(plannedOccupancy, environment);
926
958
  }
@@ -1297,7 +1329,7 @@ export function runSessionStart(projectRoot, options = {}) {
1297
1329
  lines.push(consentPrompt.trimEnd());
1298
1330
  }
1299
1331
  const writeStarted = performance.now();
1300
- const persistedOccupancy = persistOccupancyOrDeny(projectRoot, options, instant, environment);
1332
+ const persistedOccupancy = persistOccupancyOrDeny(projectRoot, options, sessionId, instant, environment);
1301
1333
  if ("payload" in persistedOccupancy) {
1302
1334
  return persistedOccupancy;
1303
1335
  }
@@ -1331,7 +1363,7 @@ export function runSessionStart(projectRoot, options = {}) {
1331
1363
  };
1332
1364
  let statePath;
1333
1365
  try {
1334
- statePath = writeRitualState(projectRoot, payload);
1366
+ statePath = (options.writeRitualState ?? writeRitualState)(projectRoot, payload);
1335
1367
  }
1336
1368
  catch (cause) {
1337
1369
  // #2994: still record failed attempt when ritual-state write throws.
@@ -1344,7 +1376,7 @@ export function runSessionStart(projectRoot, options = {}) {
1344
1376
  optionalNetwork: allowOptionalNetwork,
1345
1377
  steps: stepTimings,
1346
1378
  }, { projectRoot });
1347
- throw cause;
1379
+ throw ritualPersistenceTransitionError(cause, coldSessionId);
1348
1380
  }
1349
1381
  stepTimings.push({ name: "ritual_write", duration_ms: elapsedMs(writeStarted) });
1350
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). */