@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
@@ -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,
@@ -1,8 +1,9 @@
1
1
  import { type ActiveCliCheckResult, type ActiveCliCheckSeams } from "./active-cli.js";
2
2
  import { type GitRunner } from "./git.js";
3
3
  import { type DirectivePosture } from "./posture.js";
4
- import { type GatedStepName, type SessionCeremonyTier } from "./session-start.js";
4
+ import { type GatedStepName, type SessionCeremonyTier, WRITE_GATED_EXECUTE_STEPS, WRITE_GATED_REQUIRED_STEPS } from "./session-start.js";
5
5
  export { formatCacheFetchAllRecoveryCommand, recoveryHintForStaleFailure, } from "./cache-recovery.js";
6
+ export { WRITE_GATED_EXECUTE_STEPS, WRITE_GATED_REQUIRED_STEPS };
6
7
  export declare const ENV_SKIP = "DEFT_SESSION_RITUAL_SKIP";
7
8
  export { ENTRYPOINT_TIMEOUT_EXIT_CODE, ENTRYPOINT_TIMEOUT_SECONDS, } from "./ritual-entrypoint.js";
8
9
  export declare const GATED_ENTRYPOINT_COMMANDS: Readonly<Record<GatedStepName, readonly string[]>>;
@@ -40,6 +41,11 @@ export interface VerifyResult {
40
41
  * `rearm` when age/compact stale on an otherwise valid bind; `cold` otherwise.
41
42
  */
42
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;
43
49
  }
