@deftai/directive-core 0.66.2 → 0.68.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 (147) hide show
  1. package/dist/agents-md-advisory/evaluate.d.ts +44 -0
  2. package/dist/agents-md-advisory/evaluate.js +115 -0
  3. package/dist/agents-md-advisory/index.d.ts +2 -0
  4. package/dist/agents-md-advisory/index.js +2 -0
  5. package/dist/agents-md-budget/evaluate.d.ts +38 -0
  6. package/dist/agents-md-budget/evaluate.js +152 -0
  7. package/dist/agents-md-budget/index.d.ts +2 -0
  8. package/dist/agents-md-budget/index.js +2 -0
  9. package/dist/check/index.d.ts +1 -1
  10. package/dist/check/index.js +1 -1
  11. package/dist/check/orchestrator.d.ts +14 -4
  12. package/dist/check/orchestrator.js +30 -6
  13. package/dist/codebase/map.d.ts +1 -0
  14. package/dist/codebase/map.js +5 -2
  15. package/dist/doctor/checks.js +11 -11
  16. package/dist/doctor/constants.d.ts +7 -2
  17. package/dist/doctor/constants.js +17 -1
  18. package/dist/doctor/flags.js +5 -1
  19. package/dist/doctor/main.js +99 -8
  20. package/dist/doctor/payload-staleness.js +15 -6
  21. package/dist/doctor/types.d.ts +7 -0
  22. package/dist/forward-coverage/evaluate.d.ts +55 -0
  23. package/dist/forward-coverage/evaluate.js +254 -0
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +1 -0
  26. package/dist/init-deposit/constants.d.ts +14 -0
  27. package/dist/init-deposit/constants.js +10 -0
  28. package/dist/init-deposit/hygiene.d.ts +50 -0
  29. package/dist/init-deposit/hygiene.js +176 -0
  30. package/dist/init-deposit/init-deposit.d.ts +1 -0
  31. package/dist/init-deposit/init-deposit.js +7 -1
  32. package/dist/init-deposit/refresh.d.ts +2 -0
  33. package/dist/init-deposit/refresh.js +48 -30
  34. package/dist/init-deposit/scaffold.d.ts +3 -4
  35. package/dist/init-deposit/scaffold.js +3 -24
  36. package/dist/intake/github-auth-modes.d.ts +1 -0
  37. package/dist/intake/github-auth-modes.js +1 -0
  38. package/dist/intake/issue-ingest.d.ts +6 -2
  39. package/dist/intake/issue-ingest.js +65 -9
  40. package/dist/integration-e2e/helpers.d.ts +0 -1
  41. package/dist/integration-e2e/helpers.js +3 -7
  42. package/dist/layout/lifecycle-ref.d.ts +15 -0
  43. package/dist/layout/lifecycle-ref.js +55 -0
  44. package/dist/platform/agents-consumer-header.d.ts +13 -0
  45. package/dist/platform/agents-consumer-header.js +57 -0
  46. package/dist/platform/agents-md.d.ts +28 -0
  47. package/dist/platform/agents-md.js +38 -1
  48. package/dist/platform/index.d.ts +1 -0
  49. package/dist/platform/index.js +1 -0
  50. package/dist/policy/agents-md-advisory.d.ts +52 -0
  51. package/dist/policy/agents-md-advisory.js +63 -0
  52. package/dist/policy/agents-md-budget.d.ts +24 -0
  53. package/dist/policy/agents-md-budget.js +89 -0
  54. package/dist/policy/index.d.ts +1 -0
  55. package/dist/policy/index.js +1 -0
  56. package/dist/policy/resolve.js +56 -46
  57. package/dist/pr-merge-readiness/ci-gate.d.ts +28 -0
  58. package/dist/pr-merge-readiness/ci-gate.js +79 -0
  59. package/dist/pr-merge-readiness/compute.d.ts +15 -1
  60. package/dist/pr-merge-readiness/compute.js +129 -9
  61. package/dist/pr-merge-readiness/gh.d.ts +8 -0
  62. package/dist/pr-merge-readiness/gh.js +30 -5
  63. package/dist/pr-merge-readiness/index.d.ts +5 -1
  64. package/dist/pr-merge-readiness/index.js +3 -0
  65. package/dist/pr-merge-readiness/main.d.ts +3 -0
  66. package/dist/pr-merge-readiness/main.js +73 -6
  67. package/dist/pr-merge-readiness/mergeability.d.ts +43 -0
  68. package/dist/pr-merge-readiness/mergeability.js +105 -0
  69. package/dist/pr-merge-readiness/output.js +20 -0
  70. package/dist/pr-merge-readiness/slizard-gate.d.ts +47 -0
  71. package/dist/pr-merge-readiness/slizard-gate.js +119 -0
  72. package/dist/pr-monitor/index.d.ts +1 -1
  73. package/dist/pr-monitor/index.js +1 -1
  74. package/dist/pr-monitor/monitor.d.ts +8 -2
  75. package/dist/pr-monitor/monitor.js +35 -6
  76. package/dist/pr-wait-mergeable/wrappers.d.ts +12 -1
  77. package/dist/pr-wait-mergeable/wrappers.js +13 -5
  78. package/dist/pr-watch/constants.d.ts +33 -0
  79. package/dist/pr-watch/constants.js +33 -0
  80. package/dist/pr-watch/index.d.ts +6 -0
  81. package/dist/pr-watch/index.js +6 -0
  82. package/dist/pr-watch/main.d.ts +21 -0
  83. package/dist/pr-watch/main.js +206 -0
  84. package/dist/pr-watch/probe.d.ts +13 -0
  85. package/dist/pr-watch/probe.js +100 -0
  86. package/dist/pr-watch/types.d.ts +47 -0
  87. package/dist/pr-watch/types.js +2 -0
  88. package/dist/pr-watch/watch.d.ts +12 -0
  89. package/dist/pr-watch/watch.js +122 -0
  90. package/dist/preflight-cache/evaluate.d.ts +1 -0
  91. package/dist/preflight-cache/evaluate.js +17 -10
  92. package/dist/render/project-render.d.ts +16 -2
  93. package/dist/render/project-render.js +155 -97
  94. package/dist/scope/capacity-stamp.d.ts +24 -2
  95. package/dist/scope/capacity-stamp.js +80 -28
  96. package/dist/scope/decomposed-refs.js +2 -2
  97. package/dist/scope/project-definition-sync.js +3 -3
  98. package/dist/scope/vbrief-ref.d.ts +2 -1
  99. package/dist/scope/vbrief-ref.js +3 -2
  100. package/dist/spec-authority/constants.d.ts +6 -0
  101. package/dist/spec-authority/constants.js +11 -0
  102. package/dist/spec-authority/resolver.js +3 -3
  103. package/dist/swarm/finalize-cohort-cli.d.ts +5 -0
  104. package/dist/swarm/finalize-cohort-cli.js +100 -0
  105. package/dist/swarm/finalize-cohort.d.ts +39 -0
  106. package/dist/swarm/finalize-cohort.js +589 -0
  107. package/dist/swarm/index.d.ts +2 -0
  108. package/dist/swarm/index.js +2 -0
  109. package/dist/swarm/launch.d.ts +13 -0
  110. package/dist/swarm/launch.js +19 -7
  111. package/dist/swarm/routing-verify.d.ts +1 -1
  112. package/dist/swarm/routing-verify.js +11 -10
  113. package/dist/swarm/routing.d.ts +9 -0
  114. package/dist/swarm/routing.js +46 -0
  115. package/dist/swarm/verify-review-clean-cli.js +25 -1
  116. package/dist/swarm/verify-review-clean.d.ts +9 -1
  117. package/dist/swarm/verify-review-clean.js +76 -3
  118. package/dist/triage/queue/index.d.ts +1 -0
  119. package/dist/triage/queue/index.js +1 -0
  120. package/dist/triage/queue/project.d.ts +6 -1
  121. package/dist/triage/queue/project.js +9 -4
  122. package/dist/triage/queue/reconcile-live-state.d.ts +42 -0
  123. package/dist/triage/queue/reconcile-live-state.js +54 -0
  124. package/dist/triage/scope/mutations-core.js +91 -81
  125. package/dist/triage/scope/resolve.js +4 -3
  126. package/dist/triage/scope-drift/add-ignore.js +33 -32
  127. package/dist/triage/scope-drift/scope-rules.js +3 -3
  128. package/dist/triage/subscribe/index.js +51 -45
  129. package/dist/triage/welcome/writers.js +79 -64
  130. package/dist/vbrief-build/project-definition-io.js +8 -3
  131. package/dist/vbrief-validate/paths.js +8 -4
  132. package/dist/vbrief-validate/precutover.d.ts +1 -1
  133. package/dist/vbrief-validate/precutover.js +12 -10
  134. package/dist/verify-source/biome-config.d.ts +41 -0
  135. package/dist/verify-source/biome-config.js +97 -0
  136. package/dist/verify-source/index.d.ts +1 -0
  137. package/dist/verify-source/index.js +1 -0
  138. package/dist/xbrief-migrate/agents-header.d.ts +69 -0
  139. package/dist/xbrief-migrate/agents-header.js +179 -0
  140. package/dist/xbrief-migrate/fs-helpers.d.ts +3 -0
  141. package/dist/xbrief-migrate/fs-helpers.js +11 -0
  142. package/dist/xbrief-migrate/index.d.ts +1 -0
  143. package/dist/xbrief-migrate/index.js +1 -0
  144. package/dist/xbrief-migrate/migrate-project.js +29 -10
  145. package/package.json +15 -7
  146. package/dist/install-upgrade/index.d.ts +0 -13
  147. package/dist/install-upgrade/index.js +0 -251
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  import { resolveEvalPath, resolveProjectDefinitionPath } from "../../layout/resolve.js";
6
6
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
7
+ import { projectDefinitionMutationLock } from "../../vbrief-build/project-definition-io.js";
7
8
  import { SUBSCRIPTION_HISTORY_SCHEMA } from "./constants.js";
