@deftai/directive-core 0.65.0 → 0.66.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 (151) hide show
  1. package/dist/architecture/sor-preflight.js +6 -4
  2. package/dist/capacity/backfill.js +5 -3
  3. package/dist/capacity/show.js +5 -3
  4. package/dist/category-b-namespace/index.d.ts +39 -0
  5. package/dist/category-b-namespace/index.js +120 -0
  6. package/dist/codebase/default-extractor.js +2 -1
  7. package/dist/codebase/map.js +2 -1
  8. package/dist/codebase/provider.js +4 -2
  9. package/dist/content-contracts/standards/_helpers.js +1 -1
  10. package/dist/doctor/checks.js +2 -1
  11. package/dist/doctor/doctor-state.js +2 -2
  12. package/dist/encoding/patterns.js +19 -0
  13. package/dist/encoding/scan.js +3 -2
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +2 -0
  16. package/dist/init-deposit/refresh.js +2 -1
  17. package/dist/init-deposit/scaffold.js +2 -1
  18. package/dist/install-upgrade/index.d.ts +2 -0
  19. package/dist/install-upgrade/index.js +31 -2
  20. package/dist/intake/issue-emit.d.ts +1 -1
  21. package/dist/intake/issue-emit.js +3 -2
  22. package/dist/intake/issue-ingest.js +11 -8
  23. package/dist/intake/reconcile-issues.js +11 -4
  24. package/dist/layout/index.d.ts +2 -0
  25. package/dist/layout/index.js +2 -0
  26. package/dist/layout/resolve.d.ts +87 -0
  27. package/dist/layout/resolve.js +159 -0
  28. package/dist/lifecycle/lifecycle-hygiene.d.ts +1 -1
  29. package/dist/lifecycle/lifecycle-hygiene.js +8 -5
  30. package/dist/orchestration/judgment-policy.js +4 -6
  31. package/dist/orchestration/verify-judgment-gates.d.ts +1 -0
  32. package/dist/orchestration/verify-judgment-gates.js +4 -2
  33. package/dist/policy/autonomy.js +4 -6
  34. package/dist/policy/capacity.js +4 -6
  35. package/dist/policy/decisions.d.ts +1 -0
  36. package/dist/policy/decisions.js +4 -2
  37. package/dist/policy/index.d.ts +1 -0
  38. package/dist/policy/index.js +3 -1
  39. package/dist/policy/plan-extensions.d.ts +35 -0
  40. package/dist/policy/plan-extensions.js +57 -0
  41. package/dist/policy/resolve.d.ts +6 -2
  42. package/dist/policy/resolve.js +18 -8
  43. package/dist/policy/wip.js +6 -4
  44. package/dist/preflight-cache/evaluate.js +8 -5
  45. package/dist/release/native-steps.js +4 -3
  46. package/dist/render/framework-commands.js +2 -1
  47. package/dist/render/prd-render.js +2 -2
  48. package/dist/render/project-render.js +23 -5
  49. package/dist/render/roadmap-render.js +36 -8
  50. package/dist/render/scope-outlook.js +3 -2
  51. package/dist/scope/audit-log.js +3 -2
  52. package/dist/scope/capacity-stamp.js +2 -1
  53. package/dist/scope/decompose.js +6 -4
  54. package/dist/scope/decomposed-refs.js +2 -1
  55. package/dist/scope/demote.js +5 -4
  56. package/dist/scope/project-context.js +2 -1
  57. package/dist/scope/project-definition-sync.js +4 -3
  58. package/dist/scope/transition.js +6 -2
  59. package/dist/scope/undo.js +2 -1
  60. package/dist/scope/vbrief-ref.js +5 -3
  61. package/dist/session/resume-conditions.js +2 -1
  62. package/dist/session/ritual-sentinel.js +2 -1
  63. package/dist/session/staleness.js +2 -1
  64. package/dist/slice/record.js +4 -4
  65. package/dist/spec-authority/resolver.js +5 -5
  66. package/dist/swarm/complete-cohort.js +5 -4
  67. package/dist/swarm/launch.js +6 -7
  68. package/dist/swarm/readiness.js +22 -20
  69. package/dist/swarm/subagent-backend.js +2 -5
  70. package/dist/swarm/verify-review-clean.js +2 -1
  71. package/dist/triage/actions/candidates-log.d.ts +3 -0
  72. package/dist/triage/actions/candidates-log.js +8 -2
  73. package/dist/triage/bootstrap/gitignore.js +22 -13
  74. package/dist/triage/bootstrap/index.js +7 -4
  75. package/dist/triage/bulk/index.js +2 -1
  76. package/dist/triage/classify/index.js +13 -11
  77. package/dist/triage/queue/audit.js +3 -3
  78. package/dist/triage/queue/cache.js +3 -2
  79. package/dist/triage/queue/ranking-labels.js +2 -1
  80. package/dist/triage/queue/scope-walk.js +8 -6
  81. package/dist/triage/reconcile/audit.js +2 -1
  82. package/dist/triage/reconcile/parse-uri.js +2 -1
  83. package/dist/triage/reconcile/reconcile.js +8 -7
  84. package/dist/triage/refresh/extract.js +4 -2
  85. package/dist/triage/refresh/refresh.js +3 -2
  86. package/dist/triage/scope/mutations-core.js +13 -9
  87. package/dist/triage/scope/renderers.js +5 -3
  88. package/dist/triage/scope/resolve.js +6 -5
  89. package/dist/triage/scope/validate.js +3 -6
  90. package/dist/triage/scope-drift/add-ignore.js +4 -2
  91. package/dist/triage/scope-drift/scope-rules.js +3 -2
  92. package/dist/triage/subscribe/index.js +13 -7
  93. package/dist/triage/summary/index.js +7 -5
  94. package/dist/triage/summary/reconcilable.js +6 -4
  95. package/dist/triage/summary/scope-drift.js +2 -1
  96. package/dist/triage/welcome/prior-state.js +9 -9
  97. package/dist/triage/welcome/summary.js +9 -8
  98. package/dist/triage/welcome/writers.js +12 -10
  99. package/dist/validate-content/capacity-policy.js +5 -4
  100. package/dist/validate-content/capacity-show.js +5 -3
  101. package/dist/validate-content/filename.d.ts +1 -1
  102. package/dist/validate-content/filename.js +5 -4
  103. package/dist/validate-content/validate-strategy-output.js +14 -5
  104. package/dist/vbrief-build/build.js +1 -1
  105. package/dist/vbrief-build/constants.js +7 -0
  106. package/dist/vbrief-build/speckit.js +3 -2
  107. package/dist/vbrief-reconcile/graph.js +3 -2
  108. package/dist/vbrief-reconcile/labels.js +4 -3
  109. package/dist/vbrief-reconcile/reconciliation.js +5 -1
  110. package/dist/vbrief-reconcile/swarm-deps.js +6 -5
  111. package/dist/vbrief-reconcile/umbrellas.d.ts +1 -1
  112. package/dist/vbrief-reconcile/umbrellas.js +9 -7
  113. package/dist/vbrief-validate/conformance.d.ts +1 -2
  114. package/dist/vbrief-validate/conformance.js +29 -14
  115. package/dist/vbrief-validate/constants.d.ts +18 -1
  116. package/dist/vbrief-validate/constants.js +32 -3
  117. package/dist/vbrief-validate/epic-links.js +3 -2
  118. package/dist/vbrief-validate/filename.js +5 -3
  119. package/dist/vbrief-validate/index.d.ts +3 -1
  120. package/dist/vbrief-validate/index.js +2 -1
  121. package/dist/vbrief-validate/main.js +17 -6
  122. package/dist/vbrief-validate/origin.js +1 -1
  123. package/dist/vbrief-validate/paths.js +2 -1
  124. package/dist/vbrief-validate/plan-hooks.js +4 -3
  125. package/dist/vbrief-validate/registry-status.d.ts +1 -0
  126. package/dist/vbrief-validate/registry-status.js +3 -1
  127. package/dist/vbrief-validate/roundtrip.d.ts +36 -0
  128. package/dist/vbrief-validate/roundtrip.js +168 -0
  129. package/dist/vbrief-validate/schema.d.ts +1 -1
  130. package/dist/vbrief-validate/schema.js +62 -19
  131. package/dist/vbrief-validate/validate-all.js +12 -4
  132. package/dist/verify-source/code-structure-validate.js +8 -4
  133. package/dist/verify-source/contract-drift.d.ts +7 -3
  134. package/dist/verify-source/contract-drift.js +40 -22
  135. package/dist/xbrief-migrate/constants.d.ts +13 -0
  136. package/dist/xbrief-migrate/constants.js +14 -0
  137. package/dist/xbrief-migrate/detect.d.ts +11 -0
  138. package/dist/xbrief-migrate/detect.js +81 -0
  139. package/dist/xbrief-migrate/drift-gate.d.ts +56 -0
  140. package/dist/xbrief-migrate/drift-gate.js +205 -0
  141. package/dist/xbrief-migrate/index.d.ts +7 -0
  142. package/dist/xbrief-migrate/index.js +7 -0
  143. package/dist/xbrief-migrate/migrate-project.d.ts +33 -0
  144. package/dist/xbrief-migrate/migrate-project.js +189 -0
  145. package/dist/xbrief-migrate/signpost.d.ts +5 -0
  146. package/dist/xbrief-migrate/signpost.js +19 -0
  147. package/dist/xbrief-migrate/transforms.d.ts +46 -0
  148. package/dist/xbrief-migrate/transforms.js +182 -0
  149. package/dist/xbrief-migrate/version.d.ts +8 -0
  150. package/dist/xbrief-migrate/version.js +25 -0
  151. package/package.json +11 -3
