@deftai/directive-core 0.107.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 (167) hide show
  1. package/dist/architecture/sor-preflight.js +2 -0
  2. package/dist/check/cached-orchestrator.js +28 -3
  3. package/dist/check/named-cause.js +18 -3
  4. package/dist/codebase/provider.js +7 -1
  5. package/dist/delivery-attempt/handoff.js +1 -1
  6. package/dist/design-critique/citation-grammar.d.ts +53 -0
  7. package/dist/design-critique/citation-grammar.js +303 -0
  8. package/dist/design-critique/completed-arc-record.d.ts +62 -0
  9. package/dist/design-critique/completed-arc-record.js +232 -0
  10. package/dist/freshness/bind.d.ts +32 -3
  11. package/dist/freshness/bind.js +58 -12
  12. package/dist/hooks/classify/host-session-identity.d.ts +76 -0
  13. package/dist/hooks/classify/host-session-identity.js +470 -0
  14. package/dist/hooks/classify/index.d.ts +3 -2
  15. package/dist/hooks/classify/index.js +3 -2
  16. package/dist/hooks/classify/paths.d.ts +7 -0
  17. package/dist/hooks/classify/paths.js +35 -0
  18. package/dist/hooks/classify/stdin.d.ts +5 -0
  19. package/dist/hooks/classify/stdin.js +27 -0
  20. package/dist/hooks/dispatcher.d.ts +52 -1
  21. package/dist/hooks/dispatcher.js +538 -45
  22. package/dist/hooks/fixtures/cases.d.ts +16 -2
  23. package/dist/hooks/fixtures/cases.js +175 -0
  24. package/dist/hooks/scope.js +3 -1
  25. package/dist/init-deposit/agent-hooks.d.ts +2 -1
  26. package/dist/init-deposit/agent-hooks.js +8 -1
  27. package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
  28. package/dist/init-deposit/hook-runtime-travel.js +269 -0
  29. package/dist/intake/clause-derivation.js +9 -3
  30. package/dist/intake/issue-ingest.js +62 -2
  31. package/dist/intake/platform-capabilities.d.ts +9 -2
  32. package/dist/intake/platform-capabilities.js +38 -9
  33. package/dist/orphan-active/evaluate.d.ts +25 -0
  34. package/dist/orphan-active/evaluate.js +193 -128
  35. package/dist/orphan-active/index.d.ts +1 -0
  36. package/dist/orphan-active/index.js +1 -0
  37. package/dist/orphan-active/issue-state.d.ts +121 -0
  38. package/dist/orphan-active/issue-state.js +278 -0
  39. package/dist/platform/cursor-managed-runtime.d.ts +95 -0
  40. package/dist/platform/cursor-managed-runtime.js +241 -0
  41. package/dist/platform/index.d.ts +1 -0
  42. package/dist/platform/index.js +1 -0
  43. package/dist/platform/platform-capabilities.d.ts +23 -1
  44. package/dist/platform/platform-capabilities.js +55 -15
  45. package/dist/policy/ceremony-dial.js +5 -9
  46. package/dist/policy/host-hooks.js +4 -9
  47. package/dist/policy/org-force-on-migration.js +5 -10
  48. package/dist/policy/plan-extensions.d.ts +14 -2
  49. package/dist/policy/plan-extensions.js +24 -3
  50. package/dist/policy/product-signal.js +5 -10
  51. package/dist/policy/require-human-merge.js +5 -9
  52. package/dist/policy/resolve.js +77 -16
  53. package/dist/policy/value-feedback.js +9 -18
  54. package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
  55. package/dist/pr-closeout-attestable/evaluate.js +306 -0
  56. package/dist/pr-closeout-attestable/index.d.ts +2 -0
  57. package/dist/pr-closeout-attestable/index.js +2 -0
  58. package/dist/pr-protected-issues/gh.d.ts +6 -2
  59. package/dist/pr-protected-issues/gh.js +7 -3
  60. package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
  61. package/dist/pr-wait-mergeable/cascade.js +21 -1
  62. package/dist/pr-wait-mergeable/types.d.ts +2 -0
  63. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  64. package/dist/pr-wait-mergeable/wrappers.js +11 -0
  65. package/dist/preflight/evaluate.d.ts +15 -0
  66. package/dist/preflight/evaluate.js +29 -3
  67. package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
  68. package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
  69. package/dist/product-first-done-gate/evaluate.js +17 -7
  70. package/dist/release/cli-drift-report.d.ts +68 -0
  71. package/dist/release/cli-drift-report.js +189 -0
  72. package/dist/release/index.d.ts +1 -0
  73. package/dist/release/index.js +1 -0
  74. package/dist/release/issue-state-fetch.d.ts +5 -3
  75. package/dist/release/issue-state-fetch.js +86 -21
  76. package/dist/release/native-steps.js +1 -0
  77. package/dist/release/pipeline.js +19 -0
  78. package/dist/release/types.d.ts +14 -0
  79. package/dist/render/constants.d.ts +4 -0
  80. package/dist/render/constants.js +11 -8
  81. package/dist/render/export-spec.js +31 -5
  82. package/dist/render/index.d.ts +1 -1
  83. package/dist/render/index.js +1 -1
  84. package/dist/render/prd-render.d.ts +4 -1
  85. package/dist/render/prd-render.js +82 -23
  86. package/dist/render/project-render.js +14 -7
  87. package/dist/render/scope-outlook.d.ts +2 -0
  88. package/dist/render/scope-outlook.js +3 -0
  89. package/dist/render/spec-render.js +2 -2
  90. package/dist/resolution/package-manager.d.ts +33 -4
  91. package/dist/resolution/package-manager.js +210 -9
  92. package/dist/review-monitor/constants.d.ts +7 -0
  93. package/dist/review-monitor/constants.js +7 -0
  94. package/dist/review-monitor/github-lease.d.ts +98 -1
  95. package/dist/review-monitor/github-lease.js +182 -1
  96. package/dist/review-monitor/lease-comment.d.ts +36 -0
  97. package/dist/review-monitor/lease-comment.js +103 -2
  98. package/dist/scm/call.js +12 -3
  99. package/dist/scm/design-critique-chip.d.ts +1 -0
  100. package/dist/scm/design-critique-chip.js +22 -6
  101. package/dist/scm/gh-rest.d.ts +9 -1
  102. package/dist/scm/gh-rest.js +103 -0
  103. package/dist/scm/readiness.d.ts +2 -0
  104. package/dist/scm/readiness.js +28 -4
  105. package/dist/scope/project-definition-sync.js +14 -7
  106. package/dist/session/git.d.ts +24 -0
  107. package/dist/session/git.js +138 -2
  108. package/dist/session/occupancy.d.ts +238 -12
  109. package/dist/session/occupancy.js +815 -29
  110. package/dist/session/ritual-sentinel.d.ts +5 -0
  111. package/dist/session/ritual-sentinel.js +5 -0
  112. package/dist/session/session-ready.d.ts +5 -1
  113. package/dist/session/session-ready.js +98 -8
  114. package/dist/session/session-start.d.ts +5 -1
  115. package/dist/session/session-start.js +32 -17
  116. package/dist/session/toolchain-preflight.d.ts +13 -3
  117. package/dist/session/toolchain-preflight.js +93 -18
  118. package/dist/session/verify-session-ritual.d.ts +11 -0
  119. package/dist/session/verify-session-ritual.js +60 -15
  120. package/dist/slice/constants.d.ts +1 -1
  121. package/dist/spec-authority/constants.d.ts +10 -2
  122. package/dist/spec-authority/constants.js +53 -8
  123. package/dist/spec-authority/resolver.d.ts +3 -0
  124. package/dist/spec-authority/resolver.js +55 -7
  125. package/dist/subprocess/max-buffer.d.ts +14 -0
  126. package/dist/subprocess/max-buffer.js +15 -0
  127. package/dist/swarm/complete-cohort.d.ts +2 -0
  128. package/dist/swarm/complete-cohort.js +21 -13
  129. package/dist/swarm/index.d.ts +1 -0
  130. package/dist/swarm/index.js +1 -0
  131. package/dist/swarm/launch-cli.js +53 -33
  132. package/dist/swarm/launch.d.ts +8 -0
  133. package/dist/swarm/launch.js +63 -43
  134. package/dist/swarm/pre-dispatch-cli.js +2 -1
  135. package/dist/swarm/pre-dispatch.js +10 -1
  136. package/dist/swarm/subagent-status-dir.d.ts +27 -0
  137. package/dist/swarm/subagent-status-dir.js +42 -0
  138. package/dist/swarm/worktrees.d.ts +17 -3
  139. package/dist/swarm/worktrees.js +48 -4
  140. package/dist/triage/scope/mutations-core.d.ts +2 -6
  141. package/dist/triage/scope/mutations-core.js +23 -55
  142. package/dist/triage/scope-drift/add-ignore.js +5 -39
  143. package/dist/triage/subscribe/index.d.ts +2 -6
  144. package/dist/triage/subscribe/index.js +15 -65
  145. package/dist/triage/welcome/writers.js +15 -74
  146. package/dist/vbrief-build/index.d.ts +2 -1
  147. package/dist/vbrief-build/index.js +2 -1
  148. package/dist/vbrief-build/parity-scenarios.js +5 -5
  149. package/dist/vbrief-build/project-definition-io.d.ts +63 -3
  150. package/dist/vbrief-build/project-definition-io.js +357 -31
  151. package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
  152. package/dist/vbrief-build/project-definition-mutation.js +43 -0
  153. package/dist/vbrief-validate/conformance.d.ts +1 -0
  154. package/dist/vbrief-validate/conformance.js +85 -8
  155. package/dist/vbrief-validate/main.js +2 -0
  156. package/dist/vbrief-validate/precutover.js +5 -10
  157. package/dist/verify-ac/clauses.d.ts +62 -2
  158. package/dist/verify-ac/clauses.js +183 -111
  159. package/dist/verify-ac/index.d.ts +1 -1
  160. package/dist/verify-ac/index.js +1 -1
  161. package/dist/verify-env/node-runtime.d.ts +8 -4
  162. package/dist/verify-env/node-runtime.js +9 -6
  163. package/dist/verify-env/toolchain-check.d.ts +35 -3
  164. package/dist/verify-env/toolchain-check.js +155 -36
  165. package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
  166. package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
  167. package/package.json +7 -3