8
9
  import { pyStrRepr } from "./python-repr.js";
9
10
  import { utcIso } from "./time.js";
@@ -128,93 +129,102 @@ export function subscribe(projectRoot, options) {
128
129
  if (chosen.length !== 1) {
129
130
  throw new Error(`subscribe() requires exactly one of --label / --milestone / --issue; got ${JSON.stringify(chosen)}`);
130
131
  }
131
- const [data, path] = loadProjectDefinitionForMutation(projectRoot);
132
- const plan = data.plan;
133
- if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
134
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
135
- }
136
- const planRec = plan;
137
- migrateLegacyPolicyKey(planRec);
138
- if (planRec[PLAN_POLICY_KEY] === undefined)
139
- planRec[PLAN_POLICY_KEY] = {};
140
- const policy = planRec[PLAN_POLICY_KEY];
141
- if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
142
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan.policy' key`);
143
- }
144
- const policyRec = policy;
145
- if (policyRec.triageScope === undefined)
146
- policyRec.triageScope = [];
147
- const rules = policyRec.triageScope;
148
- if (!Array.isArray(rules)) {
149
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-list 'plan.policy.triageScope'`);
150
- }
151
- const before = snapshotRules(rules);
152
- let changed;
153
- let message;
154
- if (options.label !== undefined) {
155
- [changed, message] = applySubscribeLabel(rules, options.label);
156
- }
157
- else if (options.milestone !== undefined) {
158
- [changed, message] = applySubscribeMilestone(rules, options.milestone);
159
- }
160
- else {
161
- throw new Error("subscribe() requires exactly one of label or milestone");
162
- }
163
- if (!changed)
164
- return [false, message];
165
- atomicWriteProjectDefinition(path, data);
166
- recordSubscriptionChange(projectRoot, {
167
- op: "subscribe",
168
- label: options.label ?? null,
169
- milestone: options.milestone ?? null,
170
- before,
171
- after: snapshotRules(rules),
172
- actor: options.actor,
132
+ // Serialise the read-modify-write + subscription-history append under the
133
+ // shared PROJECT-DEFINITION mutation lock so concurrent mutators cannot lose
134
+ // an update or emit out-of-order audit rows (#1260).
135
+ return projectDefinitionMutationLock(projectRoot, () => {
136
+ const [data, path] = loadProjectDefinitionForMutation(projectRoot);
137
+ const plan = data.plan;
138
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
139
+ throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
140
+ }
141
+ const planRec = plan;
142
+ migrateLegacyPolicyKey(planRec);
143
+ if (planRec[PLAN_POLICY_KEY] === undefined)
144
+ planRec[PLAN_POLICY_KEY] = {};
145
+ const policy = planRec[PLAN_POLICY_KEY];
146
+ if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
147
+ throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan.policy' key`);
148
+ }
149
+ const policyRec = policy;
150
+ if (policyRec.triageScope === undefined)
151
+ policyRec.triageScope = [];
152
+ const rules = policyRec.triageScope;
153
+ if (!Array.isArray(rules)) {
154
+ throw new Error(`PROJECT-DEFINITION at ${path} has a non-list 'plan.policy.triageScope'`);
155
+ }
156
+ const before = snapshotRules(rules);
157
+ let changed;
158
+ let message;
159
+ if (options.label !== undefined) {
160
+ [changed, message] = applySubscribeLabel(rules, options.label);
161
+ }
162
+ else if (options.milestone !== undefined) {
163
+ [changed, message] = applySubscribeMilestone(rules, options.milestone);
164
+ }
165
+ else {
166
+ throw new Error("subscribe() requires exactly one of label or milestone");
167
+ }
168
+ if (!changed)
169
+ return [false, message];
170
+ atomicWriteProjectDefinition(path, data);
171
+ recordSubscriptionChange(projectRoot, {
172
+ op: "subscribe",
173
+ label: options.label ?? null,
174
+ milestone: options.milestone ?? null,
175
+ before,
176
+ after: snapshotRules(rules),
177
+ actor: options.actor,
178
+ });
179
+ return [true, message];
173
180
  });