44
50
  export type RitualRunner = (command: readonly string[], projectRoot: string) => {
45
51
  code: number;
@@ -81,7 +87,24 @@ export interface VerifySessionRitualOptions {
81
87
  readonly checkActiveCli?: (targetVersion: string | null, seams?: ActiveCliCheckSeams) => ActiveCliCheckResult;
82
88
  /** Injectable work-selection detector for gated `cache_fresh` (#3507). */
83
89
  readonly detectWorkSelection?: DetectWorkSelection;
90
+ /**
91
+ * Required gated steps for this surface. Defaults to all {@link GATED_STEPS}
92
+ * (session / full gated verify). Write dispatch uses
93
+ * {@link WRITE_GATED_REQUIRED_STEPS} (#3738).
94
+ */
95
+ readonly requiredGatedSteps?: readonly GatedStepName[];
96
+ /**
97
+ * Gated steps this surface may execute. Defaults to all {@link GATED_STEPS}.
98
+ * Write dispatch uses {@link WRITE_GATED_EXECUTE_STEPS} so `cache_fresh` and
99
+ * `doctor` cannot run as write-path repair (#3738).
100
+ */
101
+ readonly executeGatedSteps?: readonly GatedStepName[];
84
102
  }
103
+ /**
104
+ * Options for the write/spawn mutation verifier (#3738).
105
+ * Required set omits `cache_fresh`; execution authority is `agent_hooks` only.
106
+ */
107
+ export declare function writeGateRitualOptions(extras?: Omit<VerifySessionRitualOptions, "requiredGatedSteps" | "executeGatedSteps" | "tier">): VerifySessionRitualOptions;
85
108
  export interface InspectSessionRitualOptions {
86
109
  readonly tier?: "quick" | "gated";
87
110
  readonly now?: Date;
@@ -89,13 +112,20 @@ export interface InspectSessionRitualOptions {
89
112
  readonly posture?: DirectivePosture;
90
113
  readonly envPosture?: string | undefined;
91
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[];
92
121
  }
93
122
  /**
94
123
  * Read-only ritual-state inspection for host hooks.
95
124
  *
96
125
  * Unlike {@link verifySessionRitual}, this never runs missing gated entrypoints
97
- * and never rewrites `.deft/ritual-state.json`. A PreToolUse decision must be a
98
- * probe, not a hidden `doctor` / cache-refresh mutation boundary.
126
+ * and never rewrites `.deft/ritual-state.json`. Write dispatch does **not** use
127
+ * this function: it must still execute `agent_hooks` (#3738). A PreToolUse
128
+ * decision must not become a hidden `doctor` / cache-refresh mutation boundary.
99
129
  */
100
130
  export declare function inspectSessionRitual(projectRoot: string, options?: InspectSessionRitualOptions): VerifyResult;
101
131
  export declare function verifySessionRitual(projectRoot: string, options?: VerifySessionRitualOptions): VerifyResult;
@@ -10,9 +10,10 @@ import { pythonJsonDump } from "./json.js";
10
10
  import { ENV_SESSION_POSTURE, readOnlyPostureMessage, resolveSessionPosture, ritualStateIsPostureAuthority, } from "./posture.js";
11
11
  import { defaultRitualRunner } from "./ritual-entrypoint.js";
12
12
  import { readRitualState, ritualStateMarksRearmNeeded, ritualStatePath, ritualStep, writeRitualState, } from "./ritual-sentinel.js";
13
- import { formatSessionStartRecoveryCommand, GATED_STEPS, QUICK_STEPS, } from "./session-start.js";
13
+ import { formatSessionStartRecoveryCommand, GATED_STEPS, QUICK_STEPS, WRITE_GATED_EXECUTE_STEPS, WRITE_GATED_REQUIRED_STEPS, } from "./session-start.js";
14
14
  import { resolveSessionRitualStalenessHours } from "./staleness.js";
15
15
  export { formatCacheFetchAllRecoveryCommand, recoveryHintForStaleFailure, } from "./cache-recovery.js";
16
+ export { WRITE_GATED_EXECUTE_STEPS, WRITE_GATED_REQUIRED_STEPS };
16
17
  export const ENV_SKIP = "DEFT_SESSION_RITUAL_SKIP";
17
18
  export { ENTRYPOINT_TIMEOUT_EXIT_CODE, ENTRYPOINT_TIMEOUT_SECONDS, } from "./ritual-entrypoint.js";
18
19
  export const GATED_ENTRYPOINT_COMMANDS = {
@@ -135,7 +136,37 @@ function shouldRearmDriftProbe(payload, projectRoot, detect) {
135
136
  return false;
136
137
  return detect(projectRoot).inPlay;
137
138
  }
138
- 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) {
139
170
  const command = resolveGatedEntrypointCommand(stepName, projectRoot, detect);
140
171
  const { code, stdout, stderr } = runner(command, projectRoot);
141
172
  const message = stdout.trim() || stderr.trim() || `${command[0]} exited ${code}`;
@@ -151,11 +182,9 @@ function runGatedStep(projectRoot, payload, stepName, runner, now, detect = dete
151
182
  if (stepName === "cache_fresh") {
152
183
  applyCacheFreshDriftProbeField(payload, command);
153
184
  }
154
- try {
155
- writeRitualState(projectRoot, payload);
156
- }
157
- catch (exc) {
158
- 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}`;
159
188
  }
160
189
  return null;
161
190
  }
@@ -198,14 +227,11 @@ function evaluateLoadedState(projectRoot, state, input) {
198
227
  return { code: 1, message: headDriftRecoveryMessage(), recoveryTier: "cold" };
199
228
  }
200
229
  if (input.rebindForwardHead) {
201
- const payload = { ...state.raw, git_head: currentHead };
202
- try {
203
- writeRitualState(projectRoot, payload);
204
- }
205
- catch (exc) {
230
+ const refusal = writeRitualStateIfStillOwned(projectRoot, { ...state.raw, git_head: currentHead }, { sessionId: state.sessionId, startedAt: state.startedAt });
231
+ if (refusal !== null) {
206
232
  return {
207
233
  code: 2,
208
- message: `could not rebind session ritual git HEAD: ${String(exc)}`,
234
+ message: `could not rebind session ritual git HEAD: ${refusal}`,
209
235
  recoveryTier: "cold",
210
236
  };
211
237
  }
@@ -217,6 +243,7 @@ function evaluateLoadedState(projectRoot, state, input) {
217
243
  code: 2,
218
244
  message: staleness.error ?? "session ritual staleness policy is invalid",
219
245
  recoveryTier: "cold",
246
+ boundSessionId: state.sessionId,
220
247
  };
221
248
  }
222
249
  const maxAgeMs = staleness.hours * 60 * 60 * 1000;
@@ -231,6 +258,7 @@ function evaluateLoadedState(projectRoot, state, input) {
231
258
  message: `session ritual state is stale (older than ${staleness.hours}h).${compactNote} ` +
232
259
  `Run \`${rearmCmd}\` to re-arm (or \`${coldCmd}\` for a full cold ceremony).`,
233
260
  recoveryTier: "rearm",
261
+ boundSessionId: state.sessionId,
234
262
  };
235
263
  }
236
264
  for (const stepName of QUICK_STEPS) {
@@ -240,17 +268,23 @@ function evaluateLoadedState(projectRoot, state, input) {
240
268
  code: 1,
241
269
  message: failedStepMessage("quick", stepName, step),
242
270
  recoveryTier: "cold",
271
+ boundSessionId: state.sessionId,
243
272
  };
244
273
  }
245
274
  }
246
275
  if (input.tier === "gated") {
247
- for (const stepName of GATED_STEPS) {
276
+ const requiredGated = input.requiredGatedSteps ?? GATED_STEPS;
277
+ for (const stepName of requiredGated) {
248
278
  const step = state.gatedSteps[stepName];
249
279
  if (!stepPasses(step)) {
280
+ const recoveryTier = "cold";
250
281
  return {
251
282
  code: 1,
252
- message: failedStepMessage("gated", stepName, step),
253
- recoveryTier: "cold",
283
+ message: failedStepMessage("gated", stepName, step) +
284
+ " " +
285
+ formatRitualRecoveryInstruction(recoveryTier),
286
+ recoveryTier,
287
+ boundSessionId: state.sessionId,
254
288
  };
255
289
  }
256
290
  }
@@ -259,14 +293,34 @@ function evaluateLoadedState(projectRoot, state, input) {
259
293
  code: 0,
260
294
  message: `OK session ritual ${input.tier} tier is fresh.`,
261
295
  recoveryTier: null,
296
+ boundSessionId: state.sessionId,
297
+ };
298
+ }
299
+ /**
300
+ * Options for the write/spawn mutation verifier (#3738).
301
+ * Required set omits `cache_fresh`; execution authority is `agent_hooks` only.
302
+ */
303
+ export function writeGateRitualOptions(extras = {}) {
304
+ return {
305
+ ...extras,
306
+ tier: "gated",
307
+ posture: extras.posture ?? "mutation",
308
+ requiredGatedSteps: WRITE_GATED_REQUIRED_STEPS,
309
+ executeGatedSteps: WRITE_GATED_EXECUTE_STEPS,
310
+ // Write dispatch already refuses skip (dispatcher passes bypass: false).
311
+ // Default the same here so CI DEFT_SESSION_RITUAL_SKIP=1 cannot empty
312
+ // the execute loop in tests that use this helper (#3738).
313
+ bypass: extras.bypass ?? false,
314
+ envSkip: extras.envSkip ?? "",
262
315
  };
263
316
  }
264
317
  /**
265
318
  * Read-only ritual-state inspection for host hooks.
266
319
  *
267
320
  * Unlike {@link verifySessionRitual}, this never runs missing gated entrypoints
268
- * and never rewrites `.deft/ritual-state.json`. A PreToolUse decision must be a
269
- * probe, not a hidden `doctor` / cache-refresh mutation boundary.
321
+ * and never rewrites `.deft/ritual-state.json`. Write dispatch does **not** use
322
+ * this function: it must still execute `agent_hooks` (#3738). A PreToolUse
323
+ * decision must not become a hidden `doctor` / cache-refresh mutation boundary.
270
324
  */
271
325
  export function inspectSessionRitual(projectRoot, options = {}) {
272
326
  const tier = options.tier ?? "quick";
@@ -314,6 +368,7 @@ export function inspectSessionRitual(projectRoot, options = {}) {
314
368
  now: options.now ?? new Date(),
315
369
  runGit: options.runGit,
316
370
  rebindForwardHead: false,
371
+ requiredGatedSteps: options.requiredGatedSteps,
317
372
  });
318
373
  return {
319
374
  code: evaluated.code,
@@ -325,6 +380,7 @@ export function inspectSessionRitual(projectRoot, options = {}) {
325
380
  posture,
326
381
  ritualStateRequired,
327
382
  recoveryTier: evaluated.recoveryTier,
383
+ ...(evaluated.boundSessionId === undefined ? {} : { boundSessionId: evaluated.boundSessionId }),
328
384
  };
329
385
  }
330
386
  export function verifySessionRitual(projectRoot, options = {}) {
@@ -419,6 +475,9 @@ export function verifySessionRitual(projectRoot, options = {}) {
419
475
  posture,
420
476
  ritualStateRequired,
421
477
  recoveryTier: precheck.recoveryTier,
478
+ ...(precheck.boundSessionId === undefined
479
+ ? {}
480
+ : { boundSessionId: precheck.boundSessionId }),
422
481
  };
423
482
  }
424
483
  const reloadedAfterPrecheck = readRitualState(projectRoot);
@@ -437,12 +496,18 @@ export function verifySessionRitual(projectRoot, options = {}) {
437
496
  };
438
497
  }
439
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 };
440
502
  const gated = { ...payload.gated_steps };
