@deftai/directive-core 0.107.0 → 0.108.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/dist/architecture/sor-preflight.js +2 -0
  2. package/dist/check/cached-orchestrator.js +28 -3
  3. package/dist/check/named-cause.js +18 -3
  4. package/dist/codebase/provider.js +7 -1
  5. package/dist/delivery-attempt/handoff.js +1 -1
  6. package/dist/design-critique/citation-grammar.d.ts +53 -0
  7. package/dist/design-critique/citation-grammar.js +303 -0
  8. package/dist/design-critique/completed-arc-record.d.ts +62 -0
  9. package/dist/design-critique/completed-arc-record.js +232 -0
  10. package/dist/freshness/bind.d.ts +32 -3
  11. package/dist/freshness/bind.js +58 -12
  12. package/dist/hooks/classify/host-session-identity.d.ts +76 -0
  13. package/dist/hooks/classify/host-session-identity.js +470 -0
  14. package/dist/hooks/classify/index.d.ts +3 -2
  15. package/dist/hooks/classify/index.js +3 -2
  16. package/dist/hooks/classify/paths.d.ts +7 -0
  17. package/dist/hooks/classify/paths.js +35 -0
  18. package/dist/hooks/classify/stdin.d.ts +5 -0
  19. package/dist/hooks/classify/stdin.js +27 -0
  20. package/dist/hooks/dispatcher.d.ts +52 -1
  21. package/dist/hooks/dispatcher.js +538 -45
  22. package/dist/hooks/fixtures/cases.d.ts +16 -2
  23. package/dist/hooks/fixtures/cases.js +175 -0
  24. package/dist/hooks/scope.js +3 -1
  25. package/dist/init-deposit/agent-hooks.d.ts +2 -1
  26. package/dist/init-deposit/agent-hooks.js +8 -1
  27. package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
  28. package/dist/init-deposit/hook-runtime-travel.js +269 -0
  29. package/dist/intake/clause-derivation.js +9 -3
  30. package/dist/intake/issue-ingest.js +62 -2
  31. package/dist/intake/platform-capabilities.d.ts +9 -2
  32. package/dist/intake/platform-capabilities.js +38 -9
  33. package/dist/orphan-active/evaluate.d.ts +25 -0
  34. package/dist/orphan-active/evaluate.js +193 -128
  35. package/dist/orphan-active/index.d.ts +1 -0
  36. package/dist/orphan-active/index.js +1 -0
  37. package/dist/orphan-active/issue-state.d.ts +121 -0
  38. package/dist/orphan-active/issue-state.js +278 -0
  39. package/dist/platform/cursor-managed-runtime.d.ts +95 -0
  40. package/dist/platform/cursor-managed-runtime.js +241 -0
  41. package/dist/platform/index.d.ts +1 -0
  42. package/dist/platform/index.js +1 -0
  43. package/dist/platform/platform-capabilities.d.ts +23 -1
  44. package/dist/platform/platform-capabilities.js +55 -15
  45. package/dist/policy/ceremony-dial.js +5 -9
  46. package/dist/policy/host-hooks.js +4 -9
  47. package/dist/policy/org-force-on-migration.js +5 -10
  48. package/dist/policy/plan-extensions.d.ts +14 -2
  49. package/dist/policy/plan-extensions.js +24 -3
  50. package/dist/policy/product-signal.js +5 -10
  51. package/dist/policy/require-human-merge.js +5 -9
  52. package/dist/policy/resolve.js +77 -16
  53. package/dist/policy/value-feedback.js +9 -18
  54. package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
  55. package/dist/pr-closeout-attestable/evaluate.js +306 -0
  56. package/dist/pr-closeout-attestable/index.d.ts +2 -0
  57. package/dist/pr-closeout-attestable/index.js +2 -0
  58. package/dist/pr-protected-issues/gh.d.ts +6 -2
  59. package/dist/pr-protected-issues/gh.js +7 -3
  60. package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
  61. package/dist/pr-wait-mergeable/cascade.js +21 -1
  62. package/dist/pr-wait-mergeable/types.d.ts +2 -0
  63. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  64. package/dist/pr-wait-mergeable/wrappers.js +11 -0
  65. package/dist/preflight/evaluate.d.ts +15 -0
  66. package/dist/preflight/evaluate.js +29 -3
  67. package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
  68. package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
  69. package/dist/product-first-done-gate/evaluate.js +17 -7
  70. package/dist/release/cli-drift-report.d.ts +68 -0
  71. package/dist/release/cli-drift-report.js +189 -0
  72. package/dist/release/index.d.ts +1 -0
  73. package/dist/release/index.js +1 -0
  74. package/dist/release/issue-state-fetch.d.ts +5 -3
  75. package/dist/release/issue-state-fetch.js +86 -21
  76. package/dist/release/native-steps.js +1 -0
  77. package/dist/release/pipeline.js +19 -0
  78. package/dist/release/types.d.ts +14 -0
  79. package/dist/render/constants.d.ts +4 -0
  80. package/dist/render/constants.js +11 -8
  81. package/dist/render/export-spec.js +31 -5
  82. package/dist/render/index.d.ts +1 -1
  83. package/dist/render/index.js +1 -1
  84. package/dist/render/prd-render.d.ts +4 -1
  85. package/dist/render/prd-render.js +82 -23
  86. package/dist/render/project-render.js +14 -7
  87. package/dist/render/scope-outlook.d.ts +2 -0
  88. package/dist/render/scope-outlook.js +3 -0
  89. package/dist/render/spec-render.js +2 -2
  90. package/dist/resolution/package-manager.d.ts +33 -4
  91. package/dist/resolution/package-manager.js +210 -9
  92. package/dist/review-monitor/constants.d.ts +7 -0
  93. package/dist/review-monitor/constants.js +7 -0
  94. package/dist/review-monitor/github-lease.d.ts +98 -1
  95. package/dist/review-monitor/github-lease.js +182 -1
  96. package/dist/review-monitor/lease-comment.d.ts +36 -0
  97. package/dist/review-monitor/lease-comment.js +103 -2
  98. package/dist/scm/call.js +12 -3
  99. package/dist/scm/design-critique-chip.d.ts +1 -0
  100. package/dist/scm/design-critique-chip.js +22 -6
  101. package/dist/scm/gh-rest.d.ts +9 -1
  102. package/dist/scm/gh-rest.js +103 -0
  103. package/dist/scm/readiness.d.ts +2 -0
  104. package/dist/scm/readiness.js +28 -4
  105. package/dist/scope/project-definition-sync.js +14 -7
  106. package/dist/session/git.d.ts +24 -0
  107. package/dist/session/git.js +138 -2
  108. package/dist/session/occupancy.d.ts +238 -12
  109. package/dist/session/occupancy.js +815 -29
  110. package/dist/session/ritual-sentinel.d.ts +5 -0
  111. package/dist/session/ritual-sentinel.js +5 -0
  112. package/dist/session/session-ready.d.ts +5 -1
  113. package/dist/session/session-ready.js +98 -8
  114. package/dist/session/session-start.d.ts +5 -1
  115. package/dist/session/session-start.js +32 -17
  116. package/dist/session/toolchain-preflight.d.ts +13 -3
  117. package/dist/session/toolchain-preflight.js +93 -18
  118. package/dist/session/verify-session-ritual.d.ts +11 -0
  119. package/dist/session/verify-session-ritual.js +60 -15
  120. package/dist/slice/constants.d.ts +1 -1
  121. package/dist/spec-authority/constants.d.ts +10 -2
  122. package/dist/spec-authority/constants.js +53 -8
  123. package/dist/spec-authority/resolver.d.ts +3 -0
  124. package/dist/spec-authority/resolver.js +55 -7
  125. package/dist/subprocess/max-buffer.d.ts +14 -0
  126. package/dist/subprocess/max-buffer.js +15 -0
  127. package/dist/swarm/complete-cohort.d.ts +2 -0
  128. package/dist/swarm/complete-cohort.js +21 -13
  129. package/dist/swarm/index.d.ts +1 -0
  130. package/dist/swarm/index.js +1 -0
  131. package/dist/swarm/launch-cli.js +53 -33
  132. package/dist/swarm/launch.d.ts +8 -0
  133. package/dist/swarm/launch.js +63 -43
  134. package/dist/swarm/pre-dispatch-cli.js +2 -1
  135. package/dist/swarm/pre-dispatch.js +10 -1
  136. package/dist/swarm/subagent-status-dir.d.ts +27 -0
  137. package/dist/swarm/subagent-status-dir.js +42 -0
  138. package/dist/swarm/worktrees.d.ts +17 -3
  139. package/dist/swarm/worktrees.js +48 -4
  140. package/dist/triage/scope/mutations-core.d.ts +2 -6
  141. package/dist/triage/scope/mutations-core.js +23 -55
  142. package/dist/triage/scope-drift/add-ignore.js +5 -39
  143. package/dist/triage/subscribe/index.d.ts +2 -6
  144. package/dist/triage/subscribe/index.js +15 -65
  145. package/dist/triage/welcome/writers.js +15 -74
  146. package/dist/vbrief-build/index.d.ts +2 -1
  147. package/dist/vbrief-build/index.js +2 -1
  148. package/dist/vbrief-build/parity-scenarios.js +5 -5
  149. package/dist/vbrief-build/project-definition-io.d.ts +63 -3
  150. package/dist/vbrief-build/project-definition-io.js +357 -31
  151. package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
  152. package/dist/vbrief-build/project-definition-mutation.js +43 -0
  153. package/dist/vbrief-validate/conformance.d.ts +1 -0
  154. package/dist/vbrief-validate/conformance.js +85 -8
  155. package/dist/vbrief-validate/main.js +2 -0
  156. package/dist/vbrief-validate/precutover.js +5 -10
  157. package/dist/verify-ac/clauses.d.ts +62 -2
  158. package/dist/verify-ac/clauses.js +183 -111
  159. package/dist/verify-ac/index.d.ts +1 -1
  160. package/dist/verify-ac/index.js +1 -1
  161. package/dist/verify-env/node-runtime.d.ts +8 -4
  162. package/dist/verify-env/node-runtime.js +9 -6
  163. package/dist/verify-env/toolchain-check.d.ts +35 -3
  164. package/dist/verify-env/toolchain-check.js +155 -36
  165. package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
  166. package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
  167. package/package.json +7 -3