174
- return [true, message];
175
181
  }
176
182
  export function addIgnore(projectRoot, label) {
177
183
  if (!label.trim())
178
184
  throw new Error(`label must be a non-empty string; got ${JSON.stringify(label)}`);
179
- const [data, path] = loadProjectDefinitionForMutation(projectRoot);
180
- const plan = data.plan;
181
- if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
182
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
183
- }
184
- const planRec = plan;
185
- migrateLegacyPolicyKey(planRec);
186
- if (planRec[PLAN_POLICY_KEY] === undefined)
187
- planRec[PLAN_POLICY_KEY] = {};
188
- const policy = planRec[PLAN_POLICY_KEY];
189
- if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
190
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan.policy' key`);
191
- }
192
- const policyRec = policy;
193
- if (policyRec.triageScopeIgnores === undefined)
194
- policyRec.triageScopeIgnores = [];
195
- const raw = policyRec.triageScopeIgnores;
196
- if (!Array.isArray(raw)) {
197
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-list 'plan.policy.triageScopeIgnores'`);
198
- }
199
- const before = snapshotRules(raw);
200
- for (const entry of raw) {
201
- if (typeof entry === "object" &&
202
- entry !== null &&
203
- !Array.isArray(entry) &&
204
- entry.label === label) {
205
- return [false, `already-ignored (label=${label})`];
185
+ // Serialise the read-modify-write + subscription-history append under the
186
+ // shared PROJECT-DEFINITION mutation lock (#1260).
187
+ return projectDefinitionMutationLock(projectRoot, () => {
188
+ const [data, path] = loadProjectDefinitionForMutation(projectRoot);
189
+ const plan = data.plan;
190
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
191
+ throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
206
192
  }
207
- }
208
- raw.push({ label });
209
- atomicWriteProjectDefinition(path, data);
210
- const after = snapshotRules(raw);
211
- recordSubscriptionChange(projectRoot, {
212
- op: "ignore-label",
213
- label,
214
- before,
215
- after,
216
- actor: null,
193
+ const planRec = plan;
194
+ migrateLegacyPolicyKey(planRec);
195
+ if (planRec[PLAN_POLICY_KEY] === undefined)
196
+ planRec[PLAN_POLICY_KEY] = {};
197
+ const policy = planRec[PLAN_POLICY_KEY];
198
+ if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
199
+ throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan.policy' key`);
200
+ }
201
+ const policyRec = policy;
202
+ if (policyRec.triageScopeIgnores === undefined)
203
+ policyRec.triageScopeIgnores = [];
204
+ const raw = policyRec.triageScopeIgnores;
205
+ if (!Array.isArray(raw)) {
206
+ throw new Error(`PROJECT-DEFINITION at ${path} has a non-list 'plan.policy.triageScopeIgnores'`);
207
+ }
208
+ const before = snapshotRules(raw);
209
+ for (const entry of raw) {
210
+ if (typeof entry === "object" &&
211
+ entry !== null &&
212
+ !Array.isArray(entry) &&
213
+ entry.label === label) {
214
+ return [false, `already-ignored (label=${label})`];
215
+ }
216
+ }
217
+ raw.push({ label });
218
+ atomicWriteProjectDefinition(path, data);
219
+ const after = snapshotRules(raw);
220
+ recordSubscriptionChange(projectRoot, {
221
+ op: "ignore-label",
222
+ label,
223
+ before,
224
+ after,
225
+ actor: null,
226
+ });
227
+ return [true, `added ignore (label=${label})`];
217
228
  });