@@ -1,4 +1,6 @@
1
1
  import { resolve } from "node:path";
2
+ import { referenceTypeMatches } from "@deftai/directive-types";
3
+ import { hasArtifactSuffix, stripArtifactSuffix } from "../layout/resolve.js";
2
4
  /** Resolve a vBRIEF reference URI to an absolute path, or null. */
3
5
  export function resolveVbriefRef(uri, vbriefDir) {
4
6
  if (typeof uri !== "string" || uri.length === 0) {
@@ -48,7 +50,7 @@ export function collectChildUris(plan) {
48
50
  continue;
49
51
  }
50
52
  const r = ref;
51
- if (r.type !== "x-vbrief/plan") {
53
+ if (!referenceTypeMatches(String(r.type ?? ""), "plan")) {
52
54
  continue;
53
55
  }
54
56
  const uri = r.uri;
@@ -61,8 +63,8 @@ export function collectChildUris(plan) {
61
63
  /** Return registry IDs that may name a vBRIEF filename. */
62
64
  export function scopeIdsForFilename(filename) {
63
65
  let fullId;
64
- if (filename.endsWith(".vbrief.json")) {
65
- fullId = filename.slice(0, -".vbrief.json".length);
66
+ if (hasArtifactSuffix(filename)) {
67
+ fullId = stripArtifactSuffix(filename);
66
68
  }
67
69
  else {
68
70
  fullId = filename.replace(/\.[^.]+$/, "");
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
+ import { hasArtifactSuffix } from "../layout/resolve.js";
4
5
  export const RESUME_ELIGIBLE_DECISION = "resume-eligible";
5
6
  export const EVALUATOR_ACTOR = "agent:resume-evaluator";
6
7
  export const CACHE_DIR_NAME = ".deft-cache";
@@ -270,7 +271,7 @@ function countPending(projectRoot) {
270
271
  for (const name of readdirSync(folder)) {
271
272
  const full = join(folder, name);
272
273
  try {
273
- if (statSync(full).isFile() && name.endsWith(".vbrief.json")) {
274
+ if (statSync(full).isFile() && hasArtifactSuffix(name)) {
274
275
  count += 1;
275
276
  }
276
277
  }
@@ -1,5 +1,6 @@
1
1
  import { closeSync, existsSync, fdatasyncSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, statSync, writeSync, } from "node:fs";
2
2
  import { join, relative, resolve } from "node:path";
3
+ import { hasArtifactSuffix } from "../layout/resolve.js";
3
4
  import { stableJson } from "./json.js";
4
5
  import { parseTimestamp, timestampIso } from "./time.js";
5
6
  export const SCHEMA_VERSION = 1;
@@ -307,7 +308,7 @@ export function detectLatestActiveVbrief(projectRoot) {
307
308
  }
308
309
  const candidates = [];
309
310
  for (const name of children) {
310
- if (!name.endsWith(".vbrief.json")) {
311
+ if (!hasArtifactSuffix(name)) {
311
312
  continue;
312
313
  }
313
314
  const full = join(activeDir, name);
@@ -1,3 +1,4 @@
1
+ import { readPlanPolicy } from "../policy/plan-extensions.js";
1
2
  import { loadProjectDefinition } from "../policy/resolve.js";
2
3
  const DEFAULT_SESSION_RITUAL_STALENESS_HOURS = 4;
3
4
  function pythonTypeName(value) {
@@ -59,7 +60,7 @@ export function resolveSessionRitualStalenessHours(projectRoot) {
59
60
  error: "PROJECT-DEFINITION 'plan' is not an object",
60
61
  };
61
62
  }
62
- const policyBlock = plan.policy;
63
+ const policyBlock = readPlanPolicy(plan);
63
64
  if (typeof policyBlock !== "object" ||
64
65
  policyBlock === null ||
65
66
  Array.isArray(policyBlock) ||
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { appendFileSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, } from "node:fs";
3
- import { dirname, join, resolve } from "node:path";
4
- import { DEFAULT_SLICES_LOG_REL_PATH } from "./constants.js";
3
+ import { dirname, resolve } from "node:path";
4
+ import { resolveEvalPath } from "../layout/resolve.js";
5
5
  import { pythonJsonStringify } from "./json.js";
6
6
  import { withAppendLock } from "./lock.js";
7
7
  import { validateRecord } from "./validate.js";
@@ -16,7 +16,7 @@ const defaultDeps = {
16
16
  withLock: withAppendLock,
17
17
  };
18
18
  function resolvePath(path) {
19
- return path !== undefined ? resolve(path) : resolve(DEFAULT_SLICES_LOG_REL_PATH);
19
+ return path !== undefined ? resolve(path) : resolveEvalPath(process.cwd(), "slices.jsonl");
20
20
  }
21
21
  export function newSliceId() {
22
22
  return randomUUID();
@@ -134,6 +134,6 @@ export function findByUmbrella(umbrella, options = {}) {
134
134
  return readAll(options).filter((record) => record.umbrella === umbrella);
135
135
  }
136
136
  export function slicesPath(projectRoot) {
137
- return join(resolve(projectRoot), DEFAULT_SLICES_LOG_REL_PATH);
137
+ return resolveEvalPath(projectRoot, "slices.jsonl");
138
138
  }
139
139
  //# sourceMappingURL=record.js.map
@@ -1,16 +1,16 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { resolveLifecycleLayout, resolveLifecycleRoot, resolveProjectDefinitionPath, } from "../layout/resolve.js";
3
4
  import { SPEC_RENDER_BANNER } from "../render/constants.js";
4
5
  import { EXPORT_SPEC_PD_BANNER, GENERATED_SPEC_PURPOSE, GENERATED_SPEC_SOURCE_PD, GENERATED_SPEC_SOURCE_SPEC, } from "./constants.js";
5
- const SPEC_REL = join("vbrief", "specification.vbrief.json");
6
- const PD_REL = join("vbrief", "PROJECT-DEFINITION.vbrief.json");
7
6
  export function resolveSpecAuthority(projectRoot) {
8
7
  const root = projectRoot;
9
- const vbriefDir = join(root, "vbrief");
10
- const projectDefPath = join(root, PD_REL);
8
+ const layout = resolveLifecycleLayout(root);
9
+ const vbriefDir = resolveLifecycleRoot(root);
10
+ const projectDefPath = resolveProjectDefinitionPath(root);
11
11
  if (!existsSync(projectDefPath))
12
12
  return null;
13
- const specPath = join(root, SPEC_REL);
13
+ const specPath = join(vbriefDir, `specification${layout.artifactSuffix}`);
14
14
  const hasSpec = existsSync(specPath);
15
15
  const kind = hasSpec ? "full-spec" : "greenfield";
16
16
  const banner = kind === "full-spec" ? SPEC_RENDER_BANNER : EXPORT_SPEC_PD_BANNER;
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
+ import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
3
4
  import { detectLifecycleFolder } from "../scope/decomposed-refs.js";
4
5
  import { runTransition } from "../scope/transition.js";
5
6
  import { collectChildUris, collectPlanRefs, resolveVbriefRef } from "../scope/vbrief-ref.js";
@@ -41,7 +42,7 @@ function globResolve(pattern, projectRoot) {
41
42
  return [];
42
43
  }
43
44
  return readdirSync(dir)
44
- .filter((name) => name.endsWith(".vbrief.json") || glob === name)
45
+ .filter((name) => hasArtifactSuffix(name) || glob === name)
45
46
  .map((name) => resolve(dir, name))
46
47
  .filter((p) => existsSync(p));
47
48
  }
@@ -229,7 +230,7 @@ function completeParent(parentPath, vbriefDir, projectRoot, settled, dryRun) {
229
230
  };
230
231
  }
231
232
  export function sweepCohort(storyPaths, projectRoot, dryRun) {
232
- const vbriefDir = join(projectRoot, "vbrief");
233
+ const vbriefDir = resolveLifecycleRoot(projectRoot);
233
234
  const result = {
234
235
  project_root: resolve(projectRoot),
235
236
  dry_run: dryRun,
@@ -243,7 +244,7 @@ export function sweepCohort(storyPaths, projectRoot, dryRun) {
243
244
  const termDir = join(vbriefDir, term);
244
245
  if (existsSync(termDir)) {
245
246
  for (const name of readdirSync(termDir)) {
246
- if (name.endsWith(".vbrief.json")) {
247
+ if (hasArtifactSuffix(name)) {
247
248
  settled.add(resolve(join(termDir, name)));
248
249
  }
249
250
  }
@@ -358,7 +359,7 @@ export function completeCohort(args) {
358
359
  stderr: `Error: project root does not exist: ${projectRoot}\n`,
359
360
  };
360
361
  }
361
- if (!existsSync(join(projectRoot, "vbrief"))) {
362
+ if (!existsSync(resolveLifecycleRoot(projectRoot))) {
362
363
  return {
363
364
  exitCode: 2,
364
365
  stdout: "",
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { basename, join, resolve } from "node:path";
3
3
  import { inferGithubAuthMode } from "../intake/github-auth-modes.js";
4
4
  import { getPlatformCapabilities } from "../intake/platform-capabilities.js";
5
+ import { hasArtifactSuffix, resolveLifecycleFolder, stripArtifactSuffix, } from "../layout/resolve.js";
5
6
  import { evaluate as preflightEvaluate } from "../preflight/evaluate.js";
6
7
  import { issueNumbersFromPlan, scopeMetadataRank } from "../triage/queue/scope-walk.js";
7
8
  import { selectionOrderingKey } from "../triage/queue/selection.js";
@@ -45,9 +46,7 @@ function storyId(path, plan) {
45
46
  return value.trim();
46
47
  }
47
48
  const name = basename(path);
48
- return name.endsWith(".vbrief.json")
49
- ? name.slice(0, -".vbrief.json".length)
50
- : name.replace(/\.[^.]+$/, "");
49
+ return hasArtifactSuffix(name) ? stripArtifactSuffix(name) : name.replace(/\.[^.]+$/, "");
51
50
  }
52
51
  function extractHashNumbers(text) {
53
52
  const out = [];
@@ -128,13 +127,13 @@ function projectRel(projectRoot, path) {
128
127
  }
129
128
  }
130
129
  function indexActiveStories(projectRoot) {
131
- const activeDir = join(projectRoot, "vbrief", "active");
130
+ const activeDir = resolveLifecycleFolder(projectRoot, "active");
132
131
  const index = [];
133
132
  if (!existsSync(activeDir)) {
134
133
  return index;
135
134
  }
136
135
  for (const name of readdirSync(activeDir).sort()) {
137
- if (!name.endsWith(".vbrief.json")) {
136
+ if (!hasArtifactSuffix(name)) {
138
137
  continue;
139
138
  }
140
139
  const path = join(activeDir, name);
@@ -151,7 +150,7 @@ export function looksLikePath(token) {
151
150
  return (token.endsWith(".json") ||
152
151
  token.includes("/") ||
153
152
  token.includes("\\") ||
154
- (existsSync(token) && basename(token).endsWith(".vbrief.json")));
153
+ (existsSync(token) && hasArtifactSuffix(basename(token))));
155
154
  }
156
155
  function resolveOne(token, projectRoot, idMap, issueMap) {
157
156
  if (looksLikePath(token)) {
@@ -411,7 +410,7 @@ export function swarmLaunch(args) {
411
410
  stderr: "Error: no stories supplied. Pass --stories <ids|paths> and/or --paths <paths>.\n",
412
411
  };
413
412
  }
414
- if (!existsSync(join(projectRoot, "vbrief", "active"))) {
413
+ if (!existsSync(resolveLifecycleFolder(projectRoot, "active"))) {
415
414
  return {
416
415
  exitCode: EXIT_CONFIG_ERROR,
417
416
  stdout: "",
@@ -1,5 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { basename, join, resolve } from "node:path";
3
+ import { referenceTypeMatches } from "@deftai/directive-types";
4
+ import { ARTIFACT_SUFFIXES, hasArtifactSuffix, LIFECYCLE_DIR_NAMES, resolveLifecycleLayout, resolveLifecycleRoot, stripArtifactSuffix, } from "../layout/resolve.js";
3
5
  import { acceptanceTextsFromItems, asStrList, deprecatedSubitemsIssues, itemHasTraces, itemsHaveAcceptance, missingRequiredSwarmFields, storyQualityIssues, } from "../vbrief-validation/story-quality.js";
4
6
  import { LIFECYCLE_FOLDERS, READY } from "./constants.js";
5
7
  function loadJson(path) {
@@ -24,7 +26,8 @@ function projectRel(projectRoot, path) {
24
26
  }
25
27
  }
26
28
  function expandPaths(projectRoot, patterns) {
27
- const usePatterns = patterns.length > 0 ? patterns : ["vbrief/active/*.vbrief.json"];
29
+ const layout = resolveLifecycleLayout(projectRoot);
30
+ const usePatterns = patterns.length > 0 ? patterns : [`${layout.artifactDir}/active/*${layout.artifactSuffix}`];
28
31
  const out = [];
29
32
  const seen = new Set();
30
33
  for (const pattern of usePatterns) {
@@ -33,13 +36,14 @@ function expandPaths(projectRoot, patterns) {
33
36
  const globPart = pattern.includes("/")
34
37
  ? pattern.slice(pattern.lastIndexOf("/") + 1)
35
38
  : pattern;
36
- if (existsSync(join(projectRoot, "vbrief", "active")) && pattern.startsWith("vbrief/")) {
37
- const activeDir = join(projectRoot, pattern.split("/")[0] ?? "vbrief", pattern.split("/")[1] ?? "active");
38
- if (existsSync(activeDir)) {
39
- for (const name of readdirSync(activeDir)) {
40
- if (matchGlob(name, globPart)) {
41
- addPath(join(activeDir, name), seen, out);
42
- }
39
+ const firstSeg = pattern.split("/")[0] ?? "";
40
+ const secondSeg = pattern.split("/")[1] ?? "";
41
+ const lifecyclePattern = LIFECYCLE_DIR_NAMES.includes(firstSeg);
42
+ if (lifecyclePattern && existsSync(join(projectRoot, firstSeg, secondSeg))) {
43
+ const activeDir = join(projectRoot, firstSeg, secondSeg);
44
+ for (const name of readdirSync(activeDir)) {
45
+ if (matchGlob(name, globPart)) {
46
+ addPath(join(activeDir, name), seen, out);
43
47
  }
44
48
  }
45
49
  }
@@ -69,8 +73,10 @@ function dirnamePattern(projectRoot, pattern) {
69
73
  return idx >= 0 ? join(projectRoot, pattern.slice(0, idx)) : projectRoot;
70
74
  }
71
75
  function matchGlob(name, glob) {
72
- if (glob === "*.vbrief.json") {
73
- return name.endsWith(".vbrief.json");
76
+ // Layout-aware (#2109 part 1): an artifact-suffix glob matches either suffix,
77
+ // so a mixed/either lifecycle tree resolves the same way.
78
+ if (ARTIFACT_SUFFIXES.some((suffix) => glob === `*${suffix}`)) {
79
+ return hasArtifactSuffix(name);
74
80
  }
75
81
  if (glob.startsWith("*.")) {
76
82
  return name.endsWith(glob.slice(1));
@@ -115,7 +121,7 @@ function hasChildPlanRefs(plan) {
115
121
  return refs.some((ref) => typeof ref === "object" &&
116
122
  ref !== null &&
117
123
  !Array.isArray(ref) &&
118
- ref.type === "x-vbrief/plan");
124
+ referenceTypeMatches(String(ref.type ?? ""), "plan"));
119
125
  }
120
126
  function looksLikePhase(path, plan) {
121
127
  const title = String(plan.title ?? "");
@@ -153,9 +159,7 @@ function storyId(path, plan) {
153
159
  return value.trim();
154
160
  }
155
161
  const name = basename(path);
156
- return name.endsWith(".vbrief.json")
157
- ? name.slice(0, -".vbrief.json".length)
158
- : name.replace(/\.[^.]+$/, "");
162
+ return hasArtifactSuffix(name) ? stripArtifactSuffix(name) : name.replace(/\.[^.]+$/, "");
159
163
  }
160
164
  function hasTraces(plan, swarm) {
161
165
  const narratives = plan.narratives;
@@ -182,7 +186,7 @@ function hasTraces(plan, swarm) {
182
186
  if (typeof ref === "object" &&
183
187
  ref !== null &&
184
188
  !Array.isArray(ref) &&
185
- ref.type === "x-vbrief/spec-section") {
189
+ referenceTypeMatches(String(ref.type ?? ""), "spec-section")) {
186
190
  return true;
187
191
  }
188
192
  }
@@ -206,14 +210,14 @@ function acceptanceCountJustification(plan, swarm) {
206
210
  }
207
211
  function allScopeIds(projectRoot) {
208
212
  const ids = new Map();
209
- const vbriefDir = join(projectRoot, "vbrief");
213
+ const vbriefDir = resolveLifecycleRoot(projectRoot);
210
214
  for (const folder of LIFECYCLE_FOLDERS) {
211
215
  const dir = join(vbriefDir, folder);
212
216
  if (!existsSync(dir)) {
213
217
  continue;
214
218
  }
215
219
  for (const name of readdirSync(dir).sort()) {
216
- if (!name.endsWith(".vbrief.json")) {
220
+ if (!hasArtifactSuffix(name)) {
217
221
  continue;
218
222
  }
219
223
  const path = join(dir, name);
@@ -224,9 +228,7 @@ function allScopeIds(projectRoot) {
224
228
  const plan = planOf(data);
225
229
  const sid = storyId(path, plan);
226
230
  ids.set(sid, [path, String(plan.status ?? "")]);
227
- const stem = name.endsWith(".vbrief.json")
228
- ? name.slice(0, -".vbrief.json".length)
229
- : name.replace(/\.[^.]+$/, "");
231
+ const stem = stripArtifactSuffix(name);
230
232
  if (!ids.has(stem)) {
231
233
  ids.set(stem, [path, String(plan.status ?? "")]);
232
234
  }
@@ -11,6 +11,7 @@
11
11
  * @see {@link https://github.com/deftai/directive/issues/1739} Superseding PR
12
12
  * @see {@link https://github.com/deftai/directive/issues/1860} Hard deletion tracking
13
13
  */
14
+ import { readPlanPolicy } from "../policy/plan-extensions.js";
14
15
  import { loadProjectDefinition } from "../policy/resolve.js";
15
16
  import { LEAF_CODING_WORKER_ROLE, SUBAGENT_BACKEND_SET_CMD } from "./constants.js";
16
17
  const TRUTHY = new Set(["1", "true", "yes", "on"]);
@@ -63,11 +64,7 @@ export function probeSubagentBackends(environ = process.env) {
63
64
  });
64
65
  }
65
66
  function getPolicyBlock(data) {
66
- const plan = data.plan;
67
- if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
68
- return {};
69
- }
70
- const policy = plan.policy;
67
+ const policy = readPlanPolicy(data.plan);
71
68
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
72
69
  return {};
73
70
  }
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
+ import { hasArtifactSuffix } from "../layout/resolve.js";
3
4
  import { defaultRunGh, fetchGreptileCommentBody, fetchPrHeadSha, } from "../pr-merge-readiness/gh.js";
4
5
  import { evaluateGates, parseGreptileBody } from "../pr-merge-readiness/index.js";
5
6
  import { EXIT_EXTERNAL_ERROR, EXIT_OK, EXIT_UNCLEAN } from "./constants.js";
@@ -34,7 +35,7 @@ function globPaths(pattern) {
34
35
  return [];
35
36
  }
36
37
  return readdirSync(dir)
37
- .filter((name) => (glob === "*.vbrief.json" ? name.endsWith(".vbrief.json") : name === glob))
38
+ .filter((name) => (glob === "*.vbrief.json" ? hasArtifactSuffix(name) : name === glob))
38
39
  .map((name) => resolve(dir, name));
39
40
  }
40
41
  return [resolve(pattern)];
@@ -1,5 +1,8 @@
1
1
  import type { AuditEntry, CandidatesLog } from "./types.js";
2
+ /** Display/back-compat constant; resolution flows through resolveEvalPath (#2109). */
2
3
  export declare const AUDIT_LOG_REL_PATH = "vbrief/.eval/candidates.jsonl";
4
+ /** Layout-aware candidates audit-log path (xbrief when migrated, else vbrief). */
5
+ export declare function resolveCandidatesLogPath(projectRoot: string): string;
3
6
  /** Read every well-formed audit-log row in insertion order (#1698 shared reader). */
4
7
  export declare function readAuditLog(logPath: string, repo?: string | null): AuditEntry[];
5
8
  /** Return every entry for ``(repo, issue_number)`` in insertion order. */
@@ -1,8 +1,14 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { resolveEvalPath } from "../../layout/resolve.js";
4
5
  import { CandidatesLogError } from "./errors.js";
6
+ /** Display/back-compat constant; resolution flows through resolveEvalPath (#2109). */
5
7
  export const AUDIT_LOG_REL_PATH = "vbrief/.eval/candidates.jsonl";
8
+ /** Layout-aware candidates audit-log path (xbrief when migrated, else vbrief). */
9
+ export function resolveCandidatesLogPath(projectRoot) {
10
+ return resolveEvalPath(projectRoot, "candidates.jsonl");
11
+ }
6
12
  const VALID_DECISIONS = new Set([
7
13
  "accept",
8
14
  "reject",
@@ -103,7 +109,7 @@ function resolveLogPath(projectRoot, override) {
103
109
  if (override !== undefined) {
104
110
  return override;
105
111
  }
106
- return join(projectRoot, AUDIT_LOG_REL_PATH);
112
+ return resolveCandidatesLogPath(projectRoot);
107
113
  }
108
114
  function stableStringify(entry) {
109
115
  const sortedKeys = Object.keys(entry).sort();
@@ -227,6 +233,6 @@ export function rollbackAuditEntry(decisionId, projectRoot, logPathOverride) {
227
233
  return removed;
228
234
  }
229
235
  export function resolveAuditLogPath(projectRoot) {
230
- return join(projectRoot, AUDIT_LOG_REL_PATH);
236
+ return resolveCandidatesLogPath(projectRoot);
231
237
  }
232
238
  //# sourceMappingURL=candidates-log.js.map
@@ -1,5 +1,10 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname } from "node:path";
2
+ import { dirname, relative } from "node:path";
3
+ import { resolveEvalPath } from "../../layout/resolve.js";
4
+ /** POSIX-style display path for `absPath` relative to `projectRoot` (#2109). */
5
+ function evalRelDisplay(projectRoot, absPath) {
6
+ return relative(projectRoot, absPath).split(/[\\/]/).join("/");
7
+ }
3
8
  export const GITIGNORE_LINE = ".deft-cache/";
4
9
  export const GITIGNORE_DEFT_RUNTIME_SENTINELS = [
5
10
  ".deft/ritual-state.json",
@@ -126,7 +131,6 @@ operators should know:
126
131
  - \`.gitattributes\` -- the \`merge=union\` rule.
127
132
  - \`scripts/candidates_log.py\` -- the writer for \`candidates.jsonl\`.
128
133
  `;
129
- const CANDIDATES_RELPATH = "vbrief/.eval/candidates.jsonl";
130
134
  function stepOutcome(name, ok, message, details = {}, error = null) {
131
135
  return { name, ok, message, error, details };
132
136
  }
@@ -307,10 +311,10 @@ function ensureGitattributesMergeUnion(gitattributesPath, stepName) {
307
311
  }
308
312
  return stepOutcome(stepName, true, "created .gitattributes with vbrief/.eval/*.jsonl merge=union", { gitattributes_appended: true, gitattributes_created: true });
309
313
  }
310
- function ensureEvalReadme(readmePath, stepName) {
314
+ function ensureEvalReadme(readmePath, readmeRel, stepName) {
311
315
  try {
312
316
  readFileSync(readmePath, { encoding: "utf8" });
313
- return stepOutcome(stepName, true, "vbrief/.eval/README.md already present (no-op)", {
317
+ return stepOutcome(stepName, true, `${readmeRel} already present (no-op)`, {
314
318
  readme_created: false,
315
319
  readme_already_present: true,
316
320
  });
@@ -325,7 +329,7 @@ function ensureEvalReadme(readmePath, stepName) {
325
329
  catch (exc) {
326
330
  return stepOutcome(stepName, false, `could not create ${readmePath}`, { readme_created: false }, String(exc));
327
331
  }
328
- return stepOutcome(stepName, true, "created vbrief/.eval/README.md (#1144 hybrid policy)", {
332
+ return stepOutcome(stepName, true, `created ${readmeRel} (#1144 hybrid policy)`, {
329
333
  readme_created: true,
330
334
  });
331
335
  }
@@ -333,7 +337,10 @@ function ensureEvalReadme(readmePath, stepName) {
333
337
  export function stepEnsureGitignoreEvalEntries(projectRoot) {
334
338
  const gitignorePath = `${projectRoot}/.gitignore`;
335
339
  const gitattributesPath = `${projectRoot}/.gitattributes`;
336
- const readmePath = `${projectRoot}/vbrief/.eval/README.md`;
340
+ // Layout-aware (#2109): resolve the README under the active lifecycle `.eval`
341
+ // dir (xbrief/ when migrated, else vbrief/) instead of a hardcoded vbrief/ path.
342
+ const readmePath = resolveEvalPath(projectRoot, "README.md");
343
+ const readmeRel = evalRelDisplay(projectRoot, readmePath);
337
344
  const stepName = "ensure_gitignore_eval_entries";
338
345
  const details = {};
339
346
  const giResult = ensureGitignoreSelectiveEntries(gitignorePath, stepName);
@@ -348,7 +355,7 @@ export function stepEnsureGitignoreEvalEntries(projectRoot) {
348
355
  return stepOutcome(stepName, false, gaResult.message, details, gaResult.error ?? null);
349
356
  }
350
357
  Object.assign(details, gaResult.details);
351
- const rdResult = ensureEvalReadme(readmePath, stepName);
358
+ const rdResult = ensureEvalReadme(readmePath, readmeRel, stepName);
352
359
  if (!rdResult.ok) {
353
360
  Object.assign(details, rdResult.details);
354
361
  return stepOutcome(stepName, false, rdResult.message, details, rdResult.error ?? null);
@@ -361,7 +368,7 @@ export function stepEnsureGitignoreEvalEntries(projectRoot) {
361
368
  if (appendedLines === 0 && !appendedAttr && !createdReadme) {
362
369
  message =
363
370
  ".gitignore selective entries, .gitattributes merge=union, " +
364
- "and vbrief/.eval/README.md already present (#1144 hybrid " +
371
+ `and ${readmeRel} already present (#1144 hybrid ` +
365
372
  "policy satisfied; no-op)";
366
373
  }
367
374
  else {
@@ -373,7 +380,7 @@ export function stepEnsureGitignoreEvalEntries(projectRoot) {
373
380
  if (appendedAttr)
374
381
  parts.push(".gitattributes merge=union rule");
375
382
  if (createdReadme)
376
- parts.push("vbrief/.eval/README.md");
383
+ parts.push(readmeRel);
377
384
  message = `wrote ${parts.join(" + ")} per #1144 hybrid policy`;
378
385
  }
379
386
  message += formatBlanketWarning(Boolean(details.blanket_present));
@@ -381,7 +388,10 @@ export function stepEnsureGitignoreEvalEntries(projectRoot) {
381
388
  }
382
389
  /** Ensure `vbrief/.eval/candidates.jsonl` exists (#1240 option A). */
383
390
  export function stepSeedCandidatesLog(projectRoot) {
384
- const auditPath = `${projectRoot}/${CANDIDATES_RELPATH}`;
391
+ // Layout-aware (#2109): seed under the active lifecycle `.eval` dir (xbrief/
392
+ // when migrated, else vbrief/) instead of a hardcoded vbrief/ path.
393
+ const auditPath = resolveEvalPath(projectRoot, "candidates.jsonl");
394
+ const auditRel = evalRelDisplay(projectRoot, auditPath);
385
395
  const auditDir = dirname(auditPath);
386
396
  try {
387
397
  mkdirSync(auditDir, { recursive: true });
@@ -391,8 +401,7 @@ export function stepSeedCandidatesLog(projectRoot) {
391
401
  }
392
402
  try {
393
403
  readFileSync(auditPath, { encoding: "utf8" });
394
- const relative = CANDIDATES_RELPATH;
395
- return stepOutcome("seed_candidates_log", true, `${relative} already present (no-op)`, {
404
+ return stepOutcome("seed_candidates_log", true, `${auditRel} already present (no-op)`, {
396
405
  created: false,
397
406
  already_present: true,
398
407
  });
@@ -406,7 +415,7 @@ export function stepSeedCandidatesLog(projectRoot) {
406
415
  catch (exc) {
407
416
  return stepOutcome("seed_candidates_log", false, `could not seed ${auditPath}`, {}, String(exc));
408
417
  }
409
- return stepOutcome("seed_candidates_log", true, `created empty ${CANDIDATES_RELPATH}`, {
418
+ return stepOutcome("seed_candidates_log", true, `created empty ${auditRel}`, {
410
419
  created: true,
411
420
  already_present: false,
412
421
  });
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { promisify } from "node:util";
6
+ import { hasArtifactSuffix, resolveEvalPath, resolveLifecycleRoot } from "../../layout/resolve.js";
6
7
  import { SUBPROCESS_MAX_BUFFER } from "../../subprocess/max-buffer.js";
7
8
  import { stepEnsureGitignoreEntry, stepEnsureGitignoreEvalEntries, stepSeedCandidatesLog, } from "./gitignore.js";
8
9
  import { PROGRESS_DEFAULT } from "./types.js";
@@ -296,7 +297,9 @@ function extractIssueNumber(vbriefData) {
296
297
  if (typeof ref !== "object" || ref === null)
297
298
  continue;
298
299
  const typed = ref;
299
- if (typed.type !== "x-vbrief/github-issue")
300
+ // Dual-namespace (#2109): accept the migrated `x-xbrief/` corpus token and
301
+ // the legacy `x-vbrief/` token (still read-accepted for back-compat).
302
+ if (typed.type !== "x-xbrief/github-issue" && typed.type !== "x-vbrief/github-issue")
300
303
  continue;
301
304
  const uri = typed.uri;
302
305
  if (typeof uri !== "string")
@@ -312,7 +315,7 @@ function scanLifecycleFolder(folder) {
312
315
  if (!existsSync(folder))
313
316
  return results;
314
317
  const entries = readdirSync(folder)
315
- .filter((name) => name.endsWith(".vbrief.json"))
318
+ .filter((name) => hasArtifactSuffix(name))
316
319
  .sort();
317
320
  for (const name of entries) {
318
321
  const path = join(folder, name);
@@ -370,11 +373,11 @@ export function stepBackfillAuditLog(projectRoot, repo, options = {}) {
370
373
  if (repo === null) {
371
374
  return stepOutcome("backfill_audit_log", true, "skipped (no --repo provided; pass --repo OWNER/NAME to backfill)", { skipped: "no-repo" });
372
375
  }
373
- const vbriefRoot = join(projectRoot, "vbrief");
376
+ const vbriefRoot = resolveLifecycleRoot(projectRoot);
374
377
  if (!existsSync(vbriefRoot)) {
375
378
  return stepOutcome("backfill_audit_log", true, `skipped (no vbrief/ directory under ${projectRoot})`, { skipped: "no-vbrief" });
376
379
  }
377
- const auditPath = join(projectRoot, AUDIT_LOG_RELPATH);
380
+ const auditPath = resolveEvalPath(projectRoot, "candidates.jsonl");
378
381
  const alreadyLogged = existingAuditIssueNumbers(auditPath);
379
382
  const nowIso = options.nowIso ?? nowIsoDefault;
380
383
  const appendEntry = options.appendAuditEntry ?? appendAuditEntryDefault;
@@ -1,6 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { existsSync, readdirSync, readFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
+ import { resolveEvalPath } from "../../layout/resolve.js";
4
5
  export const ACTION_FN_NAMES = {
5
6
  accept: "accept",
6
7
  reject: "reject",
@@ -375,7 +376,7 @@ export function createFilesystemCacheModule() {
375
376
  CacheError,
376
377
  };
377
378
  }
378
- export function createFilesystemCandidatesLogModule(logPath = "vbrief/.eval/candidates.jsonl") {
379
+ export function createFilesystemCandidatesLogModule(logPath = resolveEvalPath(process.cwd(), "candidates.jsonl")) {
379
380
  return {
380
381
  readAll(options) {
381
382
  if (!existsSync(logPath)) {