@@ -9,20 +9,24 @@
9
9
  * remedy without embedding env values.
10
10
  */
11
11
  import { existsSync } from "node:fs";
12
- import { join } from "node:path";
12
+ import { join, resolve } from "node:path";
13
13
  import { allGatesCliDispatchable, GLOBAL_CLI_REMEDY, isCliNativeGate, } from "../check/cli-native-gates.js";
14
14
  import { isFrameworkRepoRoot, isFrameworkSourceContext } from "../check/context.js";
15
15
  import { CONSUMER_CHECK_GATES, checkGateId, FRAMEWORK_CHECK_GATES, } from "../check/gate-lists.js";
16
16
  import { defaultWhich } from "../doctor/which.js";
17
+ import { packageManagerSourceLabel, resolveProjectPackageManager, } from "../resolution/package-manager.js";
18
+ /** Maintainer toolchain remains pnpm-based. Consumer tools are selected dynamically. */
17
19
  export const TOOLCHAIN_PREFLIGHT_TOOLS = ["task", "pnpm", "node", "git"];
18
20
  const REMEDY = {
19
21
  task: "Install go-task: https://taskfile.dev/installation/ (e.g. winget install Task.Task / brew install go-task)",
22
+ npm: "Install or repair Node 20+ (npm is bundled), then re-run the consumer check",
20
23
  pnpm: "Enable pnpm via corepack: corepack enable && corepack prepare pnpm@latest --activate",
21
- node: "Install Node 20+ (see .nvmrc); then corepack enable",
24
+ node: "Install Node 20+ (see .nvmrc)",
22
25
  git: "Install Git: https://git-scm.com/downloads",
23
26
  };