218
- return [true, `added ignore (label=${label})`];
219
229
  }
220
230
  //# sourceMappingURL=mutations-core.js.map
@@ -1,9 +1,10 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { resolve } from "node:path";
3
+ import { resolveProjectDefinitionPath } from "../../layout/resolve.js";
3
4
  import { readPlanPolicy } from "../../policy/plan-extensions.js";
4
- import { DEFAULT_TRIAGE_SCOPE, PROJECT_DEFINITION_REL_PATH } from "./constants.js";
5
+ import { DEFAULT_TRIAGE_SCOPE } from "./constants.js";
5
6
  export function projectDefinitionPath(projectRoot) {
6
- return join(resolve(projectRoot), PROJECT_DEFINITION_REL_PATH);
7
+ return resolveProjectDefinitionPath(resolve(projectRoot));
7
8
  }
8
9
  export function loadProjectDefinition(projectRoot) {
9
10
  const path = projectDefinitionPath(projectRoot);
@@ -1,13 +1,10 @@
1
- import { existsSync, mkdtempSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
1
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
2
  import { dirname, join, resolve } from "node:path";
3
+ import { resolveProjectDefinitionPath } from "../../layout/resolve.js";
4
4
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
5
- const PROJECT_DEFINITION_REL_PATH = "vbrief/PROJECT-DEFINITION.vbrief.json";
6
- function projectDefinitionPath(projectRoot) {
7
- return join(resolve(projectRoot), PROJECT_DEFINITION_REL_PATH);
8
- }
5
+ import { projectDefinitionMutationLock } from "../../vbrief-build/project-definition-io.js";
9
6
  function loadForMutation(projectRoot) {
10
- const path = projectDefinitionPath(projectRoot);
7
+ const path = resolveProjectDefinitionPath(resolve(projectRoot));
11
8
  if (!existsSync(path)) {
12
9
  throw new Error(`PROJECT-DEFINITION not found at ${path}; run task triage:welcome / task triage:bootstrap to scaffold one first.`);
13
10
  }
@@ -49,32 +46,36 @@ export function addIgnore(projectRoot, options) {
49
46
  if (typeof value !== "string" || !value.trim()) {
50
47
  throw new Error(`${key} must be a non-empty string; got ${JSON.stringify(value)}`);
51
48
  }
52
- const [data, path] = loadForMutation(projectRoot);
53
- const plan = data.plan;
54
- if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
55
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
56
- }
57
- const planRec = plan;
58
- migrateLegacyPolicyKey(planRec);
59
- let policy = planRec[PLAN_POLICY_KEY];
60
- if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
61
- policy = {};
62
- planRec[PLAN_POLICY_KEY] = policy;
63
- }
64
- const policyRec = policy;
65
- const raw = Array.isArray(policyRec.triageScopeIgnores)
66
- ? policyRec.triageScopeIgnores
67
- : [];
68
- policyRec.triageScopeIgnores = raw;
69
- for (const entry of raw) {
70
- if (typeof entry === "object" && entry !== null && !Array.isArray(entry)) {
71
- if (entry[key] === value) {
72
- return { changed: false, message: `already-ignored (${key}=${value})` };
49
+ // Serialise the read-modify-write under the shared PROJECT-DEFINITION
50
+ // mutation lock so concurrent mutators cannot lose an update (#1260).
51
+ return projectDefinitionMutationLock(projectRoot, () => {
52
+ const [data, path] = loadForMutation(projectRoot);
53
+ const plan = data.plan;
54
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
55
+ throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
56
+ }
57
+ const planRec = plan;
58
+ migrateLegacyPolicyKey(planRec);
59
+ let policy = planRec[PLAN_POLICY_KEY];
60
+ if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
61
+ policy = {};
62
+ planRec[PLAN_POLICY_KEY] = policy;
63
+ }
64
+ const policyRec = policy;
65
+ const raw = Array.isArray(policyRec.triageScopeIgnores)
66
+ ? policyRec.triageScopeIgnores
67
+ : [];
68
+ policyRec.triageScopeIgnores = raw;
69
+ for (const entry of raw) {
70
+ if (typeof entry === "object" && entry !== null && !Array.isArray(entry)) {
71
+ if (entry[key] === value) {
72
+ return { changed: false, message: `already-ignored (${key}=${value})` };
73
+ }
73
74
  }
74
75
  }
75
- }
76
- raw.push({ [key]: value });
77
- atomicWrite(path, data);
78
- return { changed: true, message: `added ignore (${key}=${value})` };
76
+ raw.push({ [key]: value });
77
+ atomicWrite(path, data);
78
+ return { changed: true, message: `added ignore (${key}=${value})` };
79
+ });
79
80
  }
80
81
  //# sourceMappingURL=add-ignore.js.map
@@ -1,10 +1,10 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { resolve } from "node:path";
3
+ import { resolveProjectDefinitionPath } from "../../layout/resolve.js";
3
4
  import { readPlanPolicy } from "../../policy/plan-extensions.js";
4
- const PROJECT_DEFINITION_REL_PATH = "vbrief/PROJECT-DEFINITION.vbrief.json";
5
5
  const DEFAULT_TRIAGE_SCOPE = [{ rule: "all-open" }];
6
6
  function loadProjectDefinition(projectRoot) {
7
- const path = join(resolve(projectRoot), PROJECT_DEFINITION_REL_PATH);
7
+ const path = resolveProjectDefinitionPath(resolve(projectRoot));
8
8
  if (!existsSync(path))
9
9
  return null;
10
10
  try {
@@ -3,6 +3,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlink
3
3
  import { basename, join } from "node:path";
4
4
  import { resolveEvalPath, resolveProjectDefinitionPath } from "../../layout/resolve.js";
5
5
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
6
+ import { projectDefinitionMutationLock } from "../../vbrief-build/project-definition-io.js";
6
7
  export const SUBSCRIPTION_HISTORY_REL_PATH = "vbrief/.eval/subscription-history.jsonl";
7
8
  export const SUBSCRIPTION_HISTORY_SCHEMA = "deft.triage.subscription-change.v1";
8
9
  export const PROJECT_DEFINITION_REL_PATH = "vbrief/PROJECT-DEFINITION.vbrief.json";
@@ -246,57 +247,62 @@ function mutate(projectRoot, options) {
246
247
  if (chosen.length !== 1) {
247
248
  throw new Error(`${options.op}() requires exactly one of --label / --milestone / --issue; got ${JSON.stringify(chosen)}`);
248
249
  }
249
- const [data, path] = loadProjectDefinitionForMutation(projectRoot);
250
- if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
251
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
252
- }
253
- const plan = data.plan;
254
- migrateLegacyPolicyKey(plan);
255
- const existingPolicy = plan[PLAN_POLICY_KEY];
256
- if (typeof existingPolicy !== "object" ||
257
- existingPolicy === null ||
258
- Array.isArray(existingPolicy)) {
259
- if (existingPolicy === undefined) {
260
- plan[PLAN_POLICY_KEY] = {};
250
+ // Serialise the read-modify-write + subscription-history append under the
251
+ // shared PROJECT-DEFINITION mutation lock so concurrent mutators cannot lose
252
+ // an update or emit out-of-order audit rows (#1260).
253
+ return projectDefinitionMutationLock(projectRoot, () => {
254
+ const [data, path] = loadProjectDefinitionForMutation(projectRoot);
255
+ if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
256
+ throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
261
257
  }
262
- else {
263
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan.policy' key`);
258
+ const plan = data.plan;
259
+ migrateLegacyPolicyKey(plan);
260
+ const existingPolicy = plan[PLAN_POLICY_KEY];
261
+ if (typeof existingPolicy !== "object" ||
262
+ existingPolicy === null ||
263
+ Array.isArray(existingPolicy)) {
264
+ if (existingPolicy === undefined) {
265
+ plan[PLAN_POLICY_KEY] = {};
266
+ }
267
+ else {
268
+ throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan.policy' key`);
269
+ }
264
270
  }
265
- }
266
- const policy = plan[PLAN_POLICY_KEY];
267
- if (!Array.isArray(policy.triageScope)) {
268
- if (policy.triageScope === undefined) {
269
- policy.triageScope = [];
271
+ const policy = plan[PLAN_POLICY_KEY];
272
+ if (!Array.isArray(policy.triageScope)) {
273
+ if (policy.triageScope === undefined) {
274
+ policy.triageScope = [];
275
+ }
276
+ else {
277
+ throw new Error(`PROJECT-DEFINITION at ${path} has a non-list 'plan.policy.triageScope'`);
278
+ }
279
+ }
280
+ const rules = policy.triageScope;
281
+ const before = snapshotRules(rules);
282
+ let changed;
283
+ let message;
284
+ if (options.op === "subscribe") {
285
+ [changed, message] = applySubscribe(rules, options.label, options.milestone, options.issue, options.issueNote ?? "added via task triage:subscribe");
270
286
  }
271
287
  else {
272
- throw new Error(`PROJECT-DEFINITION at ${path} has a non-list 'plan.policy.triageScope'`);
288
+ [changed, message] = applyUnsubscribe(rules, options.label, options.milestone, options.issue);
273
289
  }
274
- }
275
- const rules = policy.triageScope;
276
- const before = snapshotRules(rules);
277
- let changed;
278
- let message;
279
- if (options.op === "subscribe") {
280
- [changed, message] = applySubscribe(rules, options.label, options.milestone, options.issue, options.issueNote ?? "added via task triage:subscribe");
281
- }
282
- else {
283
- [changed, message] = applyUnsubscribe(rules, options.label, options.milestone, options.issue);
284
- }
285
- if (!changed) {
286
- return [false, message];
287
- }
288
- atomicWriteProjectDefinition(path, data);
289
- const after = snapshotRules(rules);
290
- recordSubscriptionChange(projectRoot, {
291
- op: options.op,
292
- label: options.label,
293
- milestone: options.milestone,
294
- issue: options.issue,
295
- before,
296
- after,
297
- actor: options.actor,
290
+ if (!changed) {
291
+ return [false, message];
292
+ }
293
+ atomicWriteProjectDefinition(path, data);
294
+ const after = snapshotRules(rules);
295
+ recordSubscriptionChange(projectRoot, {
296
+ op: options.op,
297
+ label: options.label,
298
+ milestone: options.milestone,
299
+ issue: options.issue,
300
+ before,
301
+ after,
302
+ actor: options.actor,
303
+ });
304
+ return [true, message];
298
305
  });
299
- return [true, message];
300
306
  }
301
307
  export function subscribe(projectRoot, options = {}) {
302
308
  return mutate(projectRoot, { op: "subscribe", ...options });
@@ -2,6 +2,7 @@ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renam
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { hasArtifactSuffix, resolveLifecycleFolder, resolveProjectDefinitionPath, } from "../../layout/resolve.js";
4
4
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
5
+ import { projectDefinitionMutationLock } from "../../vbrief-build/project-definition-io.js";
5
6
  import { AUDIT_LOG_REL_PATH, DEFAULT_RELIEF_AGE_DAYS, DEFAULT_WIP_CAP, SUBSCRIPTION_PRESETS, WELCOME_AUDIT_TAG, } from "./constants.js";
6
7
  function projectDefinitionPath(projectRoot) {
7
8
  return resolveProjectDefinitionPath(projectRoot);
@@ -27,77 +28,91 @@ function atomicWrite(path, data) {
27
28
  renameSync(tmp, path);
28
29
  }
29
30
  export function writeTriageScope(projectRoot, rules, options = { presetLabel: "custom" }) {
30
- const path = projectDefinitionPath(projectRoot);
31
- if (!existsSync(path))
32
- throw new Error(`PROJECT-DEFINITION not found at ${path}`);
33
- const data = JSON.parse(readFileSync(path, "utf8"));
34
- const plan = data.plan;
35
- if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
36
- throw new Error("PROJECT-DEFINITION 'plan' is not an object");
37
- }
38
- const planRec = plan;
39
- migrateLegacyPolicyKey(planRec);
40
- let policy = planRec[PLAN_POLICY_KEY];
41
- if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
42
- policy = {};
43
- planRec[PLAN_POLICY_KEY] = policy;
44
- }
45
- const policyRec = policy;
46
- const previous = policyRec.triageScope;
47
- policyRec.triageScope = rules;
48
- atomicWrite(path, data);
49
- const changed = JSON.stringify(previous) !== JSON.stringify(rules);
50
- const actor = options.actor ?? WELCOME_AUDIT_TAG;
51
- const auditEntry = [
52
- `actor=${actor}`,
53
- "field=plan.policy.triageScope",
54
- `preset=${options.presetLabel}`,
55
- `rule_count=${rules.length}`,
56
- `changed=${changed ? "true" : "false"}`,
57
- ].join(" ");
58
- appendAuditEntry(projectRoot, auditEntry);
59
- return [changed, auditEntry];
31
+ // Serialise read-modify-write + audit append under the shared lock (#1260).
32
+ return projectDefinitionMutationLock(projectRoot, () => {
33
+ const path = projectDefinitionPath(projectRoot);
34
+ if (!existsSync(path))
35
+ throw new Error(`PROJECT-DEFINITION not found at ${path}`);
36
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
37
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
38
+ throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
39
+ }
40
+ const data = parsed;
41
+ const plan = data.plan;
42
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
43
+ throw new Error("PROJECT-DEFINITION 'plan' is not an object");
44
+ }
45
+ const planRec = plan;
46
+ migrateLegacyPolicyKey(planRec);
47
+ let policy = planRec[PLAN_POLICY_KEY];
48
+ if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
49
+ policy = {};
50
+ planRec[PLAN_POLICY_KEY] = policy;
51
+ }
52
+ const policyRec = policy;
53
+ const previous = policyRec.triageScope;
54
+ policyRec.triageScope = rules;
55
+ atomicWrite(path, data);
56
+ const changed = JSON.stringify(previous) !== JSON.stringify(rules);
57
+ const actor = options.actor ?? WELCOME_AUDIT_TAG;
58
+ const auditEntry = [
59
+ `actor=${actor}`,
60
+ "field=plan.policy.triageScope",
61
+ `preset=${options.presetLabel}`,
62
+ `rule_count=${rules.length}`,
63
+ `changed=${changed ? "true" : "false"}`,
64
+ ].join(" ");
65
+ appendAuditEntry(projectRoot, auditEntry);
66
+ return [changed, auditEntry];
67
+ });
60
68
  }
61
69
  export function writeWipCap(projectRoot, wipCap, options = {}) {
62
70
  if (!Number.isInteger(wipCap) || wipCap < 1) {
63
71
  throw new Error(`wipCap must be a positive int, got ${JSON.stringify(wipCap)}`);
64
72
  }
65
- const path = projectDefinitionPath(projectRoot);
66
- if (!existsSync(path))
67
- throw new Error(`PROJECT-DEFINITION not found at ${path}`);
68
- const data = JSON.parse(readFileSync(path, "utf8"));
69
- const plan = data.plan;
70
- if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
71
- throw new Error("PROJECT-DEFINITION 'plan' is not an object");
72
- }
73
- const planRec = plan;
74
- migrateLegacyPolicyKey(planRec);
75
- let policy = planRec[PLAN_POLICY_KEY];
76
- if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
77
- policy = {};
78
- planRec[PLAN_POLICY_KEY] = policy;
79
- }
80
- const policyRec = policy;
81
- const previous = policyRec.wipCap;
82
- const actor = options.actor ?? WELCOME_AUDIT_TAG;
83
- if (previous === undefined && wipCap === DEFAULT_WIP_CAP) {
84
- return [false, ""];
85
- }
86
- if (previous !== undefined && wipCap === DEFAULT_WIP_CAP) {
87
- delete policyRec.wipCap;
73
+ // Serialise read-modify-write + audit append under the shared lock (#1260).
74
+ return projectDefinitionMutationLock(projectRoot, () => {
75
+ const path = projectDefinitionPath(projectRoot);
76
+ if (!existsSync(path))
77
+ throw new Error(`PROJECT-DEFINITION not found at ${path}`);
78
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
79
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
80
+ throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
81
+ }
82
+ const data = parsed;
83
+ const plan = data.plan;
84
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
85
+ throw new Error("PROJECT-DEFINITION 'plan' is not an object");
86
+ }
87
+ const planRec = plan;
88
+ migrateLegacyPolicyKey(planRec);
89
+ let policy = planRec[PLAN_POLICY_KEY];
90
+ if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
91
+ policy = {};
92
+ planRec[PLAN_POLICY_KEY] = policy;
93
+ }
94
+ const policyRec = policy;
95
+ const previous = policyRec.wipCap;
96
+ const actor = options.actor ?? WELCOME_AUDIT_TAG;
97
+ if (previous === undefined && wipCap === DEFAULT_WIP_CAP) {
98
+ return [false, ""];
99
+ }
100
+ if (previous !== undefined && wipCap === DEFAULT_WIP_CAP) {
101
+ delete policyRec.wipCap;
102
+ atomicWrite(path, data);
103
+ const auditEntry = `actor=${actor} field=plan.policy.wipCap action=cleared-to-default value=${wipCap} ` +
104
+ `previous=${JSON.stringify(previous)} changed=true`;
105
+ appendAuditEntry(projectRoot, auditEntry);
106
+ return [true, auditEntry];
107
+ }
108
+ policyRec.wipCap = wipCap;
88
109
  atomicWrite(path, data);
89
- const auditEntry = `actor=${actor} field=plan.policy.wipCap action=cleared-to-default value=${wipCap} ` +
90
- `previous=${JSON.stringify(previous)} changed=true`;
110
+ const changed = previous !== wipCap;
111
+ const auditEntry = `actor=${actor} field=plan.policy.wipCap value=${wipCap} previous=${JSON.stringify(previous)} ` +
112
+ `changed=${changed ? "true" : "false"}`;
91
113
  appendAuditEntry(projectRoot, auditEntry);
92
- return [true, auditEntry];
93
- }
94
- policyRec.wipCap = wipCap;
95
- atomicWrite(path, data);
96
- const changed = previous !== wipCap;
97
- const auditEntry = `actor=${actor} field=plan.policy.wipCap value=${wipCap} previous=${JSON.stringify(previous)} ` +
98
- `changed=${changed ? "true" : "false"}`;
99
- appendAuditEntry(projectRoot, auditEntry);
100
- return [changed, auditEntry];
114
+ return [changed, auditEntry];
115
+ });
101
116
  }