441
503
  payload.gated_steps = gated;
442
504
  const runCmd = options.runner ?? defaultRitualRunner;
443
505
  const forced = new Set(options.forceGatedSteps ?? []);
444
506
  const detect = options.detectWorkSelection ?? detectWorkSelection;
507
+ const executable = new Set(options.executeGatedSteps ?? [...GATED_STEPS]);
445
508
  for (const stepName of GATED_STEPS) {
509
+ if (!executable.has(stepName))
510
+ continue;
446
511
  const step = gated[stepName];
447
512
  if (step?.deferred_reason && stepName !== "agent_hooks")
448
513
  continue;
@@ -453,7 +518,7 @@ export function verifySessionRitual(projectRoot, options = {}) {
453
518
  if (stepName !== "agent_hooks" && stepPasses(step) && !forced.has(stepName) && !rearmDrift) {
454
519
  continue;
455
520
  }
456
- const writeError = runGatedStep(projectRoot, payload, stepName, runCmd, instant, detect);
521
+ const writeError = runGatedStep(projectRoot, payload, stepName, runCmd, instant, ownerAtVerify, detect);
457
522
  if (writeError !== null) {
458
523
  return {
459
524
  code: 2,
@@ -519,6 +584,7 @@ export function verifySessionRitual(projectRoot, options = {}) {
519
584
  now: instant,
520
585
  runGit: options.runGit,
521
586
  rebindForwardHead: true,
587
+ requiredGatedSteps: options.requiredGatedSteps,
522
588
  });
523
589
  if (isBypassed) {
524
590
  return {
@@ -531,6 +597,9 @@ export function verifySessionRitual(projectRoot, options = {}) {
531
597
  posture,
532
598
  ritualStateRequired,
533
599
  recoveryTier: evaluated.recoveryTier,
600
+ ...(evaluated.boundSessionId === undefined
601
+ ? {}
602
+ : { boundSessionId: evaluated.boundSessionId }),
534
603
  };
535
604
  }
536
605
  return {
@@ -543,6 +612,7 @@ export function verifySessionRitual(projectRoot, options = {}) {
543
612
  posture,
544
613
  ritualStateRequired,
545
614
  recoveryTier: evaluated.recoveryTier,
615
+ ...(evaluated.boundSessionId === undefined ? {} : { boundSessionId: evaluated.boundSessionId }),
546
616
  };
547
617
  }
548
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;