24
27
  const CAUSE = {
25
28
  task: "go-task binary not found on PATH",
29
+ npm: "npm binary not found on PATH",
26
30
  pnpm: "pnpm binary not found on PATH",
27
31
  node: "node binary not found on PATH",
28
32
  git: "git binary not found on PATH",
@@ -32,20 +36,29 @@ const CAUSE = {
32
36
  * When task/node is missing, the orchestrator skips *all* scheduled gates
33
37
  * (dynamic from gate-lists), not this static list — avoids hardcode drift.
34
38
  */
35
- export const PNPM_DEPENDENT_GATE_IDS = [
39
+ export const PACKAGE_MANAGER_DEPENDENT_GATE_IDS = [
36
40
  "toolchain:check",
37
41
  "toolchain:check-consumer",
38
42
  "ts:check-lane",
39
43
  ];
44
+ /** @deprecated use PACKAGE_MANAGER_DEPENDENT_GATE_IDS. */
45
+ export const PNPM_DEPENDENT_GATE_IDS = PACKAGE_MANAGER_DEPENDENT_GATE_IDS;
40
46
  /** Sentinel skip id meaning "every gate in the active check composition". */
41
47
  export const SKIP_ALL_GATES = "*";
42
- function probeTool(tool, which) {
48
+ function probeTool(tool, which, packageManager) {
43
49
  const present = which(tool) !== null;
50
+ let remedy = REMEDY[tool];
51
+ if (tool === "node" && packageManager === "npm") {
52
+ remedy = "Install or repair Node 20+ (npm is bundled), then re-run the consumer check";
53
+ }
54
+ else if (tool === "node" && packageManager === "pnpm") {
55
+ remedy = "Install Node 20+ (see .nvmrc); then enable pnpm with Corepack";
56
+ }
44
57
  return {
45
58
  tool,
46
59
  present,
47
60
  cause: present ? null : CAUSE[tool],
48
- remedy: present ? null : REMEDY[tool],
61
+ remedy: present ? null : remedy,
49
62
  };
50
63
  }
51
64
  function probeCliDist(frameworkRoot, which, exists) {
@@ -78,7 +91,8 @@ function formatFindingLine(finding) {
78
91
  }
79
92
  if (finding.impact === "none") {
80
93
  const cause = finding.cause ?? "absent";
81
- return `[deft preflight] ${finding.tool}: absent (impact: none) ${cause}`;
94
+ const remedy = finding.remedy === null ? "" : `; remedy: ${finding.remedy}`;
95
+ return `[deft preflight] ${finding.tool}: absent (impact: none) — ${cause}${remedy}`;
82
96
  }
83
97
  const cause = finding.cause ?? "missing";
84
98
  const remedy = finding.remedy ?? "install the tool";
@@ -90,14 +104,17 @@ function inferConsumerDeposit(options) {
90
104
  const project = options.projectRoot;
91
105
  if (project === undefined)
92
106
  return false;
93
- // Same-path roots are framework-source only when the tree is the source repo.
94
- if (isFrameworkRepoRoot(project))
95
- return false;
96
107
  const framework = options.frameworkRoot ?? project;
108
+ // session:start passes equal roots for both framework source and a consumer
109
+ // deposit. Equal roots identify maintainer mode only when the tree has the
110
+ // framework-source markers; otherwise this is the production consumer shape.
111
+ if (resolve(framework) === resolve(project)) {
112
+ return !isFrameworkRepoRoot(project);
113
+ }
97
114
  return !isFrameworkSourceContext(framework, project);
98
115
  }
99
116
  /**
100
- * Run done-gate toolchain preflight. Pure probe — never installs or builds.
117
+ * Run done-gate toolchain preflight. Read-only probe — never installs or builds.
101
118
  */
102
119
  export function runToolchainPreflight(options = {}) {
103
120
  const which = options.which ?? defaultWhich;
@@ -106,16 +123,44 @@ export function runToolchainPreflight(options = {}) {
106
123
  const composed = options.composedGates ?? (consumerDeposit ? CONSUMER_CHECK_GATES : FRAMEWORK_CHECK_GATES);
107
124
  const allCli = allGatesCliDispatchable(composed);
108
125
  const findings = [];
109
- for (const tool of TOOLCHAIN_PREFLIGHT_TOOLS) {
110
- findings.push(probeTool(tool, which));
126
+ let packageManager = "pnpm";
127
+ let packageManagerSource = "maintainer";
128
+ let packageManagerSelectionFailed = false;
129
+ let selectedTools = TOOLCHAIN_PREFLIGHT_TOOLS;
130
+ if (consumerDeposit) {
131
+ const resolution = resolveProjectPackageManager({
132
+ projectRoot: options.projectRoot,
133
+ env: options.env,
134
+ });
135
+ if (resolution.ok) {
136
+ packageManager = resolution.packageManager;
137
+ packageManagerSource = resolution.source;
138
+ selectedTools = ["task", packageManager, "node", "git"];
139
+ }
140
+ else {
141
+ packageManager = null;
142
+ packageManagerSource = null;
143
+ packageManagerSelectionFailed = true;
144
+ selectedTools = ["task", "node", "git"];
145
+ findings.push({
146
+ tool: "package_manager",
147
+ present: false,
148
+ cause: resolution.message,
149
+ remedy: "Set package.json#packageManager (or DEFT_PACKAGE_MANAGER) to a supported npm or pnpm value",
150
+ });
151
+ }
152
+ }
153
+ for (const tool of selectedTools) {
154
+ findings.push(probeTool(tool, which, packageManager));
111
155
  }
112
156
  if (options.probeCliDist !== false) {
113
157
  findings.push(probeCliDist(options.frameworkRoot ?? options.projectRoot, which, exists));
114
158
  }
115
159
  const cliPresent = findings.some((f) => f.tool === "cli_dist" && f.present);
116
160
  const taskMissing = findings.some((f) => f.tool === "task" && !f.present);
117
- const pnpmMissing = findings.some((f) => f.tool === "pnpm" && !f.present);
118
161
  const nodeMissing = findings.some((f) => f.tool === "node" && !f.present);
162
+ const packageManagerMissing = packageManagerSelectionFailed ||
163
+ (packageManager !== null && findings.some((f) => f.tool === packageManager && !f.present));
119
164
  // #3335: in a deposit whose composed gates are all CLI-dispatchable, a
120
165
  // missing task binary is impact none — no install-go-task remedy.
121
166
  if (taskMissing && allCli) {
@@ -132,10 +177,30 @@ export function runToolchainPreflight(options = {}) {
132
177
  };
133
178
  }
134
179
  }
180
+ // Keep finding impact aligned with the skip calculation. Git is advisory in
181
+ // this preflight, and a local CLI dist matters only when task is absent and
182
+ // every composed gate must dispatch through the CLI.
183
+ for (let index = 0; index < findings.length; index += 1) {
184
+ const finding = findings[index];
185
+ if (finding === undefined || finding.present)
186
+ continue;
187
+ if (finding.tool === "git") {
188
+ findings[index] = { ...finding, impact: "none" };
189
+ }
190
+ else if (finding.tool === "cli_dist") {
191
+ findings[index] = {
192
+ ...finding,
193
+ impact: taskMissing && allCli && !cliPresent ? "degraded" : "none",
194
+ };
195
+ }
196
+ }
135
197
  const missingCritical = findings.filter((f) => {
136
198
  if (f.present || f.impact === "none")
137
199
  return false;
138
- return f.tool === "task" || f.tool === "pnpm" || f.tool === "node";
200
+ return (f.tool === "task" ||
201
+ f.tool === "node" ||
202
+ f.tool === "package_manager" ||
203
+ f.tool === packageManager);
139
204
  });
140
205
  const skip = new Set();
141
206
  if (nodeMissing) {
@@ -156,8 +221,8 @@ export function runToolchainPreflight(options = {}) {
156
221
  else if (taskMissing && allCli && !cliPresent) {
157
222
  skip.add(SKIP_ALL_GATES);
158
223
  }
159
- else if (pnpmMissing) {
160
- for (const id of PNPM_DEPENDENT_GATE_IDS) {
224
+ else if (packageManagerMissing) {
225
+ for (const id of PACKAGE_MANAGER_DEPENDENT_GATE_IDS) {
161
226
  skip.add(id);
162
227
  }
163
228
  }
@@ -166,6 +231,12 @@ export function runToolchainPreflight(options = {}) {
166
231
  const status = ok ? "ok" : "degraded";
167
232
  const lines = [];
168
233
  lines.push(`[deft preflight] toolchain status: ${status}`);
234
+ if (consumerDeposit &&
235
+ packageManager !== null &&
236
+ packageManagerSource !== null &&
237
+ packageManagerSource !== "maintainer") {
238
+ lines.push(`[deft preflight] package manager: ${packageManager} (${packageManagerSourceLabel(packageManagerSource)})`);
239
+ }
169
240
  for (const finding of findings) {
170
241
  if (!finding.present) {
171
242
  lines.push(formatFindingLine(finding));
@@ -173,8 +244,8 @@ export function runToolchainPreflight(options = {}) {
173
244
  }
174
245
  if (ok) {
175
246
  lines.push(taskMissing && allCli
176
- ? "[deft preflight] done-gate toolchain ready (CLI dispatch; go-task not required) (#3335)"
177
- : "[deft preflight] done-gate toolchain ready (task, pnpm, node)");
247
+ ? `[deft preflight] done-gate toolchain ready (CLI dispatch; go-task not required; package manager: ${packageManager}) (#3335)`
248
+ : `[deft preflight] done-gate toolchain ready (task, ${packageManager}, node)`);
178
249
  }
179
250
  else {
180
251
  lines.push("[deft preflight] degraded mode: directive check will skip toolchain-dependent gates " +
@@ -190,6 +261,8 @@ export function runToolchainPreflight(options = {}) {
190
261
  findings,
191
262
  lines,
192
263
  skipGateIds: [...skip].sort(),
264
+ packageManager,
265
+ packageManagerSource,
193
266
  };
194
267
  }
195
268
  /** Serialize preflight for run-summary / ritual-state (no env values). */
@@ -198,6 +271,8 @@ export function toolchainPreflightToDict(result) {
198
271
  status: result.status,
199
272
  ok: result.ok,
200
273
  degraded: result.degraded,
274
+ package_manager: result.packageManager ?? null,
275
+ package_manager_source: result.packageManagerSource ?? null,
201
276
  findings: result.findings.map((f) => ({
202
277
  tool: f.tool,
203
278
  present: f.present,
@@ -41,6 +41,11 @@ export interface VerifyResult {
41
41
  * `rearm` when age/compact stale on an otherwise valid bind; `cold` otherwise.
42
42
  */
43
43
  readonly recoveryTier?: SessionCeremonyTier | null;
44
+ /**
45
+ * Internal owner from the exact ritual state evaluated for this verdict.
46
+ * Absent until worktree/HEAD binding succeeds; never projected by emitVerifyJson.
47
+ */
48
+ readonly boundSessionId?: string;
44
49
  }
45
50
  export type RitualRunner = (command: readonly string[], projectRoot: string) => {
46
51
  code: number;
@@ -107,6 +112,12 @@ export interface InspectSessionRitualOptions {
107
112
  readonly posture?: DirectivePosture;
108
113
  readonly envPosture?: string | undefined;
109
114
  readonly handoffText?: string | null;
115
+ /**
116
+ * Required gated steps for this surface. Defaults to all {@link GATED_STEPS}.
117
+ * Write dispatch passes {@link WRITE_GATED_REQUIRED_STEPS} so a no-write
118
+ * inspect reports the same readiness its verifying counterpart would (#3738).
119
+ */
120
+ readonly requiredGatedSteps?: readonly GatedStepName[];
110
121
  }
111
122
  /**
112
123
  * Read-only ritual-state inspection for host hooks.
@@ -136,7 +136,37 @@ function shouldRearmDriftProbe(payload, projectRoot, detect) {
136
136
  return false;
137
137
  return detect(projectRoot).inPlay;
138
138
  }
139
- function runGatedStep(projectRoot, payload, stepName, runner, now, detect = detectWorkSelection) {
139
+ /**
140
+ * Persist a payload derived from `expected` only while the record on disk is
141
+ * still the one this verification read (#3769).
142
+ *
143
+ * A lease transfer mid-verification can re-arm the tree under a new owner. The
144
+ * losing session is denied by the occupancy recheck, but without this
145
+ * compare-and-swap its in-flight write would already have replaced the new
146
+ * owner record. Serializing ritual writes outright is the separate
147
+ * ritual-state locking item, deliberately out of scope.
148
+ *
149
+ * Returns null on success, or the reason the write was refused.
150
+ */
151
+ function writeRitualStateIfStillOwned(projectRoot, payload, expected) {
152
+ const [current] = readRitualState(projectRoot);
153
+ if (current === null) {
154
+ return "session ritual state was removed while this verification was running";
155
+ }
156
+ if (current.sessionId !== expected.sessionId ||
157
+ current.startedAt.getTime() !== expected.startedAt.getTime()) {
158
+ return (`session ritual state was re-armed by ${current.sessionId ?? "<unbound>"} while this ` +
159
+ "verification was running; refusing to overwrite the current owner record");
160
+ }
161
+ try {
162
+ writeRitualState(projectRoot, payload);
163
+ }
164
+ catch (exc) {
165
+ return String(exc);
166
+ }
167
+ return null;
168
+ }
169
+ function runGatedStep(projectRoot, payload, stepName, runner, now, expected, detect = detectWorkSelection) {
140
170
  const command = resolveGatedEntrypointCommand(stepName, projectRoot, detect);
141
171
  const { code, stdout, stderr } = runner(command, projectRoot);
142
172
  const message = stdout.trim() || stderr.trim() || `${command[0]} exited ${code}`;
@@ -152,11 +182,9 @@ function runGatedStep(projectRoot, payload, stepName, runner, now, detect = dete
152
182
  if (stepName === "cache_fresh") {
153
183
  applyCacheFreshDriftProbeField(payload, command);
154
184
  }
155
- try {
156
- writeRitualState(projectRoot, payload);
157
- }
158
- catch (exc) {
159
- return `could not write session ritual state after ${stepName}: ${String(exc)}`;
185
+ const refusal = writeRitualStateIfStillOwned(projectRoot, payload, expected);
186
+ if (refusal !== null) {
187
+ return `could not write session ritual state after ${stepName}: ${refusal}`;
160
188
  }
161
189
  return null;
162
190
  }
@@ -199,14 +227,11 @@ function evaluateLoadedState(projectRoot, state, input) {
199
227
  return { code: 1, message: headDriftRecoveryMessage(), recoveryTier: "cold" };
200
228
  }
201
229
  if (input.rebindForwardHead) {
202
- const payload = { ...state.raw, git_head: currentHead };
203
- try {
204
- writeRitualState(projectRoot, payload);
205
- }
206
- catch (exc) {
230
+ const refusal = writeRitualStateIfStillOwned(projectRoot, { ...state.raw, git_head: currentHead }, { sessionId: state.sessionId, startedAt: state.startedAt });
231
+ if (refusal !== null) {
207
232
  return {
208
233
  code: 2,
209
- message: `could not rebind session ritual git HEAD: ${String(exc)}`,
234
+ message: `could not rebind session ritual git HEAD: ${refusal}`,
210
235
  recoveryTier: "cold",
211
236
  };
212
237
  }
@@ -218,6 +243,7 @@ function evaluateLoadedState(projectRoot, state, input) {
218
243
  code: 2,
219
244
  message: staleness.error ?? "session ritual staleness policy is invalid",
220
245
  recoveryTier: "cold",
246
+ boundSessionId: state.sessionId,
221
247
  };
222
248
  }
223
249
  const maxAgeMs = staleness.hours * 60 * 60 * 1000;
@@ -232,6 +258,7 @@ function evaluateLoadedState(projectRoot, state, input) {
232
258
  message: `session ritual state is stale (older than ${staleness.hours}h).${compactNote} ` +
233
259
  `Run \`${rearmCmd}\` to re-arm (or \`${coldCmd}\` for a full cold ceremony).`,
234
260
  recoveryTier: "rearm",
261
+ boundSessionId: state.sessionId,
235
262
  };
236
263
  }
237
264
  for (const stepName of QUICK_STEPS) {
@@ -241,6 +268,7 @@ function evaluateLoadedState(projectRoot, state, input) {
241
268
  code: 1,
242
269
  message: failedStepMessage("quick", stepName, step),
243
270
  recoveryTier: "cold",
271
+ boundSessionId: state.sessionId,
244
272
  };
245
273
  }
246
274
  }
@@ -249,10 +277,14 @@ function evaluateLoadedState(projectRoot, state, input) {
249
277
  for (const stepName of requiredGated) {
250
278
  const step = state.gatedSteps[stepName];
251
279
  if (!stepPasses(step)) {
280
+ const recoveryTier = "cold";
252
281
  return {
253
282
  code: 1,
254
- message: failedStepMessage("gated", stepName, step),
255
- recoveryTier: "cold",
283
+ message: failedStepMessage("gated", stepName, step) +
284
+ " " +
285
+ formatRitualRecoveryInstruction(recoveryTier),
286
+ recoveryTier,
287
+ boundSessionId: state.sessionId,
256
288
  };
257
289
  }
258
290
  }
@@ -261,6 +293,7 @@ function evaluateLoadedState(projectRoot, state, input) {
261
293
  code: 0,
262
294
  message: `OK session ritual ${input.tier} tier is fresh.`,
263
295
  recoveryTier: null,
296
+ boundSessionId: state.sessionId,
264
297
  };
265
298
  }
266
299
  /**
@@ -335,6 +368,7 @@ export function inspectSessionRitual(projectRoot, options = {}) {
335
368
  now: options.now ?? new Date(),
336
369
  runGit: options.runGit,
337
370
  rebindForwardHead: false,
371
+ requiredGatedSteps: options.requiredGatedSteps,
338
372
  });
339
373
  return {
340
374
  code: evaluated.code,
@@ -346,6 +380,7 @@ export function inspectSessionRitual(projectRoot, options = {}) {
346
380
  posture,
347
381
  ritualStateRequired,
348
382
  recoveryTier: evaluated.recoveryTier,
383
+ ...(evaluated.boundSessionId === undefined ? {} : { boundSessionId: evaluated.boundSessionId }),
349
384
  };
350
385
  }
351
386
  export function verifySessionRitual(projectRoot, options = {}) {
@@ -440,6 +475,9 @@ export function verifySessionRitual(projectRoot, options = {}) {
440
475
  posture,
441
476
  ritualStateRequired,
442
477
  recoveryTier: precheck.recoveryTier,
478
+ ...(precheck.boundSessionId === undefined
479
+ ? {}
480
+ : { boundSessionId: precheck.boundSessionId }),
443
481
  };
444
482
  }
445
483
  const reloadedAfterPrecheck = readRitualState(projectRoot);
@@ -458,6 +496,9 @@ export function verifySessionRitual(projectRoot, options = {}) {
458
496
  };
459
497
  }
460
498
  const payload = { ...state.raw };
499
+ // Identity of the record this verification is amending; a mid-flight lease
500
+ // transfer must not let these writes land on a newer owner (#3769).
501
+ const ownerAtVerify = { sessionId: state.sessionId, startedAt: state.startedAt };
461
502
  const gated = { ...payload.gated_steps };
462
503
  payload.gated_steps = gated;
463
504
  const runCmd = options.runner ?? defaultRitualRunner;
@@ -477,7 +518,7 @@ export function verifySessionRitual(projectRoot, options = {}) {
477
518
  if (stepName !== "agent_hooks" && stepPasses(step) && !forced.has(stepName) && !rearmDrift) {
478
519
  continue;
479
520
  }
480
- const writeError = runGatedStep(projectRoot, payload, stepName, runCmd, instant, detect);
521
+ const writeError = runGatedStep(projectRoot, payload, stepName, runCmd, instant, ownerAtVerify, detect);
481
522
  if (writeError !== null) {
482
523
  return {
483
524
  code: 2,
@@ -556,6 +597,9 @@ export function verifySessionRitual(projectRoot, options = {}) {
556
597
  posture,
557
598
  ritualStateRequired,
558
599
  recoveryTier: evaluated.recoveryTier,
600
+ ...(evaluated.boundSessionId === undefined
601
+ ? {}
602
+ : { boundSessionId: evaluated.boundSessionId }),
559
603
  };
560
604
  }
561
605
  return {
@@ -568,6 +612,7 @@ export function verifySessionRitual(projectRoot, options = {}) {
568
612
  posture,
569
613
  ritualStateRequired,
570
614
  recoveryTier: evaluated.recoveryTier,
615
+ ...(evaluated.boundSessionId === undefined ? {} : { boundSessionId: evaluated.boundSessionId }),
571
616
  };
572
617
  }
573
618
  export function emitVerifyJson(result) {
@@ -10,7 +10,7 @@ export declare const REQUIRED_FIELDS: readonly ["slice_id", "umbrella", "umbrell
10
10
  export declare const OPTIONAL_FIELDS: readonly ["notes"];
11
11
  export declare const ALLOWED_FIELDS: Set<"actor" | "slice_id" | "umbrella" | "umbrella_url" | "sliced_at" | "children" | "expected_close_signal" | "notes">;
12
12
  export declare const CHILD_REQUIRED_FIELDS: readonly ["n", "url", "wave", "role"];
13
- export declare const CHILD_ALLOWED_FIELDS: Set<"n" | "url" | "wave" | "role">;
13
+ export declare const CHILD_ALLOWED_FIELDS: Set<"n" | "role" | "url" | "wave">;
14
14
  export declare const UUID_RE: RegExp;
15
15
  export declare const ISO8601_RE: RegExp;
16
16
  export declare const WAVE_FLAG_RE: RegExp;
@@ -4,10 +4,18 @@ export declare const GENERATED_SPEC_SOURCE_SPEC = "<!-- Source of truth: vbrief/
4
4
  export declare const GENERATED_SPEC_SOURCE_SPEC_XBRIEF = "<!-- Source of truth: xbrief/specification.xbrief.json -->";
5
5
  export declare const GENERATED_SPEC_SOURCE_PD = "<!-- Source of truth: vbrief/PROJECT-DEFINITION.vbrief.json -->";
6
6
  export declare const GENERATED_SPEC_SOURCE_PD_XBRIEF = "<!-- Source of truth: xbrief/PROJECT-DEFINITION.xbrief.json -->";
7
- /** True when markdown carries a generated-spec source marker for either layout. */
8
- export declare function contentHasGeneratedSpecSource(content: string): boolean;
7
+ /** Return decoded non-PROJECT-DEFINITION source markers from generated markdown. */
8
+ export declare function generatedSpecSourcePaths(content: string): string[];
9
+ /** True when markdown names the expected generated-spec source artifact. */
10
+ export declare function contentHasGeneratedSpecSource(content: string, expectedSourcePath: string): boolean;
9
11
  /** True when markdown carries a generated greenfield PD source marker for either layout. */
10
12
  export declare function contentHasGeneratedPdSource(content: string): boolean;
13
+ /** Format a source path for a generated artifact banner. */
14
+ export declare function generatedSourcePath(sourcePath: string): string;
15
+ /** Build a canonical four-line generated-artifact banner from the actual source path. */
16
+ export declare function buildGeneratedArtifactBanner(generator: string, purpose: string, sourcePath: string): string;
17
+ /** Build the greenfield project export banner for the resolved PROJECT-DEFINITION path. */
18
+ export declare function buildExportSpecPdBanner(projectDefPath: string): string;
11
19
  export declare const EXPORT_SPEC_PD_BANNER: string;
12
20
  /** Narratives excluded from stakeholder SPEC/PRD exports (#2013 Wave 0 §5). */
13
21
  export declare const STAKEHOLDER_EXCLUDED_NARRATIVE_KEYS: Set<string>;
@@ -1,22 +1,67 @@
1
+ import { basename, dirname, normalize, sep } from "node:path";
1
2
  /** Generated-spec banner tokens (lockstep with Python ``_precutover.py``). */
2
3
  export const GENERATED_SPEC_PURPOSE = "<!-- Purpose: rendered specification -->";
3
4
  export const GENERATED_SPEC_SOURCE_SPEC = "<!-- Source of truth: vbrief/specification.vbrief.json -->";
4
5
  export const GENERATED_SPEC_SOURCE_SPEC_XBRIEF = "<!-- Source of truth: xbrief/specification.xbrief.json -->";
5
6
  export const GENERATED_SPEC_SOURCE_PD = "<!-- Source of truth: vbrief/PROJECT-DEFINITION.vbrief.json -->";
6
7
  export const GENERATED_SPEC_SOURCE_PD_XBRIEF = "<!-- Source of truth: xbrief/PROJECT-DEFINITION.xbrief.json -->";
7
- /** True when markdown carries a generated-spec source marker for either layout. */
8
- export function contentHasGeneratedSpecSource(content) {
9
- return (content.includes(GENERATED_SPEC_SOURCE_SPEC) ||
10
- content.includes(GENERATED_SPEC_SOURCE_SPEC_XBRIEF));
8
+ const GENERATED_SOURCE_MARKER = /^<!-- Source of truth: (.+?) -->\r?$/gm;
9
+ const PROJECT_DEFINITION_SOURCE = /(?:^|\/)PROJECT-DEFINITION\.(?:vbrief|xbrief)\.json$/i;
10
+ /** Return decoded non-PROJECT-DEFINITION source markers from generated markdown. */
11
+ export function generatedSpecSourcePaths(content) {
12
+ const sources = [];
13
+ for (const match of content.matchAll(GENERATED_SOURCE_MARKER)) {
14
+ let source;
15
+ try {
16
+ source = match[1] ? decodeURI(match[1]) : undefined;
17
+ }
18
+ catch {
19
+ // A generated source path is URI-encoded by the banner writer. Reject
20
+ // malformed escapes instead of guessing at a filesystem path.
21
+ }
22
+ if (source && !PROJECT_DEFINITION_SOURCE.test(source))
23
+ sources.push(source);
24
+ }
25
+ return sources;
26
+ }
27
+ /** True when markdown names the expected generated-spec source artifact. */
28
+ export function contentHasGeneratedSpecSource(content, expectedSourcePath) {
29
+ const expected = generatedSourcePath(expectedSourcePath);
30
+ return generatedSpecSourcePaths(content).includes(expected);
11
31
  }
12
32
  /** True when markdown carries a generated greenfield PD source marker for either layout. */
13
33
  export function contentHasGeneratedPdSource(content) {
14
34
  return (content.includes(GENERATED_SPEC_SOURCE_PD) || content.includes(GENERATED_SPEC_SOURCE_PD_XBRIEF));
15
35
  }
16
- export const EXPORT_SPEC_PD_BANNER = "<!-- AUTO-GENERATED by task project:export-spec -- DO NOT EDIT MANUALLY -->\n" +
17
- `${GENERATED_SPEC_PURPOSE}\n` +
18
- `${GENERATED_SPEC_SOURCE_PD}\n` +
19
- "<!-- Regenerate with: task project:export-spec -->\n";
36
+ /** Format a source path for a generated artifact banner. */
37
+ export function generatedSourcePath(sourcePath) {
38
+ const parent = basename(dirname(sourcePath));
39
+ if (parent === "xbrief" || parent === "vbrief") {
40
+ return `${parent}/${basename(sourcePath)}`;
41
+ }
42
+ return normalize(sourcePath).split(sep).join("/");
43
+ }
44
+ function generatedBannerValue(value) {
45
+ return value.replace(/\r\n?|\n/g, " ").replaceAll("-->", "--&gt;");
46
+ }
47
+ /** Build a canonical four-line generated-artifact banner from the actual source path. */
48
+ export function buildGeneratedArtifactBanner(generator, purpose, sourcePath) {
49
+ const safeGenerator = generatedBannerValue(generator);
50
+ const safePurpose = generatedBannerValue(purpose);
51
+ // Source paths must be reversible so an explicit --spec artifact can be
52
+ // bound back to the resolved authority. URI encoding keeps CR/LF and an
53
+ // embedded comment closer on one safe line without losing path identity.
54
+ const safeSourcePath = encodeURI(generatedSourcePath(sourcePath));
55
+ return (`<!-- AUTO-GENERATED by ${safeGenerator} -- DO NOT EDIT MANUALLY -->\n` +
56
+ `<!-- Purpose: ${safePurpose} -->\n` +
57
+ `<!-- Source of truth: ${safeSourcePath} -->\n` +
58
+ `<!-- Regenerate with: ${safeGenerator} -->\n`);
59
+ }
60
+ /** Build the greenfield project export banner for the resolved PROJECT-DEFINITION path. */
61
+ export function buildExportSpecPdBanner(projectDefPath) {
62
+ return buildGeneratedArtifactBanner("task project:export-spec", "rendered specification", projectDefPath);
63
+ }
64
+ export const EXPORT_SPEC_PD_BANNER = buildExportSpecPdBanner("xbrief/PROJECT-DEFINITION.xbrief.json");
20
65
  /** Narratives excluded from stakeholder SPEC/PRD exports (#2013 Wave 0 §5). */
21
66
  export const STAKEHOLDER_EXCLUDED_NARRATIVE_KEYS = new Set([
22
67
  "projectconfig",
@@ -5,10 +5,13 @@ export interface ResolvedSpecAuthority {
5
5
  readonly vbriefDir: string;
6
6
  readonly projectDefPath: string;
7
7
  readonly specPath: string | null;
8
+ readonly sourcePath: string;
8
9
  readonly banner: string;
9
10
  }
10
11
  export declare function resolveSpecAuthority(projectRoot: string): ResolvedSpecAuthority | null;
11
12
  export declare function readSpecMarkdown(projectRoot: string): string;
13
+ /** True when the markdown source marker resolves to the project's specification authority. */
14
+ export declare function contentHasResolvedSpecSource(projectRoot: string, content: string): boolean;
12
15
  export declare function isFullSpecState(projectRoot: string): boolean;
13
16
  export declare function isGreenfieldSpecExport(projectRoot: string): boolean;
14
17
  export declare function isCurrentGeneratedSpecification(projectRoot: string): boolean;
@@ -1,8 +1,8 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { resolveLifecycleLayout, resolveLifecycleRoot, resolveProjectDefinitionPath, } from "../layout/resolve.js";
4
- import { SPEC_RENDER_BANNER } from "../render/constants.js";
5
- import { contentHasGeneratedPdSource, contentHasGeneratedSpecSource, EXPORT_SPEC_PD_BANNER, GENERATED_SPEC_PURPOSE, } from "./constants.js";
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
+ import { isDeepStrictEqual } from "node:util";
4
+ import { resolveLifecycleLayout, resolveLifecycleRoot, resolveProjectDefinitionPath, resolveSpecArtifactPath, } from "../layout/resolve.js";
5
+ import { buildExportSpecPdBanner, buildGeneratedArtifactBanner, contentHasGeneratedPdSource, contentHasGeneratedSpecSource, GENERATED_SPEC_PURPOSE, generatedSpecSourcePaths, } from "./constants.js";
6
6
  export function resolveSpecAuthority(projectRoot) {
7
7
  const root = projectRoot;
8
8
  let layout;
@@ -20,13 +20,17 @@ export function resolveSpecAuthority(projectRoot) {
20
20
  const specPath = join(vbriefDir, `specification${layout.artifactSuffix}`);
21
21
  const hasSpec = existsSync(specPath);
22
22
  const kind = hasSpec ? "full-spec" : "greenfield";
23
- const banner = kind === "full-spec" ? SPEC_RENDER_BANNER : EXPORT_SPEC_PD_BANNER;
23
+ const sourcePath = hasSpec ? specPath : projectDefPath;
24
+ const banner = hasSpec
25
+ ? buildGeneratedArtifactBanner("task spec:render", "rendered specification", sourcePath)
26
+ : buildExportSpecPdBanner(sourcePath);
24
27
  return {
25
28
  kind,
26
29
  projectRoot: root,
27
30
  vbriefDir,
28
31
  projectDefPath,
29
32
  specPath: hasSpec ? specPath : null,
33
+ sourcePath,
30
34
  banner,
31
35
  };
32
36
  }
@@ -39,12 +43,56 @@ export function readSpecMarkdown(projectRoot) {
39
43
  return "";
40
44
  }
41
45
  }
46
+ function sourcePathFromMarker(projectRoot, source) {
47
+ return isAbsolute(source) ? source : resolve(projectRoot, source);
48
+ }
49
+ function contentMatchesResolvedSpecSource(projectRoot, content, resolvedSourcePath) {
50
+ if (contentHasGeneratedSpecSource(content, resolvedSourcePath))
51
+ return true;
52
+ let resolvedSource;
53
+ try {
54
+ resolvedSource = JSON.parse(readFileSync(resolvedSourcePath, "utf8"));
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ for (const source of generatedSpecSourcePaths(content)) {
60
+ try {
61
+ const candidate = sourcePathFromMarker(projectRoot, source);
62
+ if (!statSync(candidate).isFile())
63
+ continue;
64
+ // Explicit --spec paths remain valid aliases only when they identify the
65
+ // same JSON authority as the layout-resolved specification artifact.
66
+ if (isDeepStrictEqual(JSON.parse(readFileSync(candidate, "utf8")), resolvedSource)) {
67
+ return true;
68
+ }
69
+ }
70
+ catch {
71
+ // Missing or unreadable explicit source markers are not resolved authority.
72
+ }
73
+ }
74
+ return false;
75
+ }
76
+ /** True when the markdown source marker resolves to the project's specification authority. */
77
+ export function contentHasResolvedSpecSource(projectRoot, content) {
78
+ let resolvedSourcePath;
79
+ try {
80
+ resolvedSourcePath = resolveSpecArtifactPath(projectRoot);
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ if (!existsSync(resolvedSourcePath))
86
+ return false;
87
+ return contentMatchesResolvedSpecSource(projectRoot, content, resolvedSourcePath);
88
+ }
42
89
  export function isFullSpecState(projectRoot) {
43
90
  const authority = resolveSpecAuthority(projectRoot);
44
91
  if (authority?.kind !== "full-spec")
45
92
  return false;
46
93
  const specMd = readSpecMarkdown(projectRoot);
47
- return specMd.includes(GENERATED_SPEC_PURPOSE) && contentHasGeneratedSpecSource(specMd);
94
+ return (specMd.includes(GENERATED_SPEC_PURPOSE) &&
95
+ contentMatchesResolvedSpecSource(projectRoot, specMd, authority.sourcePath));
48
96
  }
49
97
  export function isGreenfieldSpecExport(projectRoot) {
50
98
  const authority = resolveSpecAuthority(projectRoot);
@@ -15,4 +15,18 @@
15
15
  * so an overflow is never silent again.
16
16
  */
17
17
  export declare const SUBPROCESS_MAX_BUFFER: number;
18
+ /**
19
+ * Reason text for a failed capture, so an overflow is never blank (#1867 / #3903).
20
+ *
21
+ * A spawn-level failure (ENOBUFS past the ceiling, timeout kill, missing
22
+ * binary) reports no exit status and empty stderr, whether it arrives as a
23
+ * `spawnSync` result or as a thrown `execFileSync` error. Only then does the
24
+ * error message stand in for stderr: a process that exited with a status and
25
+ * chose to say nothing keeps its empty stderr.
26
+ */
27
+ export declare function resolveCaptureFailureStderr(capture: {
28
+ readonly captured: string;
29
+ readonly status: number | null | undefined;
30
+ readonly message: string | undefined;
31
+ }): string;
18
32
  //# sourceMappingURL=max-buffer.d.ts.map