102
117
  function daysInPending(path, now) {
103
118
  try {
@@ -1,6 +1,7 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, writeSync, } from "node:fs";
3
- import { dirname, join } from "node:path";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { resolveProjectDefinitionPath } from "../layout/resolve.js";
4
5
  import { PROJECT_DEFINITION_REL_PATH } from "./constants.js";
5
6
  import { pythonJsonPretty } from "./json.js";
6
7
  import { ProjectDefinitionIOError } from "./types.js";
@@ -18,8 +19,12 @@ function defaultSleep(ms) {
18
19
  export function projectDefinitionMutationLock(projectRoot, fn, deps = {}) {
19
20
  const sleepMs = deps.sleepMs ?? defaultSleep;
20
21
  const now = deps.now ?? Date.now;
21
- const path = projectDefinitionPath(projectRoot);
22
- const lockPath = join(dirname(path), `${path.split(/[/\\]/).pop()}.lock`);
22
+ // Derive the sidecar lock path from the layout-aware resolved PROJECT-DEFINITION
23
+ // path (xbrief/ when migrated, else vbrief/) so the lock lives next to the real
24
+ // artifact and every mutator sharing a project root contends on the same lock,
25
+ // instead of the constant vbrief/ path which would strand a stray lock (#1260).
26
+ const path = resolveProjectDefinitionPath(resolve(projectRoot));
27
+ const lockPath = `${path}.lock`;
23
28
  mkdirSync(dirname(lockPath), { recursive: true });
24
29
  if (mutationThreadLock.held) {
25
30
  throw new Error("project definition mutation lock is not reentrant");