@@ -2,6 +2,7 @@ import { mkdirSync, readFileSync } from "node:fs";
2
2
  import { isAbsolute, resolve as pathResolve } from "node:path";
3
3
  import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
4
4
  import { C3_FIELDS } from "./constants.js";
5
+ import { ensureSubagentStatusDir } from "./subagent-status-dir.js";
5
6
  import { runText } from "./subprocess.js";
6
7
  export class WorktreeMapError extends Error {
7
8
  name = "WorktreeMapError";
@@ -24,6 +25,9 @@ export class WorktreeMapConfigError extends Error {
24
25
  export class WorktreePathEscapeError extends WorktreeMapError {
25
26
  name = "WorktreePathEscapeError";
26
27
  }
28
+ export class WorktreeRevisionMismatchError extends WorktreeMapError {
29
+ name = "WorktreeRevisionMismatchError";
30
+ }
27
31
  export const defaultGitRunner = (args, cwd) => runText(["git", ...args], { cwd });
28
32
  function resolvePath(raw, repoRoot) {
29
33
  const candidate = isAbsolute(raw) ? raw : pathResolve(repoRoot, raw);
@@ -44,14 +48,15 @@ function assertWorktreePathContained(repoRoot, worktreePath) {
44
48
  export function compareKey(pathStr) {
45
49
  return pathStr.replace(/\\/g, "/").toLowerCase();
46
50
  }
47
- /** Parse `git worktree list --porcelain` into `{compareKey: branch|null}`. */
51
+ /** Parse `git worktree list --porcelain` into `{compareKey: {branch, head}}`. */
48
52
  export function parseWorktreePorcelain(text) {
49
53
  const registered = new Map();
50
54
  let currentPath = null;
51
55
  let currentBranch = null;
56
+ let currentHead = null;
52
57
  const flush = () => {
53
58
  if (currentPath !== null) {
54
- registered.set(compareKey(currentPath), currentBranch);
59
+ registered.set(compareKey(currentPath), { branch: currentBranch, head: currentHead });
55
60
  }
56
61
  };
57
62
  for (const line of text.split("\n")) {
@@ -59,6 +64,11 @@ export function parseWorktreePorcelain(text) {
59
64
  flush();
60
65
  currentPath = pathResolve(line.slice("worktree ".length).trim());
61
66
  currentBranch = null;
67
+ currentHead = null;
68
+ }
69
+ else if (line.startsWith("HEAD ")) {
70
+ const oid = line.slice("HEAD ".length).trim().toLowerCase();
71
+ currentHead = oid.length > 0 ? oid : null;
62
72
  }
63
73
  else if (line.startsWith("branch ")) {
64
74
  const ref = line.slice("branch ".length).trim();
@@ -82,6 +92,24 @@ function gitWorktreeList(repoRoot, git) {
82
92
  }
83
93
  return parseWorktreePorcelain(proc.stdout);
84
94
  }
95
+ function resolveCommitOid(repoRoot, revision, git) {
96
+ let proc;
97
+ try {
98
+ proc = git(["rev-parse", "--verify", "--end-of-options", `${revision}^{commit}`], repoRoot);
99
+ }
100
+ catch (exc) {
101
+ throw new WorktreeMapConfigError(`could not resolve base '${revision}' to a commit OID in ${repoRoot}: ${String(exc)}`);
102
+ }
103
+ if (proc.returncode !== 0) {
104
+ throw new WorktreeMapConfigError(`could not resolve base '${revision}' to a commit OID in ${repoRoot} ` +
105
+ `(rc=${proc.returncode}): ${proc.stderr.trim() || "<no stderr>"}`);
106
+ }
107
+ const oid = (proc.stdout.trim().split(/\s+/)[0] ?? "").toLowerCase();
108
+ if (!/^[0-9a-f]{40,64}$/.test(oid)) {
109
+ throw new WorktreeMapConfigError(`git rev-parse for base '${revision}' did not return a commit OID: ${JSON.stringify(oid)}`);
110
+ }
111
+ return oid;
112
+ }
85
113
  function createWorktree(repoRoot, worktreePath, baseBranch, git) {
86
114
  mkdirSync(pathResolve(worktreePath, ".."), { recursive: true });
87
115
  let proc;
@@ -96,7 +124,13 @@ function createWorktree(repoRoot, worktreePath, baseBranch, git) {
96
124
  `(rc=${proc.returncode}): ${proc.stderr.trim() || "<no stderr>"}`);
97
125
  }
98
126
  }
99
- /** Resolve a story-to-worktree mapping into normalized C3 records (frozen contract). */
127
+ /**
128
+ * Resolve a story-to-worktree mapping into normalized C3 records (frozen contract).
129
+ *
130
+ * Registered paths are reused only when their porcelain HEAD OID matches the
131
+ * requested base OID. That comparison is a snapshot at resolution time — not a
132
+ * pin on the worker's start revision (`swarm:launch` emits a manifest and stops).
133
+ */
100
134
  export function resolveWorktreeMap(mapping, baseBranch, createMissing = true, options = {}) {
101
135
  if (!Array.isArray(mapping)) {
102
136
  throw new WorktreeMapConfigError(`worktree map must be a list of records, got ${typeof mapping}`);
@@ -154,14 +188,24 @@ export function resolveWorktreeMap(mapping, baseBranch, createMissing = true, op
154
188
  });
155
189
  }
156
190
  const registered = gitWorktreeList(root, git);
191
+ const requestedOid = resolveCommitOid(root, trimmedBase, git);
157
192
  for (const entry of resolved) {
158
- if (registered.has(entry._key)) {
193
+ const existing = registered.get(entry._key);
194
+ if (existing !== undefined) {
195
+ const actualOid = existing.head ?? "";
196
+ if (actualOid !== requestedOid) {
197
+ throw new WorktreeRevisionMismatchError(`registered worktree '${entry.worktree_path}' HEAD OID is ${actualOid || "(missing)"} ` +
198
+ `but requested base '${trimmedBase}' resolves to ${requestedOid}; ` +
199
+ `this is a snapshot check at resolution time, not a pin on the worker's start revision`);
200
+ }
201
+ ensureSubagentStatusDir(entry._abs);
159
202
  continue;
160
203
  }
161
204
  if (!createMissing) {
162
205
  throw new MissingWorktreeError(`story '${entry.story_id}' maps to '${entry.worktree_path}' which is not a registered git worktree and create_missing is disabled`);
163
206
  }
164
207
  createWorktree(root, entry._abs, trimmedBase, git);
208
+ ensureSubagentStatusDir(entry._abs);
165
209
  }
166
210
  return resolved.map(({ story_id, worktree_path, base_branch }) => ({
167
211
  story_id,
@@ -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
  }
@@ -3,12 +3,9 @@ export declare const SUBSCRIPTION_HISTORY_SCHEMA = "deft.triage.subscription-cha
3
3
  export declare const PROJECT_DEFINITION_REL_PATH = "xbrief/PROJECT-DEFINITION.xbrief.json";
4
4
  /** Python ``!r``-style quoting for parity with triage_subscribe.py messages. */
5
5
  export declare function pyRepr(value: string): string;
6
- export declare class ProjectDefinitionIOError extends Error {
7
- constructor(message: string);
8
- }
6
+ export { atomicWriteProjectDefinition, loadProjectDefinitionForMutation, projectDefinitionPath, } from "../../vbrief-build/project-definition-io.js";
7
+ export { ProjectDefinitionIOError } from "../../vbrief-build/types.js";
9
8
  type TriageRule = Record<string, unknown>;
10
- export declare function loadProjectDefinitionForMutation(projectRoot: string): [Record<string, unknown>, string];
11
- export declare function atomicWriteProjectDefinition(path: string, data: Record<string, unknown>): void;
12
9
  export declare function recordSubscriptionChange(projectRoot: string, options: {
13
10
  op: string;
14
11
  label?: string | null;
@@ -34,5 +31,4 @@ export declare function unsubscribe(projectRoot: string, options?: {
34
31
  actor?: string | null;
35
32
  }): [boolean, string];
36
33
  export declare const RECONCILE_HINT: string;
37
- export {};
38
34
  //# sourceMappingURL=index.d.ts.map
@@ -1,9 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
3
- import { basename, join } from "node:path";
4
- import { resolveProjectDefinitionPath } from "../../layout/resolve.js";
2
+ import { appendFileSync, mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
5
4
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
6
- import { projectDefinitionMutationLock } from "../../vbrief-build/project-definition-io.js";
5
+ import { withProjectDefinitionMutation } from "../../vbrief-build/project-definition-mutation.js";
7
6
  import { resolveTriageCachePath } from "../cache-path.js";
8
7
  export const SUBSCRIPTION_HISTORY_REL_PATH = "xbrief/.triage-cache/subscription-history.jsonl";
9
8
  export const SUBSCRIPTION_HISTORY_SCHEMA = "deft.triage.subscription-change.v1";
@@ -12,61 +11,12 @@ export const PROJECT_DEFINITION_REL_PATH = "xbrief/PROJECT-DEFINITION.xbrief.jso
12
11
  export function pyRepr(value) {
13
12
  return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
14
13
  }
15
- export class ProjectDefinitionIOError extends Error {
16
- constructor(message) {
17
- super(message);
18
- this.name = "ProjectDefinitionIOError";
19
- }
20
- }
21
- function projectDefinitionPath(projectRoot) {
22
- return resolveProjectDefinitionPath(projectRoot);
23
- }
24
- export function loadProjectDefinitionForMutation(projectRoot) {
25
- const path = projectDefinitionPath(projectRoot);
26
- if (!existsSync(path)) {
27
- throw new ProjectDefinitionIOError(`PROJECT-DEFINITION not found at ${path}; run task triage:welcome / ` +
28
- "task triage:bootstrap to scaffold one first.");
29
- }
30
- let raw;
31
- try {
32
- raw = readFileSync(path, { encoding: "utf8" });
33
- }
34
- catch (exc) {
35
- throw new ProjectDefinitionIOError(`Could not read PROJECT-DEFINITION at ${path}: ${String(exc)}`);
36
- }
37
- let data;
38
- try {
39
- data = JSON.parse(raw);
40
- }
41
- catch (exc) {
42
- throw new ProjectDefinitionIOError(`PROJECT-DEFINITION at ${path} is not valid JSON: ${String(exc)}`);
43
- }
44
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
45
- throw new ProjectDefinitionIOError(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
46
- }
47
- return [data, path];
48
- }
49
- export function atomicWriteProjectDefinition(path, data) {
50
- const parent = join(path, "..");
51
- mkdirSync(parent, { recursive: true });
52
- const payload = JSON.stringify(data, null, 2);
53
- const tmpName = join(parent, `${basename(path)}.${process.pid}.${Date.now()}.tmp`);
54
- try {
55
- writeFileSync(tmpName, payload.endsWith("\n") ? payload : `${payload}\n`, {
56
- encoding: "utf8",
57
- });
58
- renameSync(tmpName, path);
59
- }
60
- catch (exc) {
61
- try {
62
- unlinkSync(tmpName);
63
- }
64
- catch {
65
- // ignore
66
- }
67
- throw exc;
68
- }
69
- }
14
+ // One canonical PROJECT-DEFINITION identity (#3796) -- see the same note in
15
+ // `triage/scope/mutations-core.ts`. These were module-local duplicates that
16
+ // resolved the layout path directly instead of honouring the override the lock
17
+ // uses, and wrote through their own temp sink.
18
+ export { atomicWriteProjectDefinition, loadProjectDefinitionForMutation, projectDefinitionPath, } from "../../vbrief-build/project-definition-io.js";
19
+ export { ProjectDefinitionIOError } from "../../vbrief-build/types.js";
70
20
  function snapshotRules(rules) {
71
21
  return JSON.parse(JSON.stringify(rules));
72
22
  }
@@ -251,10 +201,10 @@ function mutate(projectRoot, options) {
251
201
  // Serialise the read-modify-write + subscription-history append under the
252
202
  // shared PROJECT-DEFINITION mutation lock so concurrent mutators cannot lose
253
203
  // an update or emit out-of-order audit rows (#1260).
254
- return projectDefinitionMutationLock(projectRoot, () => {
255
- const [data, path] = loadProjectDefinitionForMutation(projectRoot);
204
+ return withProjectDefinitionMutation(projectRoot, (mutation) => {
205
+ const data = mutation.load();
256
206
  if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
257
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
207
+ throw new Error(`PROJECT-DEFINITION at ${mutation.artifactLabel} has a non-object 'plan' key`);
258
208
  }
259
209
  const plan = data.plan;
260
210
  migrateLegacyPolicyKey(plan);
@@ -266,7 +216,7 @@ function mutate(projectRoot, options) {
266
216
  plan[PLAN_POLICY_KEY] = {};
267
217
  }
268
218
  else {
269
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan.policy' key`);
219
+ throw new Error(`PROJECT-DEFINITION at ${mutation.artifactLabel} has a non-object 'plan.policy' key`);
270
220
  }
271
221
  }
272
222
  const policy = plan[PLAN_POLICY_KEY];
@@ -275,7 +225,7 @@ function mutate(projectRoot, options) {
275
225
  policy.triageScope = [];
276
226
  }
277
227
  else {
278
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-list 'plan.policy.triageScope'`);
228
+ throw new Error(`PROJECT-DEFINITION at ${mutation.artifactLabel} has a non-list 'plan.policy.triageScope'`);
279
229
  }
280
230
  }
281
231
  const rules = policy.triageScope;
@@ -291,7 +241,7 @@ function mutate(projectRoot, options) {
291
241
  if (!changed) {
292
242
  return [false, message];
293
243
  }
294
- atomicWriteProjectDefinition(path, data);
244
+ mutation.persist(data);
295
245
  const after = snapshotRules(rules);
296
246
  recordSubscriptionChange(projectRoot, {
297
247
  op: options.op,
@@ -1,15 +1,11 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, } from "node:fs";
2
- import { dirname, join, resolve } from "node:path";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
3
  import { containedWrite } from "../../fs/contained-write.js";
4
- import { assertWriteTargetSafe } from "../../fs/projection-containment.js";
5
- import { hasArtifactSuffix, resolveLifecycleFolder, resolveProjectDefinitionPath, } from "../../layout/resolve.js";
4
+ import { hasArtifactSuffix, resolveLifecycleFolder } from "../../layout/resolve.js";
6
5
  import { migrateLegacyPolicyKey, PLAN_ONBOARDING_KEY, PLAN_POLICY_KEY, } from "../../policy/plan-extensions.js";
7
6
  import { stampChangedToken } from "../../policy/resolve.js";
8
- import { projectDefinitionMutationLock } from "../../vbrief-build/project-definition-io.js";
7
+ import { withProjectDefinitionMutation } from "../../vbrief-build/project-definition-mutation.js";
9
8
  import { AUDIT_LOG_REL_PATH, DEFAULT_RELIEF_AGE_DAYS, DEFAULT_WIP_CAP, SUBSCRIPTION_PRESETS, WELCOME_AUDIT_TAG, } from "./constants.js";
10
- function projectDefinitionPath(projectRoot) {
11
- return resolveProjectDefinitionPath(projectRoot);
12
- }
13
9
  function utcIso() {
14
10
  return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
15
11
  }
@@ -38,51 +34,10 @@ export function appendAuditEntry(projectRoot, entry, changed = true) {
38
34
  });
39
35
  return logPath;
40
36
  }
41
- /**
42
- * Atomically write PROJECT-DEFINITION JSON under projectRoot containment (#3077).
43
- * Containment root is projectRoot (not dirname(path)) so a force-added `xbrief/`
44
- * directory symlink fails closed before temp+rename — same class as #3042.
45
- */
46
- function atomicWrite(projectRoot, path, data) {
47
- const root = resolve(projectRoot);
48
- const targetAbs = resolve(path);
49
- // Refuse leaf/parent symlinks and out-of-root targets before temp+rename.
50
- assertWriteTargetSafe(root, targetAbs);
51
- const dir = dirname(targetAbs);
52
- const payload = `${JSON.stringify(data, null, 2)}\n`;
53
- const tmpName = `.${Date.now()}.tmp`;
54
- const tmp = join(dir, tmpName);
55
- try {
56
- // #2980 wave D / #3077: product write routes through containedWrite under projectRoot.
57
- containedWrite({
58
- root,
59
- target: tmp,
60
- data: payload,
61
- mode: "create",
62
- });
63
- renameSync(tmp, targetAbs);
64
- }
65
- catch (err) {
66
- try {
67
- rmSync(tmp, { force: true });
68
- }
69
- catch {
70
- /* best-effort cleanup */
71
- }
72
- throw err;
73
- }
74
- }
75
37
  export function writeTriageScope(projectRoot, rules, options = { presetLabel: "custom" }) {
76
38
  // Serialise read-modify-write + audit append under the shared lock (#1260).
77
- return projectDefinitionMutationLock(projectRoot, () => {
78
- const path = projectDefinitionPath(projectRoot);
79
- if (!existsSync(path))
80
- throw new Error(`PROJECT-DEFINITION not found at ${path}`);
81
- const parsed = JSON.parse(readFileSync(path, "utf8"));
82
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
83
- throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
84
- }
85
- const data = parsed;
39
+ return withProjectDefinitionMutation(projectRoot, (mutation) => {
40
+ const data = mutation.load();
86
41
  const plan = data.plan;
87
42
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
88
43
  throw new Error("PROJECT-DEFINITION 'plan' is not an object");
@@ -97,7 +52,7 @@ export function writeTriageScope(projectRoot, rules, options = { presetLabel: "c
97
52
  const policyRec = policy;
98
53
  const previous = policyRec.triageScope;
99
54
  policyRec.triageScope = rules;
100
- atomicWrite(projectRoot, path, data);
55
+ mutation.persist(data);
101
56
  const changed = JSON.stringify(previous) !== JSON.stringify(rules);
102
57
  const actor = options.actor ?? WELCOME_AUDIT_TAG;
103
58
  const auditEntry = [
@@ -117,15 +72,8 @@ export function writeTriageScope(projectRoot, rules, options = { presetLabel: "c
117
72
  * onboarding decision and MUST be representable without writing the value field.
118
73
  */
119
74
  export function writeWipCapDecision(projectRoot, options = {}) {
120
- return projectDefinitionMutationLock(projectRoot, () => {
121
- const path = projectDefinitionPath(projectRoot);
122
- if (!existsSync(path))
123
- throw new Error(`PROJECT-DEFINITION not found at ${path}`);
124
- const parsed = JSON.parse(readFileSync(path, "utf8"));
125
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
126
- throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
127
- }
128
- const data = parsed;
75
+ return withProjectDefinitionMutation(projectRoot, (mutation) => {
76
+ const data = mutation.load();
129
77
  const plan = data.plan;
130
78
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
131
79
  throw new Error("PROJECT-DEFINITION 'plan' is not an object");
@@ -149,7 +97,7 @@ export function writeWipCapDecision(projectRoot, options = {}) {
149
97
  next.value = options.value;
150
98
  }
151
99
  planRec[PLAN_ONBOARDING_KEY] = next;
152
- atomicWrite(projectRoot, path, data);
100
+ mutation.persist(data);
153
101
  const changed = !alreadyDecided || previousRec.acceptedDefault !== acceptedDefault;
154
102
  const auditEntry = [
155
103
  `actor=${actor}`,
@@ -167,15 +115,8 @@ export function writeWipCap(projectRoot, wipCap, options = {}) {
167
115
  throw new Error(`wipCap must be a positive int, got ${JSON.stringify(wipCap)}`);
168
116
  }
169
117
  // Serialise read-modify-write + audit append under the shared lock (#1260).
170
- return projectDefinitionMutationLock(projectRoot, () => {
171
- const path = projectDefinitionPath(projectRoot);
172
- if (!existsSync(path))
173
- throw new Error(`PROJECT-DEFINITION not found at ${path}`);
174
- const parsed = JSON.parse(readFileSync(path, "utf8"));
175
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
176
- throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
177
- }
178
- const data = parsed;
118
+ return withProjectDefinitionMutation(projectRoot, (mutation) => {
119
+ const data = mutation.load();
179
120
  const plan = data.plan;
180
121
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
181
122
  throw new Error("PROJECT-DEFINITION 'plan' is not an object");
@@ -206,7 +147,7 @@ export function writeWipCap(projectRoot, wipCap, options = {}) {
206
147
  }
207
148
  planRec[PLAN_ONBOARDING_KEY] = onboardingRecord;
208
149
  if (previous === undefined && wipCap === DEFAULT_WIP_CAP) {
209
- atomicWrite(projectRoot, path, data);
150
+ mutation.persist(data);
210
151
  const auditEntry = `actor=${actor} field=${PLAN_ONBOARDING_KEY}.wipCapDecided value=true ` +
211
152
  `acceptedDefault=true changed=true note=default-no-materialize`;
212
153
  appendAuditEntry(projectRoot, auditEntry);
@@ -214,14 +155,14 @@ export function writeWipCap(projectRoot, wipCap, options = {}) {
214
155
  }
215
156
  if (previous !== undefined && wipCap === DEFAULT_WIP_CAP) {
216
157
  delete policyRec.wipCap;
217
- atomicWrite(projectRoot, path, data);
158
+ mutation.persist(data);
218
159
  const auditEntry = `actor=${actor} field=plan.policy.wipCap action=cleared-to-default value=${wipCap} ` +
219
160
  `previous=${JSON.stringify(previous)} changed=true`;
220
161
  appendAuditEntry(projectRoot, auditEntry);
221
162
  return [true, auditEntry];
222
163
  }
223
164
  policyRec.wipCap = wipCap;
224
- atomicWrite(projectRoot, path, data);
165
+ mutation.persist(data);
225
166
  const changed = previous !== wipCap;
226
167
  const auditEntry = `actor=${actor} field=plan.policy.wipCap value=${wipCap} previous=${JSON.stringify(previous)} ` +
227
168
  `changed=${changed ? "true" : "false"}`;
@@ -3,7 +3,8 @@ export { DEFAULT_STATUS_FOR_FOLDER, DEPRECATION_SENTINEL, EMITTED_VBRIEF_VERSION
3
3
  export { pythonJsonPretty } from "./json.js";
4
4
  export { cmdVbriefBuild, run, usage } from "./main.js";
5
5
  export { PARITY_SCENARIO_NAMES, renderScenarioOutput, runParityScenario, SAMPLE_PROJECT_MD, SAMPLE_ROADMAP_MD, SAMPLE_SPEC_MD, SAMPLE_SPEC_VBRIEF, } from "./parity-scenarios.js";
6
- export { atomicWriteProjectDefinition, loadProjectDefinitionForMutation, projectDefinitionMutationLock, projectDefinitionPath, } from "./project-definition-io.js";
6
+ export { atomicWriteProjectDefinition, CONFIGURED_PROJECT_DEFINITION_LABEL, loadProjectDefinitionForMutation, type MutationLockDeps, ProjectDefinitionLockError, parseProjectDefinitionAt, projectDefinitionArtifactLabel, projectDefinitionMutationLock, projectDefinitionPath, } from "./project-definition-io.js";
7
+ export { type ProjectDefinitionMutation, withProjectDefinitionMutation, } from "./project-definition-mutation.js";
7
8
  export { loadProjectInvariants, parseProjectInvariantsField } from "./project-invariants-io.js";
8
9
  export { buildScopeVbriefFromReconciled, defaultStatusForFolder, folderForStatus, migrationTimestamp, planStatusMatchesFolder, } from "./routing.js";
9
10
  export { deriveOverviewNarrative, extractTechStack, firstProseParagraph, parseRoadmapItems, resolveRepoUrl, } from "./sources.js";
@@ -3,7 +3,8 @@ export { DEFAULT_STATUS_FOR_FOLDER, DEPRECATION_SENTINEL, EMITTED_VBRIEF_VERSION
3
3
  export { pythonJsonPretty } from "./json.js";
4
4
  export { cmdVbriefBuild, run, usage } from "./main.js";
5
5
  export { PARITY_SCENARIO_NAMES, renderScenarioOutput, runParityScenario, SAMPLE_PROJECT_MD, SAMPLE_ROADMAP_MD, SAMPLE_SPEC_MD, SAMPLE_SPEC_VBRIEF, } from "./parity-scenarios.js";
6
- export { atomicWriteProjectDefinition, loadProjectDefinitionForMutation, projectDefinitionMutationLock, projectDefinitionPath, } from "./project-definition-io.js";
6
+ export { atomicWriteProjectDefinition, CONFIGURED_PROJECT_DEFINITION_LABEL, loadProjectDefinitionForMutation, ProjectDefinitionLockError, parseProjectDefinitionAt, projectDefinitionArtifactLabel, projectDefinitionMutationLock, projectDefinitionPath, } from "./project-definition-io.js";
7
+ export { withProjectDefinitionMutation, } from "./project-definition-mutation.js";
7
8
  export { loadProjectInvariants, parseProjectInvariantsField } from "./project-invariants-io.js";
8
9
  export { buildScopeVbriefFromReconciled, defaultStatusForFolder, folderForStatus, migrationTimestamp, planStatusMatchesFolder, } from "./routing.js";
9
10
  export { deriveOverviewNarrative, extractTechStack, firstProseParagraph, parseRoadmapItems, resolveRepoUrl, } from "./sources.js";