@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
@@ -0,0 +1,12 @@
1
+ import { type GithubIssueSnapshot, type ValueAdvice } from "./types.js";
2
+ export declare class ReservedClearanceError extends Error {
3
+ name: string;
4
+ }
5
+ export declare function assertNoReservedClearance(text: string, label: string): void;
6
+ /**
7
+ * Value MAY recommend critique via `critique-recommend:`.
8
+ * It MUST NOT stamp the reserved ADR-005 clearance grammar.
9
+ */
10
+ export declare function buildValueAdvice(issue: GithubIssueSnapshot | null): ValueAdvice;
11
+ export declare function formatValueField(advice: ValueAdvice): string;
12
+ //# sourceMappingURL=value.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { CRITIQUE_RECOMMEND_FIELD, RESERVED_CLEARANCE_RE, } from "./types.js";
2
+ export class ReservedClearanceError extends Error {
3
+ name = "ReservedClearanceError";
4
+ }
5
+ export function assertNoReservedClearance(text, label) {
6
+ if (RESERVED_CLEARANCE_RE.test(text)) {
7
+ throw new ReservedClearanceError(`${label} must not emit the reserved clearance line ` +
8
+ "`design-critique: warranted | not warranted, because …`");
9
+ }
10
+ }
11
+ /**
12
+ * Value MAY recommend critique via `critique-recommend:`.
13
+ * It MUST NOT stamp the reserved ADR-005 clearance grammar.
14
+ */
15
+ export function buildValueAdvice(issue) {
16
+ const labels = issue?.labels ?? [];
17
+ const stamped = labels.some((label) => label === "design-critique:mechanism-shaped");
18
+ const reason = stamped
19
+ ? "GitHub label design-critique:mechanism-shaped is present; author still stamps clearance"
20
+ : "no mechanism-shaped stamp; critique not recommended from evaluation";
21
+ const advice = {
22
+ [CRITIQUE_RECOMMEND_FIELD]: stamped,
23
+ reason,
24
+ };
25
+ assertNoReservedClearance(JSON.stringify(advice), "value advice");
26
+ return advice;
27
+ }
28
+ export function formatValueField(advice) {
29
+ const line = `${CRITIQUE_RECOMMEND_FIELD}: ${advice[CRITIQUE_RECOMMEND_FIELD] ? "true" : "false"}`;
30
+ assertNoReservedClearance(`${line}\n${advice.reason}`, "value field");
31
+ return `${line}\n${advice.reason}`;
32
+ }
33
+ //# sourceMappingURL=value.js.map
@@ -0,0 +1,5 @@
1
+ import type { WipCensus, WipHit } from "./types.js";
2
+ /** Parent-owned. Evaluators must never receive this object. */
3
+ export declare function collectWipCensus(projectRoot: string, issues: readonly number[]): WipCensus;
4
+ export declare function wipHitsForIssue(census: WipCensus, issue: number): WipHit[];
5
+ //# sourceMappingURL=wip-census.d.ts.map
@@ -0,0 +1,39 @@
1
+ import { readPlanSequence } from "../../plan-sequence/store.js";
2
+ import { listXbriefHits } from "./xbrief-refs.js";
3
+ function hitsForIssues(hits, issues, kind) {
4
+ return hits
5
+ .filter((hit) => issues.has(hit.issue))
6
+ .map((hit) => ({ kind, path: hit.path, issue: hit.issue }));
7
+ }
8
+ /** Parent-owned. Evaluators must never receive this object. */
9
+ export function collectWipCensus(projectRoot, issues) {
10
+ const wanted = new Set(issues);
11
+ const activeHits = listXbriefHits(projectRoot, "active");
12
+ const pendingHits = listXbriefHits(projectRoot, "pending");
13
+ const planHits = [];
14
+ const sequence = readPlanSequence(projectRoot);
15
+ if (sequence !== null) {
16
+ for (const entry of sequence.entries) {
17
+ const fromField = entry.issue;
18
+ const fromId = Number.parseInt(entry.id.trim().replace(/^#/u, ""), 10);
19
+ const asNumber = typeof fromField === "number" ? fromField : fromId;
20
+ if (!Number.isFinite(asNumber) || !wanted.has(asNumber)) {
21
+ continue;
22
+ }
23
+ planHits.push({
24
+ kind: "plan-sequence",
25
+ path: ".deft/plan-sequence.json",
26
+ issue: asNumber,
27
+ });
28
+ }
29
+ }
30
+ return {
31
+ active: hitsForIssues(activeHits, wanted, "active-xbrief"),
32
+ pending: hitsForIssues(pendingHits, wanted, "pending-xbrief"),
33
+ planSequence: planHits,
34
+ };
35
+ }
36
+ export function wipHitsForIssue(census, issue) {
37
+ return [...census.active, ...census.pending, ...census.planSequence].filter((hit) => hit.issue === issue);
38
+ }
39
+ //# sourceMappingURL=wip-census.js.map
@@ -0,0 +1,8 @@
1
+ import type { GitRunner } from "./types.js";
2
+ export declare class EvaluatorWorktreeError extends Error {
3
+ name: string;
4
+ }
5
+ export declare function addEvaluatorWorktree(projectRoot: string, issue: number, invocationId: string, originSha: string, git?: GitRunner): string;
6
+ /** Verb-owned remove. Not a shared core git-worktree helper. */
7
+ export declare function removeEvaluatorWorktree(projectRoot: string, worktreePath: string, git?: GitRunner): void;
8
+ //# sourceMappingURL=worktrees.d.ts.map
@@ -0,0 +1,68 @@
1
+ import { existsSync, mkdirSync, rmSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { defaultGitRunner as swarmGitRunner, } from "../../swarm/worktrees.js";
4
+ import { evaluatorWorktreePath } from "./paths.js";
5
+ export class EvaluatorWorktreeError extends Error {
6
+ name = "EvaluatorWorktreeError";
7
+ }
8
+ function toSwarm(git) {
9
+ return (args, cwd) => git(args, cwd);
10
+ }
11
+ export function addEvaluatorWorktree(projectRoot, issue, invocationId, originSha, git = swarmGitRunner) {
12
+ const sha = originSha.trim();
13
+ if (sha.length === 0) {
14
+ throw new EvaluatorWorktreeError("originSha is required for a detached evaluator worktree");
15
+ }
16
+ const worktreePath = evaluatorWorktreePath(projectRoot, issue, invocationId);
17
+ mkdirSync(dirname(resolve(worktreePath)), { recursive: true });
18
+ const proc = toSwarm(git)(["worktree", "add", "--detach", worktreePath, sha], projectRoot);
19
+ if (proc.returncode !== 0) {
20
+ throw new EvaluatorWorktreeError(`git worktree add --detach failed for issue ${issue}: ${proc.stderr.trim() || "<no stderr>"}`);
21
+ }
22
+ return worktreePath;
23
+ }
24
+ function forceDeleteWorktreeDir(worktreePath) {
25
+ if (!existsSync(worktreePath)) {
26
+ return;
27
+ }
28
+ rmSync(worktreePath, { recursive: true, force: true });
29
+ }
30
+ function worktreeStillRegistered(git, projectRoot, worktreePath) {
31
+ const listed = git(["worktree", "list", "--porcelain"], projectRoot);
32
+ if (listed.returncode !== 0) {
33
+ return true;
34
+ }
35
+ const needle = resolve(worktreePath).replace(/\\/g, "/").toLowerCase();
36
+ for (const line of listed.stdout.split(/\r?\n/u)) {
37
+ if (!line.startsWith("worktree ")) {
38
+ continue;
39
+ }
40
+ const listedPath = line.slice("worktree ".length).replace(/\\/g, "/").toLowerCase();
41
+ if (listedPath === needle) {
42
+ return true;
43
+ }
44
+ }
45
+ return false;
46
+ }
47
+ /** Verb-owned remove. Not a shared core git-worktree helper. */
48
+ export function removeEvaluatorWorktree(projectRoot, worktreePath, git = swarmGitRunner) {
49
+ const runner = toSwarm(git);
50
+ const proc = runner(["worktree", "remove", "--force", worktreePath], projectRoot);
51
+ if (proc.returncode === 0) {
52
+ return;
53
+ }
54
+ const firstError = proc.stderr.trim() || "<no stderr>";
55
+ forceDeleteWorktreeDir(worktreePath);
56
+ runner(["worktree", "prune"], projectRoot);
57
+ const retry = runner(["worktree", "remove", "--force", worktreePath], projectRoot);
58
+ if (retry.returncode === 0) {
59
+ return;
60
+ }
61
+ forceDeleteWorktreeDir(worktreePath);
62
+ runner(["worktree", "prune"], projectRoot);
63
+ if (!worktreeStillRegistered(runner, projectRoot, worktreePath) && !existsSync(worktreePath)) {
64
+ return;
65
+ }
66
+ throw new EvaluatorWorktreeError(`git worktree remove failed for ${worktreePath}: ${firstError}; fallback: ${retry.stderr.trim() || "<no stderr>"}`);
67
+ }
68
+ //# sourceMappingURL=worktrees.js.map
@@ -0,0 +1,8 @@
1
+ export interface XbriefHit {
2
+ readonly issue: number;
3
+ readonly path: string;
4
+ readonly folder: "active" | "pending" | "proposed" | "completed" | "cancelled";
5
+ }
6
+ export declare function issueNumbersFromXbriefJson(raw: unknown): number[];
7
+ export declare function listXbriefHits(root: string, folder: XbriefHit["folder"]): XbriefHit[];
8
+ //# sourceMappingURL=xbrief-refs.d.ts.map
@@ -0,0 +1,60 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const ISSUE_URI_RE = /\/issues\/(\d+)\b/u;
4
+ const ISSUE_HASH_RE = /(?:Issue\s+)?#(\d+)\b/u;
5
+ function numbersFromUnknown(value, into) {
6
+ if (typeof value === "string") {
7
+ for (const re of [ISSUE_URI_RE, ISSUE_HASH_RE]) {
8
+ const match = re.exec(value);
9
+ if (match?.[1] !== undefined) {
10
+ into.add(Number.parseInt(match[1], 10));
11
+ }
12
+ }
13
+ return;
14
+ }
15
+ if (Array.isArray(value)) {
16
+ for (const item of value) {
17
+ numbersFromUnknown(item, into);
18
+ }
19
+ return;
20
+ }
21
+ if (value !== null && typeof value === "object") {
22
+ for (const nested of Object.values(value)) {
23
+ numbersFromUnknown(nested, into);
24
+ }
25
+ }
26
+ }
27
+ export function issueNumbersFromXbriefJson(raw) {
28
+ const found = new Set();
29
+ if (raw === null || typeof raw !== "object") {
30
+ return [];
31
+ }
32
+ const plan = raw.plan;
33
+ numbersFromUnknown(plan?.references, found);
34
+ return [...found];
35
+ }
36
+ export function listXbriefHits(root, folder) {
37
+ const dir = join(root, "xbrief", folder);
38
+ if (!existsSync(dir)) {
39
+ return [];
40
+ }
41
+ const hits = [];
42
+ for (const name of readdirSync(dir)) {
43
+ if (!name.endsWith(".xbrief.json") && !name.endsWith(".vbrief.json")) {
44
+ continue;
45
+ }
46
+ const path = join(dir, name);
47
+ let parsed;
48
+ try {
49
+ parsed = JSON.parse(readFileSync(path, "utf8"));
50
+ }
51
+ catch {
52
+ continue;
53
+ }
54
+ for (const issue of issueNumbersFromXbriefJson(parsed)) {
55
+ hits.push({ issue, path, folder });
56
+ }
57
+ }
58
+ return hits;
59
+ }
60
+ //# sourceMappingURL=xbrief-refs.js.map
@@ -23,6 +23,17 @@ export declare const registryData: {
23
23
  readonly see_also: readonly ["task triage:bootstrap", "task cache:fetch-all", "#1119 / D5"];
24
24
  readonly placeholder: false;
25
25
  };
26
+ readonly "task triage:evaluate": {
27
+ readonly name: "task triage:evaluate";
28
+ readonly summary: "Isolated validity + parent WIP census + value advice";
29
+ readonly refs: "(#3648)";
30
+ readonly description: "Stage A issue-eval: detached origin/master worktrees own validity and ADR/contract reads; the parent on the live working set owns WIP census (xbrief/active, xbrief/pending, plan-sequence); GitHub REST owns open PRs/issues/duplicates. Writes a gitignored sink under .deft-scratch/issue-eval/<sha12>/<invocation-id>/. No GitHub writes. No candidates-log row. Value may set critique-recommend: and must not emit the reserved design-critique clearance line. Fan-out default 4, override --concurrency N.";
31
+ readonly usage: "task triage:evaluate -- <N...> [--concurrency N] [--repo OWNER/NAME] [--json]";
32
+ readonly flags: readonly [readonly ["<N...>", "(required)", "Issue numbers to evaluate."], readonly ["--concurrency N", "4", "Parallel evaluator cap (bind, not a measured existing cap)."], readonly ["--repo OWNER/NAME", "(git remote / env)", "Upstream repo for REST reads."], readonly ["--json", "(off)", "Emit the structured EvaluateResult."], readonly ["--project-root PATH", "(cwd)", "Project root override (Taskfile threads USER_WORKING_DIR)."]];
33
+ readonly examples: readonly ["task triage:evaluate -- 3648", "task triage:evaluate -- 10 11 --concurrency 2 --json"];
34
+ readonly see_also: readonly ["task triage:queue", "task triage:accept", "skills/deft-directive-issue-eval/SKILL.md", "#3648"];
35
+ readonly placeholder: false;
36
+ };
26
37
  readonly "task triage:accept": {
27
38
  readonly name: "task triage:accept";
28
39
  readonly summary: "Mark issue accepted; chains into scope:promote w/ flag";
@@ -444,12 +455,12 @@ export declare const registryData: {
444
455
  readonly "task scope:complete": {
445
456
  readonly name: "task scope:complete";
446
457
  readonly summary: "active/ -> completed/ (set status completed)";
447
- readonly refs: "(#845)";
448
- readonly description: "Mark an active vBRIEF complete and move it to vbrief/completed/. Terminal transition; use scope:undo if you need reversibility (refused on terminal actions per D15).";
449
- readonly usage: "task scope:complete -- <file>";
450
- readonly flags: readonly [readonly ["<file>", "(required)", "Path to vBRIEF."], readonly ["--project-root PATH", "(detected)", "Consumer project root override."]];
451
- readonly examples: readonly ["task scope:complete -- vbrief/active/2026-05-19-foo.vbrief.json"];
452
- readonly see_also: readonly ["task scope:fail", "task scope:cancel", "#1119 / #845"];
458
+ readonly refs: "(#845, #3041, #3721)";
459
+ readonly description: "Mark an active vBRIEF complete and move it to vbrief/completed/. Terminal transition; use scope:undo if you need reversibility (refused on terminal actions per D15). Code-bearing completion requires delivery evidence: --merge-commit and --pr. Those flags feed the delivery gate; they do not skip literal-AC.";
460
+ readonly usage: "task scope:complete -- <file> [--merge-commit SHA] [--pr N]";
461
+ readonly flags: readonly [readonly ["<file>", "(required)", "Path to vBRIEF."], readonly ["--project-root PATH", "(detected)", "Consumer project root override."], readonly ["--merge-commit SHA", "(required for code-bearing)", "Merge commit SHA for delivery evidence (#3041)."], readonly ["--pr N", "(required for code-bearing)", "Merged PR number for delivery evidence (#3041)."], readonly ["--pr-base BRANCH", "(optional)", "PR base branch for delivery evidence."], readonly ["--delivery-branch BRANCH", "(optional)", "Delivery branch tip for completed-tracked."]];
462
+ readonly examples: readonly ["task scope:complete -- vbrief/active/2026-05-19-foo.vbrief.json", "task scope:complete -- xbrief/active/story.xbrief.json --merge-commit abc123 --pr 42"];
463
+ readonly see_also: readonly ["task scope:fail", "task scope:cancel", "#1119 / #845", "#3041"];
453
464
  readonly placeholder: false;
454
465
  };
455
466
  readonly "task scope:fail": {
@@ -530,7 +541,7 @@ export declare const registryData: {
530
541
  readonly placeholder: false;
531
542
  };
532
543
  };
533
- readonly categoriesTriage: readonly [readonly ["Session-start", readonly ["task triage:summary", "task verify:cache-fresh"]], readonly ["State verbs (mutate audit log)", readonly ["task triage:accept", "task triage:defer", "task triage:reject", "task triage:needs-ac", "task triage:mark-duplicate", "task triage:reset", "task triage:status", "task triage:history"]], readonly ["Read verbs", readonly ["task triage:queue", "task triage:audit", "task triage:show", "task triage:scope", "task triage:scope-drift", "task triage:classify"]], readonly ["Lifecycle", readonly ["task triage:bootstrap", "task triage:welcome", "task triage:reconcile"]], readonly ["Bulk variants", readonly ["task triage:bulk-accept", "task triage:bulk-reject", "task triage:bulk-defer", "task triage:bulk-needs-ac", "task triage:refresh-active", "task triage:smoketest"]], readonly ["Subscription mutation", readonly ["task triage:subscribe", "task triage:unsubscribe"]], readonly ["Archive / rotation", readonly ["task triage:audit:prune", "task triage:archive-list", "task triage:restore-from-archive", "task triage:audit-log:rotate", "task triage:metrics"]], readonly ["Framework eval (#1703)", readonly ["task eval:health", "task eval:triggers", "task eval:run", "task eval:report"]]];
544
+ readonly categoriesTriage: readonly [readonly ["Session-start", readonly ["task triage:summary", "task verify:cache-fresh"]], readonly ["State verbs (mutate audit log)", readonly ["task triage:accept", "task triage:defer", "task triage:reject", "task triage:needs-ac", "task triage:mark-duplicate", "task triage:reset", "task triage:status", "task triage:history"]], readonly ["Read verbs", readonly ["task triage:queue", "task triage:evaluate", "task triage:audit", "task triage:show", "task triage:scope", "task triage:scope-drift", "task triage:classify"]], readonly ["Lifecycle", readonly ["task triage:bootstrap", "task triage:welcome", "task triage:reconcile"]], readonly ["Bulk variants", readonly ["task triage:bulk-accept", "task triage:bulk-reject", "task triage:bulk-defer", "task triage:bulk-needs-ac", "task triage:refresh-active", "task triage:smoketest"]], readonly ["Subscription mutation", readonly ["task triage:subscribe", "task triage:unsubscribe"]], readonly ["Archive / rotation", readonly ["task triage:audit:prune", "task triage:archive-list", "task triage:restore-from-archive", "task triage:audit-log:rotate", "task triage:metrics"]], readonly ["Framework eval (#1703)", readonly ["task eval:health", "task eval:triggers", "task eval:run", "task eval:report"]]];
534
545
  readonly categoriesScope: readonly [readonly ["Promote / demote", readonly ["task scope:promote", "task scope:demote"]], readonly ["Activate / complete", readonly ["task scope:activate", "task scope:complete", "task scope:fail", "task scope:cancel", "task scope:block", "task scope:unblock"]], readonly ["Reversibility", readonly ["task scope:undo", "task scope:restore"]], readonly ["Decomposition", readonly ["task scope:decompose"]]];
535
546
  readonly scriptSubcommandMap: {
536
547
  readonly triage_actions: {
@@ -575,6 +586,9 @@ export declare const registryData: {
575
586
  readonly triage_reconcile: {
576
587
  readonly __default__: "task triage:reconcile";
577
588
  };
589
+ readonly triage_evaluate: {
590
+ readonly __default__: "task triage:evaluate";
591
+ };
578
592
  readonly triage_queue: {
579
593
  readonly queue: "task triage:queue";
580
594
  readonly show: "task triage:show";
@@ -34,6 +34,35 @@ export const registryData = {
34
34
  see_also: ["task triage:bootstrap", "task cache:fetch-all", "#1119 / D5"],
35
35
  placeholder: false,
36
36
  },
37
+ "task triage:evaluate": {
38
+ name: "task triage:evaluate",
39
+ summary: "Isolated validity + parent WIP census + value advice",
40
+ refs: "(#3648)",
41
+ description: "Stage A issue-eval: detached origin/master worktrees own validity and ADR/contract reads; the parent on the live working set owns WIP census (xbrief/active, xbrief/pending, plan-sequence); GitHub REST owns open PRs/issues/duplicates. Writes a gitignored sink under .deft-scratch/issue-eval/<sha12>/<invocation-id>/. No GitHub writes. No candidates-log row. Value may set critique-recommend: and must not emit the reserved design-critique clearance line. Fan-out default 4, override --concurrency N.",
42
+ usage: "task triage:evaluate -- <N...> [--concurrency N] [--repo OWNER/NAME] [--json]",
43
+ flags: [
44
+ ["<N...>", "(required)", "Issue numbers to evaluate."],
45
+ ["--concurrency N", "4", "Parallel evaluator cap (bind, not a measured existing cap)."],
46
+ ["--repo OWNER/NAME", "(git remote / env)", "Upstream repo for REST reads."],
47
+ ["--json", "(off)", "Emit the structured EvaluateResult."],
48
+ [
49
+ "--project-root PATH",
50
+ "(cwd)",
51
+ "Project root override (Taskfile threads USER_WORKING_DIR).",
52
+ ],
53
+ ],
54
+ examples: [
55
+ "task triage:evaluate -- 3648",
56
+ "task triage:evaluate -- 10 11 --concurrency 2 --json",
57
+ ],
58
+ see_also: [
59
+ "task triage:queue",
60
+ "task triage:accept",
61
+ "skills/deft-directive-issue-eval/SKILL.md",
62
+ "#3648",
63
+ ],
64
+ placeholder: false,
65
+ },
37
66
  "task triage:accept": {
38
67
  name: "task triage:accept",
39
68
  summary: "Mark issue accepted; chains into scope:promote w/ flag",
@@ -782,15 +811,30 @@ export const registryData = {
782
811
  "task scope:complete": {
783
812
  name: "task scope:complete",
784
813
  summary: "active/ -> completed/ (set status completed)",
785
- refs: "(#845)",
786
- description: "Mark an active vBRIEF complete and move it to vbrief/completed/. Terminal transition; use scope:undo if you need reversibility (refused on terminal actions per D15).",
787
- usage: "task scope:complete -- <file>",
814
+ refs: "(#845, #3041, #3721)",
815
+ description: "Mark an active vBRIEF complete and move it to vbrief/completed/. Terminal transition; use scope:undo if you need reversibility (refused on terminal actions per D15). Code-bearing completion requires delivery evidence: --merge-commit and --pr. Those flags feed the delivery gate; they do not skip literal-AC.",
816
+ usage: "task scope:complete -- <file> [--merge-commit SHA] [--pr N]",
788
817
  flags: [
789
818
  ["<file>", "(required)", "Path to vBRIEF."],
790
819
  ["--project-root PATH", "(detected)", "Consumer project root override."],
820
+ [
821
+ "--merge-commit SHA",
822
+ "(required for code-bearing)",
823
+ "Merge commit SHA for delivery evidence (#3041).",
824
+ ],
825
+ [
826
+ "--pr N",
827
+ "(required for code-bearing)",
828
+ "Merged PR number for delivery evidence (#3041).",
829
+ ],
830
+ ["--pr-base BRANCH", "(optional)", "PR base branch for delivery evidence."],
831
+ ["--delivery-branch BRANCH", "(optional)", "Delivery branch tip for completed-tracked."],
791
832
  ],
792
- examples: ["task scope:complete -- vbrief/active/2026-05-19-foo.vbrief.json"],
793
- see_also: ["task scope:fail", "task scope:cancel", "#1119 / #845"],
833
+ examples: [
834
+ "task scope:complete -- vbrief/active/2026-05-19-foo.vbrief.json",
835
+ "task scope:complete -- xbrief/active/story.xbrief.json --merge-commit abc123 --pr 42",
836
+ ],
837
+ see_also: ["task scope:fail", "task scope:cancel", "#1119 / #845", "#3041"],
794
838
  placeholder: false,
795
839
  },
796
840
  "task scope:fail": {
@@ -930,6 +974,7 @@ export const registryData = {
930
974
  "Read verbs",
931
975
  [
932
976
  "task triage:queue",
977
+ "task triage:evaluate",
933
978
  "task triage:audit",
934
979
  "task triage:show",
935
980
  "task triage:scope",
@@ -1024,6 +1069,9 @@ export const registryData = {
1024
1069
  triage_reconcile: {
1025
1070
  __default__: "task triage:reconcile",
1026
1071
  },
1072
+ triage_evaluate: {
1073
+ __default__: "task triage:evaluate",
1074
+ },
1027
1075
  triage_queue: {
1028
1076
  queue: "task triage:queue",
1029
1077
  show: "task triage:show",
@@ -13,6 +13,7 @@ export * as bulk from "./bulk/index.js";
13
13
  export * as cachePath from "./cache-path.js";
14
14
  export * from "./cache-path.js";
15
15
  export * as classify from "./classify/index.js";
16
+ export * as evaluate from "./evaluate/index.js";
16
17
  export * as help from "./help/index.js";
17
18
  export * as queue from "./queue/index.js";
18
19
  export * as reconcile from "./reconcile/index.js";
@@ -13,6 +13,7 @@ export * as bulk from "./bulk/index.js";
13
13
  export * as cachePath from "./cache-path.js";
14
14
  export * from "./cache-path.js";
15
15
  export * as classify from "./classify/index.js";
16
+ export * as evaluate from "./evaluate/index.js";
16
17
  export * as help from "./help/index.js";
17
18
  export * as queue from "./queue/index.js";
18
19
  export * as reconcile from "./reconcile/index.js";
@@ -1,9 +1,5 @@
1
- export declare class ProjectDefinitionIOError extends Error {
2
- readonly name = "ProjectDefinitionIOError";
3
- }
4
- export declare function projectDefinitionPath(projectRoot: string): string;
5
- export declare function loadProjectDefinitionForMutation(projectRoot: string): [Record<string, unknown>, string];
6
- export declare function atomicWriteProjectDefinition(path: string, data: Record<string, unknown>): void;
1
+ export { atomicWriteProjectDefinition, loadProjectDefinitionForMutation, projectDefinitionPath, } from "../../vbrief-build/project-definition-io.js";
2
+ export { ProjectDefinitionIOError } from "../../vbrief-build/types.js";
7
3
  export declare function recordSubscriptionChange(projectRoot: string, options: {
8
4
  op: string;
9
5
  label?: string | null;
@@ -1,52 +1,20 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import { resolveProjectDefinitionPath } from "../../layout/resolve.js";
2
+ import { appendFileSync, mkdirSync } from "node:fs";
3
+ import { dirname } from "node:path";
6
4
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
7
- import { projectDefinitionMutationLock } from "../../vbrief-build/project-definition-io.js";
5
+ import { withProjectDefinitionMutation } from "../../vbrief-build/project-definition-mutation.js";
8
6
  import { resolveTriageCachePath } from "../cache-path.js";
9
7
  import { SUBSCRIPTION_HISTORY_SCHEMA } from "./constants.js";
10
8
  import { pyStrRepr } from "./python-repr.js";
11
9
  import { utcIso } from "./time.js";
12
- export class ProjectDefinitionIOError extends Error {
13
- name = "ProjectDefinitionIOError";
14
- }
15
- export function projectDefinitionPath(projectRoot) {
16
- return resolveProjectDefinitionPath(projectRoot);
17
- }
18
- export function loadProjectDefinitionForMutation(projectRoot) {
19
- const path = projectDefinitionPath(projectRoot);
20
- if (!existsSync(path)) {
21
- throw new ProjectDefinitionIOError(`PROJECT-DEFINITION not found at ${path}; run task triage:welcome / ` +
22
- "task triage:bootstrap to scaffold one first.");
23
- }
24
- let raw;
25
- try {
26
- raw = readFileSync(path, "utf8");
27
- }
28
- catch (err) {
29
- throw new ProjectDefinitionIOError(`Could not read PROJECT-DEFINITION at ${path}: ${String(err)}`);
30
- }
31
- let data;
32
- try {
33
- data = JSON.parse(raw);
34
- }
35
- catch (err) {
36
- throw new ProjectDefinitionIOError(`PROJECT-DEFINITION at ${path} is not valid JSON: ${String(err)}`);
37
- }
38
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
39
- throw new ProjectDefinitionIOError(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
40
- }
41
- return [data, path];
42
- }
43
- export function atomicWriteProjectDefinition(path, data) {
44
- mkdirSync(dirname(path), { recursive: true });
45
- const payload = `${JSON.stringify(data, null, 2)}\n`;
46
- const tmp = join(tmpdir(), `${randomUUID()}.tmp`);
47
- writeFileSync(tmp, payload, "utf8");
48
- renameSync(tmp, path);
49
- }
10
+ // One canonical PROJECT-DEFINITION identity (#3796). The resolver, loader,
11
+ // error type, and write sink are the shared implementations the mutation
12
+ // capability binds. Module-local duplicates used to resolve the layout path
13
+ // directly -- ignoring the `DEFT_PROJECT_PATH` override the lock honours -- and
14
+ // wrote through an uncontained temp sink, so a caller could lock one artifact
15
+ // and load or persist another.
16
+ export { atomicWriteProjectDefinition, loadProjectDefinitionForMutation, projectDefinitionPath, } from "../../vbrief-build/project-definition-io.js";
17
+ export { ProjectDefinitionIOError } from "../../vbrief-build/types.js";
50
18
  function resolveActor(actor) {
51
19
  if (typeof actor === "string" && actor.trim())
52
20
  return actor;
@@ -133,11 +101,11 @@ export function subscribe(projectRoot, options) {
133
101
  // Serialise the read-modify-write + subscription-history append under the
134
102
  // shared PROJECT-DEFINITION mutation lock so concurrent mutators cannot lose
135
103
  // an update or emit out-of-order audit rows (#1260).
136
- return projectDefinitionMutationLock(projectRoot, () => {
137
- const [data, path] = loadProjectDefinitionForMutation(projectRoot);
104
+ return withProjectDefinitionMutation(projectRoot, (mutation) => {
105
+ const data = mutation.load();
138
106
  const plan = data.plan;
139
107
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
140
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
108
+ throw new Error(`PROJECT-DEFINITION at ${mutation.artifactLabel} has a non-object 'plan' key`);
141
109
  }
142
110
  const planRec = plan;
143
111
  migrateLegacyPolicyKey(planRec);
@@ -145,14 +113,14 @@ export function subscribe(projectRoot, options) {
145
113
  planRec[PLAN_POLICY_KEY] = {};
146
114
  const policy = planRec[PLAN_POLICY_KEY];
147
115
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
148
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan.policy' key`);
116
+ throw new Error(`PROJECT-DEFINITION at ${mutation.artifactLabel} has a non-object 'plan.policy' key`);
149
117
  }
150
118
  const policyRec = policy;
151
119
  if (policyRec.triageScope === undefined)
152
120
  policyRec.triageScope = [];
153
121
  const rules = policyRec.triageScope;
154
122
  if (!Array.isArray(rules)) {
155
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-list 'plan.policy.triageScope'`);
123
+ throw new Error(`PROJECT-DEFINITION at ${mutation.artifactLabel} has a non-list 'plan.policy.triageScope'`);
156
124
  }
157
125
  const before = snapshotRules(rules);
158
126
  let changed;
@@ -168,7 +136,7 @@ export function subscribe(projectRoot, options) {
168
136
  }
169
137
  if (!changed)
170
138
  return [false, message];
171
- atomicWriteProjectDefinition(path, data);
139
+ mutation.persist(data);
172
140
  recordSubscriptionChange(projectRoot, {
173
141
  op: "subscribe",
174
142
  label: options.label ?? null,
@@ -185,11 +153,11 @@ export function addIgnore(projectRoot, label) {
185
153
  throw new Error(`label must be a non-empty string; got ${JSON.stringify(label)}`);
186
154
  // Serialise the read-modify-write + subscription-history append under the
187
155
  // shared PROJECT-DEFINITION mutation lock (#1260).
188
- return projectDefinitionMutationLock(projectRoot, () => {
189
- const [data, path] = loadProjectDefinitionForMutation(projectRoot);
156
+ return withProjectDefinitionMutation(projectRoot, (mutation) => {
157
+ const data = mutation.load();
190
158
  const plan = data.plan;
191
159
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
192
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
160
+ throw new Error(`PROJECT-DEFINITION at ${mutation.artifactLabel} has a non-object 'plan' key`);
193
161
  }
194
162
  const planRec = plan;
195
163
  migrateLegacyPolicyKey(planRec);
@@ -197,14 +165,14 @@ export function addIgnore(projectRoot, label) {
197
165
  planRec[PLAN_POLICY_KEY] = {};
198
166
  const policy = planRec[PLAN_POLICY_KEY];
199
167
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
200
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan.policy' key`);
168
+ throw new Error(`PROJECT-DEFINITION at ${mutation.artifactLabel} has a non-object 'plan.policy' key`);
201
169
  }
202
170
  const policyRec = policy;
203
171
  if (policyRec.triageScopeIgnores === undefined)
204
172
  policyRec.triageScopeIgnores = [];
205
173
  const raw = policyRec.triageScopeIgnores;
206
174
  if (!Array.isArray(raw)) {
207
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-list 'plan.policy.triageScopeIgnores'`);
175
+ throw new Error(`PROJECT-DEFINITION at ${mutation.artifactLabel} has a non-list 'plan.policy.triageScopeIgnores'`);
208
176
  }
209
177
  const before = snapshotRules(raw);
210
178
  for (const entry of raw) {
@@ -216,7 +184,7 @@ export function addIgnore(projectRoot, label) {
216
184
  }
217
185
  }
218
186
  raw.push({ label });
219
- atomicWriteProjectDefinition(path, data);
187
+ mutation.persist(data);
220
188
  const after = snapshotRules(raw);
221
189
  recordSubscriptionChange(projectRoot, {
222
190
  op: "ignore-label",
@@ -1,39 +1,5 @@
1
- import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
- import { dirname, join, resolve } from "node:path";
3
- import { resolveProjectDefinitionPath } from "../../layout/resolve.js";
4
1
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
5
- import { projectDefinitionMutationLock } from "../../vbrief-build/project-definition-io.js";
6
- function loadForMutation(projectRoot) {
7
- const path = resolveProjectDefinitionPath(resolve(projectRoot));
8
- if (!existsSync(path)) {
9
- throw new Error(`PROJECT-DEFINITION not found at ${path}; run task triage:welcome / task triage:bootstrap to scaffold one first.`);
10
- }
11
- let raw;
12
- try {
13
- raw = readFileSync(path, "utf8");
14
- }
15
- catch (err) {
16
- throw new Error(`Could not read PROJECT-DEFINITION at ${path}: ${String(err)}`);
17
- }
18
- let data;
19
- try {
20
- data = JSON.parse(raw);
21
- }
22
- catch (err) {
23
- throw new Error(`PROJECT-DEFINITION at ${path} is not valid JSON: ${String(err)}`);
24
- }
25
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
26
- throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
27
- }
28
- return [data, path];
29
- }
30
- function atomicWrite(path, data) {
31
- const dir = dirname(path);
32
- const payload = `${JSON.stringify(data, null, 2)}\n`;
33
- const tmp = join(dir, `.${Date.now()}.tmp`);
34
- writeFileSync(tmp, payload, "utf8");
35
- renameSync(tmp, path);
36
- }
2
+ import { withProjectDefinitionMutation } from "../../vbrief-build/project-definition-mutation.js";
37
3
  /** Append a label/milestone ignore entry — mirrors Python `add_ignore`. */
38
4
  export function addIgnore(projectRoot, options) {
39
5
  const hasLabel = options.label !== undefined;
@@ -48,11 +14,11 @@ export function addIgnore(projectRoot, options) {
48
14
  }
49
15
  // Serialise the read-modify-write under the shared PROJECT-DEFINITION
50
16
  // mutation lock so concurrent mutators cannot lose an update (#1260).
51
- return projectDefinitionMutationLock(projectRoot, () => {
52
- const [data, path] = loadForMutation(projectRoot);
17
+ return withProjectDefinitionMutation(projectRoot, (mutation) => {
18
+ const data = mutation.load();
53
19
  const plan = data.plan;
54
20
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
55
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
21
+ throw new Error(`PROJECT-DEFINITION at ${mutation.artifactLabel} has a non-object 'plan' key`);
56
22
  }
57
23
  const planRec = plan;
58
24
  migrateLegacyPolicyKey(planRec);
@@ -74,7 +40,7 @@ export function addIgnore(projectRoot, options) {
74
40
  }
75
41
  }
76
42
  raw.push({ [key]: value });
77
- atomicWrite(path, data);
43
+ mutation.persist(data);
78
44
  return { changed: true, message: `added ignore (${key}=${value})` };
79
45
  });
80
46
  }