@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
@@ -1,10 +1,35 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
3
- import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { hasArtifactSuffix, resolveLifecycleRoot, stripArtifactSuffix } from "../layout/resolve.js";
4
4
  import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
5
+ import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
5
6
  import { deriveRegistryItemStatus, registryMetadataReferencesFromScope, } from "../vbrief-validate/registry-status.js";
7
+ import { LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, VBRIEF_VERSION, } from "../xbrief-migrate/constants.js";
6
8
  import { PROJECT_LIFECYCLE_FOLDERS, SKELETON_NARRATIVES } from "./constants.js";
7
9
  import { splitCamel, splitWords } from "./text-utils.js";
10
+ /**
11
+ * Resolve the PROJECT-DEFINITION artifact shape for a lifecycle root directory.
12
+ *
13
+ * The decision is STRUCTURAL and keyed on the lifecycle root directory name -- the
14
+ * same signal `resolveLifecycleLayout` / `resolveLifecycleRoot` produce, so ingest and
15
+ * render never diverge (#2149). A migrated `xbrief/` root gets `PROJECT-DEFINITION.xbrief.json`
16
+ * + `xBRIEFInfo`; a legacy `vbrief/` root keeps `PROJECT-DEFINITION.vbrief.json` + `vBRIEFInfo`.
17
+ * This prevents render from writing a legacy-named/enveloped artifact into a migrated tree.
18
+ */
19
+ function resolveProjectDefinitionLayout(vbriefDir) {
20
+ const migrated = basename(vbriefDir) === MIGRATED_ARTIFACT_DIR;
21
+ return migrated
22
+ ? {
23
+ filename: `PROJECT-DEFINITION${MIGRATED_ARTIFACT_SUFFIX}`,
24
+ infoRootKey: MIGRATED_INFO_ROOT_KEY,
25
+ infoVersion: VBRIEF_VERSION,
26
+ }
27
+ : {
28
+ filename: `PROJECT-DEFINITION${LEGACY_ARTIFACT_SUFFIX}`,
29
+ infoRootKey: LEGACY_INFO_ROOT_KEY,
30
+ infoVersion: EMITTED_VBRIEF_VERSION,
31
+ };
32
+ }
8
33
  export function scanLifecycleFolders(vbriefDir) {
9
34
  const items = [];
10
35
  for (const folderName of PROJECT_LIFECYCLE_FOLDERS) {
@@ -25,11 +50,11 @@ export function scanLifecycleFolders(vbriefDir) {
25
50
  try {
26
51
  const data = JSON.parse(readFileSync(full, "utf8"));
27
52
  const plan = (data.plan ?? {});
28
- const title = String(plan.title ?? vbriefFile.replace(/\.vbrief\.json$/, ""));
53
+ const title = String(plan.title ?? stripArtifactSuffix(vbriefFile));
29
54
  const status = deriveRegistryItemStatus(plan.status, folderName);
30
55
  const references = registryMetadataReferencesFromScope(plan.references);
31
56
  const item = {
32
- id: vbriefFile.replace(/\.vbrief\.json$/, "").replace(/\.vbrief$/, ""),
57
+ id: stripArtifactSuffix(vbriefFile),
33
58
  title,
34
59
  status,
35
60
  metadata: {
@@ -44,7 +69,7 @@ export function scanLifecycleFolders(vbriefDir) {
44
69
  }
45
70
  catch {
46
71
  items.push({
47
- id: vbriefFile.replace(/\.vbrief\.json$/, "").replace(/\.vbrief$/, ""),
72
+ id: stripArtifactSuffix(vbriefFile),
48
73
  title: `[unreadable] ${vbriefFile}`,
49
74
  status: "draft",
50
75
  metadata: {
@@ -136,12 +161,12 @@ export function computeStalenessFlags(narratives, completedItems, review = null)
136
161
  const pending = unacknowledgedCompletedItems(completedItems, review);
137
162
  return flagStaleNarratives(narratives, pending);
138
163
  }
139
- export function createSkeleton(items, now) {
164
+ export function createSkeleton(items, now, layout) {
140
165
  const completedItems = items.filter((i) => i.status === "completed");
141
166
  const stalenessFlags = computeStalenessFlags({ ...SKELETON_NARRATIVES }, completedItems);
142
167
  return {
143
- vBRIEFInfo: {
144
- version: EMITTED_VBRIEF_VERSION,
168
+ [layout.infoRootKey]: {
169
+ version: layout.infoVersion,
145
170
  description: "Project definition -- synthesized gestalt of the project",
146
171
  created: now,
147
172
  updated: now,
@@ -155,107 +180,142 @@ export function createSkeleton(items, now) {
155
180
  },
156
181
  };
157
182
  }
158
- /** Regenerate PROJECT-DEFINITION.vbrief.json (mirrors ``scripts/project_render.render_project_definition``). */
183
+ /**
184
+ * Regenerate the PROJECT-DEFINITION artifact for `vbriefDir`.
185
+ *
186
+ * Layout-aware (#2149): on a migrated `xbrief/` root it targets
187
+ * `PROJECT-DEFINITION.xbrief.json` with an `xBRIEFInfo` envelope; on a legacy `vbrief/`
188
+ * root it keeps `PROJECT-DEFINITION.vbrief.json` + `vBRIEFInfo`.
189
+ * (Mirrors ``scripts/project_render.render_project_definition``.)
190
+ */
159
191
  export function renderProjectDefinition(vbriefDir, options = {}) {
160
- const nowDate = options.now ?? new Date();
161
- const now = nowDate.toISOString().replace(/\.\d{3}Z$/, "Z");
162
- const projectDefPath = join(vbriefDir, "PROJECT-DEFINITION.vbrief.json");
163
- const items = scanLifecycleFolders(vbriefDir);
164
- const createdNew = !existsSync(projectDefPath);
165
- let projectDef;
166
- if (existsSync(projectDefPath)) {
192
+ // Serialise the whole read-modify-write of PROJECT-DEFINITION under the shared
193
+ // mutation lock so a concurrent policy/triage mutator cannot be clobbered by the
194
+ // materialised items/metadata write (or vice versa) (#1260).
195
+ return projectDefinitionMutationLock(resolve(vbriefDir, ".."), () => {
196
+ const nowDate = options.now ?? new Date();
197
+ const now = nowDate.toISOString().replace(/\.\d{3}Z$/, "Z");
198
+ const layout = resolveProjectDefinitionLayout(vbriefDir);
199
+ const projectDefPath = join(vbriefDir, layout.filename);
200
+ const items = scanLifecycleFolders(vbriefDir);
201
+ const createdNew = !existsSync(projectDefPath);
202
+ let projectDef;
203
+ if (existsSync(projectDefPath)) {
204
+ let parsed;
205
+ try {
206
+ parsed = JSON.parse(readFileSync(projectDefPath, "utf8"));
207
+ }
208
+ catch (exc) {
209
+ return [false, `✗ Failed to read ${projectDefPath}: ${String(exc)}`];
210
+ }
211
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
212
+ return [false, `✗ ${projectDefPath} top-level value is not a JSON object`];
213
+ }
214
+ projectDef = parsed;
215
+ const plan = (projectDef.plan ?? {});
216
+ plan.items = items;
217
+ if (typeof projectDef[layout.infoRootKey] !== "object" ||
218
+ projectDef[layout.infoRootKey] === null) {
219
+ projectDef[layout.infoRootKey] = {};
220
+ }
221
+ projectDef[layout.infoRootKey].updated = now;
222
+ const narratives = typeof plan.narratives === "object" &&
223
+ plan.narratives !== null &&
224
+ !Array.isArray(plan.narratives)
225
+ ? plan.narratives
226
+ : {};
227
+ const completedItems = items.filter((i) => i.status === "completed");
228
+ if (typeof plan.metadata !== "object" ||
229
+ plan.metadata === null ||
230
+ Array.isArray(plan.metadata)) {
231
+ plan.metadata = {};
232
+ }
233
+ const planMetadata = plan.metadata;
234
+ const review = parseStalenessReview(planMetadata);
235
+ const flags = computeStalenessFlags(narratives, completedItems, review);
236
+ planMetadata.staleness_flags = flags;
237
+ projectDef.plan = plan;
238
+ }
239
+ else {
240
+ projectDef = createSkeleton(items, now, layout);
241
+ }
242
+ mkdirSync(vbriefDir, { recursive: true });
243
+ // Atomic temp+rename write under the lock so external readers never observe
244
+ // a partially-written PROJECT-DEFINITION (#1260).
245
+ atomicWriteProjectDefinition(projectDefPath, projectDef);
246
+ const itemCount = items.length;
247
+ const planMeta = (projectDef.plan?.metadata ?? {});
248
+ const flagCount = Array.isArray(planMeta.staleness_flags) ? planMeta.staleness_flags.length : 0;
249
+ const action = createdNew ? "created" : "updated";
250
+ const parts = [`✓ ${layout.filename} ${action} (${itemCount} scope items)`];
251
+ if (flagCount > 0)
252
+ parts.push(`⚠ ${flagCount} staleness flag(s) -- agent review recommended`);
253
+ return [true, parts.join("\n")];
254
+ });
255
+ }
256
+ /**
257
+ * Mark current completed scopes as reviewed for PROJECT-DEFINITION narratives.
258
+ *
259
+ * Distinct from `task reconcile:issues`, which reconciles origin freshness on scope
260
+ * vBRIEFs — this path only clears narrative staleness heuristics on render.
261
+ */
262
+ export function acknowledgeProjectDefinitionStaleness(vbriefDir, options = {}) {
263
+ // Serialise the read-modify-write of PROJECT-DEFINITION under the shared
264
+ // mutation lock so a concurrent policy/triage mutator is not clobbered (#1260).
265
+ return projectDefinitionMutationLock(resolve(vbriefDir, ".."), () => {
266
+ const nowDate = options.now ?? new Date();
267
+ const now = isoTimestamp(nowDate);
268
+ const layout = resolveProjectDefinitionLayout(vbriefDir);
269
+ const projectDefPath = join(vbriefDir, layout.filename);
270
+ if (!existsSync(projectDefPath)) {
271
+ return [false, `✗ ${projectDefPath} not found — run project:render first`];
272
+ }
273
+ let parsed;
167
274
  try {
168
- projectDef = JSON.parse(readFileSync(projectDefPath, "utf8"));
275
+ parsed = JSON.parse(readFileSync(projectDefPath, "utf8"));
169
276
  }
170
277
  catch (exc) {
171
278
  return [false, `✗ Failed to read ${projectDefPath}: ${String(exc)}`];
172
279
  }
280
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
281
+ return [false, `✗ ${projectDefPath} top-level value is not a JSON object`];
282
+ }
283
+ const projectDef = parsed;
173
284
  const plan = (projectDef.plan ?? {});
174
- plan.items = items;
175
- if (typeof projectDef.vBRIEFInfo !== "object" || projectDef.vBRIEFInfo === null) {
176
- projectDef.vBRIEFInfo = {};
285
+ if (typeof plan.metadata !== "object" ||
286
+ plan.metadata === null ||
287
+ Array.isArray(plan.metadata)) {
288
+ plan.metadata = {};
177
289
  }
178
- projectDef.vBRIEFInfo.updated = now;
290
+ const planMetadata = plan.metadata;
291
+ const items = scanLifecycleFolders(vbriefDir);
292
+ const completedItems = items.filter((i) => i.status === "completed");
293
+ const existing = parseStalenessReview(planMetadata);
294
+ planMetadata.staleness_review = buildStalenessAcknowledgement(completedItems, {
295
+ now: nowDate,
296
+ existing,
297
+ });
179
298
  const narratives = typeof plan.narratives === "object" &&
180
299
  plan.narratives !== null &&
181
300
  !Array.isArray(plan.narratives)
182
301
  ? plan.narratives
183
302
  : {};
184
- const completedItems = items.filter((i) => i.status === "completed");
185
- if (typeof plan.metadata !== "object" ||
186
- plan.metadata === null ||
187
- Array.isArray(plan.metadata)) {
188
- plan.metadata = {};
303
+ planMetadata.staleness_flags = computeStalenessFlags(narratives, completedItems, parseStalenessReview(planMetadata));
304
+ if (typeof projectDef[layout.infoRootKey] !== "object" ||
305
+ projectDef[layout.infoRootKey] === null) {
306
+ projectDef[layout.infoRootKey] = {};
189
307
  }
190
- const planMetadata = plan.metadata;
191
- const review = parseStalenessReview(planMetadata);
192
- const flags = computeStalenessFlags(narratives, completedItems, review);
193
- planMetadata.staleness_flags = flags;
308
+ projectDef[layout.infoRootKey].updated = now;
194
309
  projectDef.plan = plan;
195
- }
196
- else {
197
- projectDef = createSkeleton(items, now);
198
- }
199
- mkdirSync(vbriefDir, { recursive: true });
200
- writeFileSync(projectDefPath, `${JSON.stringify(projectDef, null, 2)}\n`, "utf8");
201
- const itemCount = items.length;
202
- const planMeta = (projectDef.plan?.metadata ?? {});
203
- const flagCount = Array.isArray(planMeta.staleness_flags) ? planMeta.staleness_flags.length : 0;
204
- const action = createdNew ? "created" : "updated";
205
- const parts = [`✓ PROJECT-DEFINITION.vbrief.json ${action} (${itemCount} scope items)`];
206
- if (flagCount > 0)
207
- parts.push(`⚠ ${flagCount} staleness flag(s) -- agent review recommended`);
208
- return [true, parts.join("\n")];
209
- }
210
- /**
211
- * Mark current completed scopes as reviewed for PROJECT-DEFINITION narratives.
212
- *
213
- * Distinct from `task reconcile:issues`, which reconciles origin freshness on scope
214
- * vBRIEFs — this path only clears narrative staleness heuristics on render.
215
- */
216
- export function acknowledgeProjectDefinitionStaleness(vbriefDir, options = {}) {
217
- const nowDate = options.now ?? new Date();
218
- const now = isoTimestamp(nowDate);
219
- const projectDefPath = join(vbriefDir, "PROJECT-DEFINITION.vbrief.json");
220
- if (!existsSync(projectDefPath)) {
221
- return [false, `✗ ${projectDefPath} not found — run project:render first`];
222
- }
223
- let projectDef;
224
- try {
225
- projectDef = JSON.parse(readFileSync(projectDefPath, "utf8"));
226
- }
227
- catch (exc) {
228
- return [false, `✗ Failed to read ${projectDefPath}: ${String(exc)}`];
229
- }
230
- const plan = (projectDef.plan ?? {});
231
- if (typeof plan.metadata !== "object" || plan.metadata === null || Array.isArray(plan.metadata)) {
232
- plan.metadata = {};
233
- }
234
- const planMetadata = plan.metadata;
235
- const items = scanLifecycleFolders(vbriefDir);
236
- const completedItems = items.filter((i) => i.status === "completed");
237
- const existing = parseStalenessReview(planMetadata);
238
- planMetadata.staleness_review = buildStalenessAcknowledgement(completedItems, {
239
- now: nowDate,
240
- existing,
310
+ // Atomic temp+rename write under the lock so external readers never observe
311
+ // a partially-written PROJECT-DEFINITION (#1260).
312
+ atomicWriteProjectDefinition(projectDefPath, projectDef);
313
+ const ackCount = completedItems.length;
314
+ return [
315
+ true,
316
+ `✓ PROJECT-DEFINITION staleness acknowledged (${ackCount} completed scope(s) watermarked)`,
317
+ ];
241
318
  });
242
- const narratives = typeof plan.narratives === "object" &&
243
- plan.narratives !== null &&
244
- !Array.isArray(plan.narratives)
245
- ? plan.narratives
246
- : {};
247
- planMetadata.staleness_flags = computeStalenessFlags(narratives, completedItems, parseStalenessReview(planMetadata));
248
- if (typeof projectDef.vBRIEFInfo !== "object" || projectDef.vBRIEFInfo === null) {
249
- projectDef.vBRIEFInfo = {};
250
- }
251
- projectDef.vBRIEFInfo.updated = now;
252
- projectDef.plan = plan;
253
- writeFileSync(projectDefPath, `${JSON.stringify(projectDef, null, 2)}\n`, "utf8");
254
- const ackCount = completedItems.length;
255
- return [
256
- true,
257
- `✓ PROJECT-DEFINITION staleness acknowledged (${ackCount} completed scope(s) watermarked)`,
258
- ];
259
319
  }
260
320
  /** CLI entry (mirrors ``scripts/project_render.main``). */
261
321
  export function main(argv) {
@@ -280,9 +340,7 @@ export function main(argv) {
280
340
  process.stderr.write("Usage: project-render [--acknowledge-staleness] [--project-root <dir>] [vbrief_dir]\n");
281
341
  return 2;
282
342
  }
283
- const vbriefDir = projectRoot !== undefined
284
- ? resolveLifecycleRoot(resolve(projectRoot))
285
- : (positional[0] ?? "vbrief");
343
+ const vbriefDir = positional[0] ?? resolveLifecycleRoot(resolve(projectRoot !== undefined ? projectRoot : "."));
286
344
  const [ok, message] = acknowledge
287
345
  ? acknowledgeProjectDefinitionStaleness(vbriefDir)
288
346
  : renderProjectDefinition(vbriefDir);
@@ -1,3 +1,25 @@
1
- /** Stamp completedAt + capacityBucket onto a completing vBRIEF (#1419). */
2
- export declare function stampCompletionMetadata(plan: Record<string, unknown>, projectRoot: string, timestamp: string): void;
1
+ /** Read the labels for an issue reference. Returns null when they cannot be resolved. */
2
+ export type LabelReader = (repo: string, issueNumber: number) => ReadonlySet<string> | null;
3
+ export interface StampCompletionOptions {
4
+ /**
5
+ * Explicit label set for the completing brief. When provided it takes precedence
6
+ * over any linked-issue lookup, keeping bucket resolution pure and network-free.
7
+ */
8
+ readonly labels?: Iterable<string>;
9
+ /**
10
+ * Injectable reader used to resolve labels from the brief's linked issue when no
11
+ * explicit `labels` are supplied. Defaults to a cached-issue lookup (no network).
12
+ * A `null` return signals a cache MISS and triggers the live fallback below.
13
+ */
14
+ readonly labelReader?: LabelReader;
15
+ /**
16
+ * Injectable fail-open fallback used ONLY on a cache miss (`labelReader` returned
17
+ * `null`). Defaults to a single REST read via the scm shim (PRs excluded, #2246).
18
+ * A `null` return leaves resolution on the `defaultBucket` fallback. Injected in
19
+ * tests so the fallback path is exercised without a network call.
20
+ */
21
+ readonly liveLabelReader?: LabelReader;
22
+ }
23
+ /** Stamp completedAt + label-matched capacityBucket onto a completing vBRIEF (#1419, #2237). */
24
+ export declare function stampCompletionMetadata(plan: Record<string, unknown>, projectRoot: string, timestamp: string, options?: StampCompletionOptions): void;
3
25
  //# sourceMappingURL=capacity-stamp.d.ts.map
@@ -1,33 +1,84 @@
1
- import { readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
3
- import { readPlanPolicy } from "../policy/plan-extensions.js";
4
- import { PROJECT_DEFINITION_REL_PATH } from "../policy/resolve.js";
5
- function resolveDefaultCapacityBucket(projectRoot) {
6
- try {
7
- const path = join(resolve(projectRoot), PROJECT_DEFINITION_REL_PATH);
8
- const raw = readFileSync(path, "utf8");
9
- const data = JSON.parse(raw);
10
- const plan = data.plan;
11
- if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
12
- return "";
1
+ import { resolve } from "node:path";
2
+ import { cachedIssueLabels, extractIssueRef } from "../capacity/backfill.js";
3
+ import { classifyBucket, loadBucketMatchers } from "../policy/capacity.js";
4
+ import { restIssueView } from "../scm/gh-rest.js";
5
+ function normalizeLabels(labels) {
6
+ const out = new Set();
7
+ for (const label of labels) {
8
+ if (typeof label === "string" && label.trim().length > 0) {
9
+ out.add(label);
13
10
  }
14
- const policy = readPlanPolicy(plan);
15
- if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
16
- return "";
11
+ }
12
+ return out;
13
+ }
14
+ /** Extract label name strings from a raw REST/cache `labels` array. */
15
+ function labelsFromRaw(raw) {
16
+ const out = new Set();
17
+ if (!Array.isArray(raw)) {
18
+ return out;
19
+ }
20
+ for (const label of raw) {
21
+ if (typeof label === "string" && label.length > 0) {
22
+ out.add(label);
17
23
  }
18
- const allocation = policy.capacityAllocation;
19
- if (typeof allocation !== "object" || allocation === null || Array.isArray(allocation)) {
20
- return "";
24
+ else if (typeof label === "object" && label !== null && !Array.isArray(label)) {
25
+ const name = label.name;
26
+ if (typeof name === "string" && name.length > 0) {
27
+ out.add(name);
28
+ }
29
+ }
30
+ }
31
+ return out;
32
+ }
33
+ /**
34
+ * Fail-open live label read for a cache miss: a single REST GET through the scm
35
+ * shim (`restIssueView`), with PRs excluded. Any error (or a PR ref) yields `null`
36
+ * so resolution falls through to `defaultBucket` instead of crashing (#2246).
37
+ */
38
+ function liveIssueLabels(repo, issueNumber) {
39
+ try {
40
+ const issue = restIssueView(repo, issueNumber);
41
+ // The issues endpoint also returns PRs; a PR object carries `pull_request`.
42
+ // Exclude PRs from label-based bucket resolution.
43
+ if (issue.pull_request !== undefined) {
44
+ return null;
21
45
  }
22
- const defaultBucket = allocation.defaultBucket;
23
- return typeof defaultBucket === "string" ? defaultBucket : "";
46
+ return labelsFromRaw(issue.labels);
24
47
  }
25
48
  catch {
26
- return "";
49
+ return null;
27
50
  }
28
51
  }
29
- /** Stamp completedAt + capacityBucket onto a completing vBRIEF (#1419). */
30
- export function stampCompletionMetadata(plan, projectRoot, timestamp) {
52
+ /**
53
+ * Resolve the capacityBucket for a completing brief by matching its issue labels
54
+ * against `capacityAllocation.buckets[].match.labels` (first declared match wins),
55
+ * falling back to `defaultBucket` when nothing matches (#2237).
56
+ */
57
+ function resolveCapacityBucket(plan, projectRoot, options) {
58
+ const root = resolve(projectRoot);
59
+ const { matchers, default_bucket: defaultBucket } = loadBucketMatchers(root);
60
+ let labels = null;
61
+ if (options.labels !== undefined) {
62
+ labels = normalizeLabels(options.labels);
63
+ }
64
+ else {
65
+ const [repo, issueNumber] = extractIssueRef(plan);
66
+ if (repo !== null && issueNumber !== null) {
67
+ const reader = options.labelReader ?? ((r, n) => cachedIssueLabels(root, r, n));
68
+ labels = reader(repo, issueNumber);
69
+ // Cache MISS (null) -- take the fail-open live fallback. A cache HIT (even an
70
+ // empty set) stays the fast path and makes NO network call (#2246).
71
+ if (labels === null) {
72
+ const live = options.liveLabelReader ?? liveIssueLabels;
73
+ labels = live(repo, issueNumber);
74
+ }
75
+ }
76
+ }
77
+ const [bucket] = classifyBucket(labels ?? new Set(), matchers, defaultBucket);
78
+ return bucket;
79
+ }
80
+ /** Stamp completedAt + label-matched capacityBucket onto a completing vBRIEF (#1419, #2237). */
81
+ export function stampCompletionMetadata(plan, projectRoot, timestamp, options = {}) {
31
82
  let metadata = plan.metadata;
32
83
  if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
33
84
  metadata = {};
@@ -36,11 +87,12 @@ export function stampCompletionMetadata(plan, projectRoot, timestamp) {
36
87
  const meta = metadata;
37
88
  meta.completedAt = timestamp;
38
89
  const existing = meta.capacityBucket;
39
- if (!(typeof existing === "string" && existing.trim().length > 0)) {
40
- const bucket = resolveDefaultCapacityBucket(projectRoot);
41
- if (bucket.length > 0) {
42
- meta.capacityBucket = bucket;
43
- }
90
+ if (typeof existing === "string" && existing.trim().length > 0) {
91
+ return;
92
+ }
93
+ const bucket = resolveCapacityBucket(plan, projectRoot, options);
94
+ if (bucket.length > 0) {
95
+ meta.capacityBucket = bucket;
44
96
  }
45
97
  }
46
98
  //# sourceMappingURL=capacity-stamp.js.map
@@ -7,7 +7,7 @@ function rewriteOnePlanRef(value, oldParentResolved, newParentRel, vbriefDir) {
7
7
  if (typeof value !== "string" || value.length === 0) {
8
8
  return [value, false];
9
9
  }
10
- const resolved = resolveVbriefRef(value, vbriefDir);
10
+ const resolved = resolveVbriefRef(value, vbriefDir, { allowCrossFolderSearch: false });
11
11
  if (resolved === null || resolve(resolved) !== resolve(oldParentResolved)) {
12
12
  return [value, false];
13
13
  }
@@ -40,7 +40,7 @@ function rewriteParentChildReference(parentPath, oldChildResolved, newChildRel,
40
40
  if (!referenceTypeMatches(String(r.type ?? ""), "plan")) {
41
41
  continue;
42
42
  }
43
- const resolved = resolveVbriefRef(r.uri, vbriefDir);
43
+ const resolved = resolveVbriefRef(r.uri, vbriefDir, { allowCrossFolderSearch: false });
44
44
  if (resolved === null || resolve(resolved) !== resolve(oldChildResolved)) {
45
45
  continue;
46
46
  }
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname, join, resolve } from "node:path";
2
+ import { dirname, resolve } from "node:path";
3
3
  import { referenceTypeMatches } from "@deftai/directive-types";
4
- import { PROJECT_DEFINITION_REL_PATH } from "../policy/resolve.js";
4
+ import { resolveProjectDefinitionPath } from "../layout/resolve.js";
5
5
  import { formatVbriefJson } from "./vbrief-json.js";
6
6
  import { relativeToVbrief, resolveVbriefRef, scopeIdsForFilename } from "./vbrief-ref.js";
7
7
  function rewriteProjectDefinitionPlanReference(ref, oldResolved, newRel, vbriefRoot) {
@@ -95,7 +95,7 @@ export function syncProjectDefinitionAfterScopeMove(scopeData, oldPath, newPath,
95
95
  return;
96
96
  }
97
97
  const projectRoot = dirname(resolve(vbriefRoot));
98
- const projectDefPath = join(projectRoot, PROJECT_DEFINITION_REL_PATH);
98
+ const projectDefPath = resolveProjectDefinitionPath(projectRoot);
99
99
  if (!existsSync(projectDefPath)) {
100
100
  return;
101
101
  }
@@ -1,5 +1,6 @@
1
+ import { type ResolveLifecycleArtifactRefOptions } from "../layout/lifecycle-ref.js";
1
2
  /** Resolve a vBRIEF reference URI to an absolute path, or null. */
2
- export declare function resolveVbriefRef(uri: unknown, vbriefDir: string): string | null;
3
+ export declare function resolveVbriefRef(uri: unknown, vbriefDir: string, options?: ResolveLifecycleArtifactRefOptions): string | null;
3
4
  /** Collect planRef values from the plan root and top-level items. */
4
5
  export declare function collectPlanRefs(plan: Record<string, unknown>): string[];
5
6
  /** Collect x-vbrief/plan child reference uris from a parent plan. */
@@ -1,8 +1,9 @@
1
1
  import { resolve } from "node:path";
2
2
  import { referenceTypeMatches } from "@deftai/directive-types";
3
+ import { resolveLifecycleArtifactRef, } from "../layout/lifecycle-ref.js";
3
4
  import { hasArtifactSuffix, stripArtifactSuffix } from "../layout/resolve.js";
4
5
  /** Resolve a vBRIEF reference URI to an absolute path, or null. */
5
- export function resolveVbriefRef(uri, vbriefDir) {
6
+ export function resolveVbriefRef(uri, vbriefDir, options = {}) {
6
7
  if (typeof uri !== "string" || uri.length === 0) {
7
8
  return null;
8
9
  }
@@ -16,7 +17,7 @@ export function resolveVbriefRef(uri, vbriefDir) {
16
17
  else {
17
18
  rel = uri;
18
19
  }
19
- return resolve(vbriefDir, rel);
20
+ return resolveLifecycleArtifactRef(rel, vbriefDir, options);
20
21
  }
21
22
  /** Collect planRef values from the plan root and top-level items. */
22
23
  export function collectPlanRefs(plan) {
@@ -1,7 +1,13 @@
1
1
  /** Generated-spec banner tokens (lockstep with Python ``_precutover.py``). */
2
2
  export declare const GENERATED_SPEC_PURPOSE = "<!-- Purpose: rendered specification -->";
3
3
  export declare const GENERATED_SPEC_SOURCE_SPEC = "<!-- Source of truth: vbrief/specification.vbrief.json -->";
4
+ export declare const GENERATED_SPEC_SOURCE_SPEC_XBRIEF = "<!-- Source of truth: xbrief/specification.xbrief.json -->";
4
5
  export declare const GENERATED_SPEC_SOURCE_PD = "<!-- Source of truth: vbrief/PROJECT-DEFINITION.vbrief.json -->";
6
+ export declare const GENERATED_SPEC_SOURCE_PD_XBRIEF = "<!-- Source of truth: xbrief/PROJECT-DEFINITION.xbrief.json -->";
7
+ /** True when markdown carries a generated-spec source marker for either layout. */
8
+ export declare function contentHasGeneratedSpecSource(content: string): boolean;
9
+ /** True when markdown carries a generated greenfield PD source marker for either layout. */
10
+ export declare function contentHasGeneratedPdSource(content: string): boolean;
5
11
  export declare const EXPORT_SPEC_PD_BANNER: string;
6
12
  /** Narratives excluded from stakeholder SPEC/PRD exports (#2013 Wave 0 §5). */
7
13
  export declare const STAKEHOLDER_EXCLUDED_NARRATIVE_KEYS: Set<string>;
@@ -1,7 +1,18 @@
1
1
  /** Generated-spec banner tokens (lockstep with Python ``_precutover.py``). */
2
2
  export const GENERATED_SPEC_PURPOSE = "<!-- Purpose: rendered specification -->";
3
3
  export const GENERATED_SPEC_SOURCE_SPEC = "<!-- Source of truth: vbrief/specification.vbrief.json -->";
4
+ export const GENERATED_SPEC_SOURCE_SPEC_XBRIEF = "<!-- Source of truth: xbrief/specification.xbrief.json -->";
4
5
  export const GENERATED_SPEC_SOURCE_PD = "<!-- Source of truth: vbrief/PROJECT-DEFINITION.vbrief.json -->";
6
+ export const GENERATED_SPEC_SOURCE_PD_XBRIEF = "<!-- Source of truth: xbrief/PROJECT-DEFINITION.xbrief.json -->";
7
+ /** True when markdown carries a generated-spec source marker for either layout. */
8
+ export function contentHasGeneratedSpecSource(content) {
9
+ return (content.includes(GENERATED_SPEC_SOURCE_SPEC) ||
10
+ content.includes(GENERATED_SPEC_SOURCE_SPEC_XBRIEF));
11
+ }
12
+ /** True when markdown carries a generated greenfield PD source marker for either layout. */
13
+ export function contentHasGeneratedPdSource(content) {
14
+ return (content.includes(GENERATED_SPEC_SOURCE_PD) || content.includes(GENERATED_SPEC_SOURCE_PD_XBRIEF));
15
+ }
5
16
  export const EXPORT_SPEC_PD_BANNER = "<!-- AUTO-GENERATED by task project:export-spec -- DO NOT EDIT MANUALLY -->\n" +
6
17
  `${GENERATED_SPEC_PURPOSE}\n` +
7
18
  `${GENERATED_SPEC_SOURCE_PD}\n` +
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { resolveLifecycleLayout, resolveLifecycleRoot, resolveProjectDefinitionPath, } from "../layout/resolve.js";
4
4
  import { SPEC_RENDER_BANNER } from "../render/constants.js";
5
- import { EXPORT_SPEC_PD_BANNER, GENERATED_SPEC_PURPOSE, GENERATED_SPEC_SOURCE_PD, GENERATED_SPEC_SOURCE_SPEC, } from "./constants.js";
5
+ import { contentHasGeneratedPdSource, contentHasGeneratedSpecSource, EXPORT_SPEC_PD_BANNER, GENERATED_SPEC_PURPOSE, } from "./constants.js";
6
6
  export function resolveSpecAuthority(projectRoot) {
7
7
  const root = projectRoot;
8
8
  const layout = resolveLifecycleLayout(root);
@@ -37,14 +37,14 @@ export function isFullSpecState(projectRoot) {
37
37
  if (authority?.kind !== "full-spec")
38
38
  return false;
39
39
  const specMd = readSpecMarkdown(projectRoot);
40
- return specMd.includes(GENERATED_SPEC_PURPOSE) && specMd.includes(GENERATED_SPEC_SOURCE_SPEC);
40
+ return specMd.includes(GENERATED_SPEC_PURPOSE) && contentHasGeneratedSpecSource(specMd);
41
41
  }
42
42
  export function isGreenfieldSpecExport(projectRoot) {
43
43
  const authority = resolveSpecAuthority(projectRoot);
44
44
  if (authority?.kind !== "greenfield")
45
45
  return false;
46
46
  const specMd = readSpecMarkdown(projectRoot);
47
- return specMd.includes(GENERATED_SPEC_PURPOSE) && specMd.includes(GENERATED_SPEC_SOURCE_PD);
47
+ return specMd.includes(GENERATED_SPEC_PURPOSE) && contentHasGeneratedPdSource(specMd);
48
48
  }
49
49
  export function isCurrentGeneratedSpecification(projectRoot) {
50
50
  return isFullSpecState(projectRoot) || isGreenfieldSpecExport(projectRoot);
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { finalizeCohort } from "./finalize-cohort.js";
3
+ export declare function parseFinalizeCohortArgv(argv: readonly string[]): Parameters<typeof finalizeCohort>[0];
4
+ export declare function finalizeCohortMain(argv?: string[]): number;
5
+ //# sourceMappingURL=finalize-cohort-cli.d.ts.map