@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,5 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { referenceTypeMatches } from "@deftai/directive-types";
4
+ import { hasArtifactSuffix, resolveEvalPath, resolveLifecycleRoot } from "../../layout/resolve.js";
3
5
  import { AUDIT_LOG_REL_PATH, readAuditLog } from "../actions/candidates-log.js";
4
6
  /** Lifecycle folders scanned for reconcilable vBRIEFs (#1468). */
5
7
  export const BACKFILL_FOLDERS = ["proposed", "pending", "active"];
@@ -36,7 +38,7 @@ function extractIssueRef(data) {
36
38
  continue;
37
39
  }
38
40
  const obj = ref;
39
- if (obj.type !== "x-vbrief/github-issue") {
41
+ if (!referenceTypeMatches(String(obj.type ?? ""), "github-issue")) {
40
42
  continue;
41
43
  }
42
44
  const [repo, number] = parseGithubIssueUri(obj.uri);
@@ -61,11 +63,11 @@ function auditKey(repo, issueNumber) {
61
63
  */
62
64
  export function countReconcilable(projectRoot, options = {}) {
63
65
  try {
64
- const auditPath = options.auditLogPath ?? join(projectRoot, CANDIDATES_LOG_REL_PATH);
66
+ const auditPath = options.auditLogPath ?? resolveEvalPath(projectRoot, "candidates.jsonl");
65
67
  const existing = existingAuditRefs(auditPath);
66
68
  const defaultRepo = options.defaultRepo ?? null;
67
69
  const keys = new Set();
68
- const vbriefRoot = join(projectRoot, "vbrief");
70
+ const vbriefRoot = resolveLifecycleRoot(projectRoot);
69
71
  for (const folderName of BACKFILL_FOLDERS) {
70
72
  const folder = join(vbriefRoot, folderName);
71
73
  if (!existsSync(folder)) {
@@ -73,7 +75,7 @@ export function countReconcilable(projectRoot, options = {}) {
73
75
  }
74
76
  const entries = readdirSync(folder, { withFileTypes: true });
75
77
  for (const entry of entries) {
76
- if (!entry.isFile() || !entry.name.endsWith(".vbrief.json")) {
78
+ if (!entry.isFile() || !hasArtifactSuffix(entry.name)) {
77
79
  continue;
78
80
  }
79
81
  const path = join(folder, entry.name);
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { readPlanPolicy } from "../../policy/plan-extensions.js";
3
4
  import { loadProjectDefinition } from "../../policy/resolve.js";
4
5
  const DRIFT_MIN_ISSUES = 3;
5
6
  const CACHE_SOURCE = "github-issue";
@@ -88,7 +89,7 @@ function resolveScopeRules(projectRoot) {
88
89
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
89
90
  return [{ rule: "all-open" }];
90
91
  }
91
- const policy = plan.policy;
92
+ const policy = readPlanPolicy(plan);
92
93
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
93
94
  return [{ rule: "all-open" }];
94
95
  }
@@ -1,8 +1,10 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
- import { CACHE_DIR_NAME, CACHE_SOURCE, CANDIDATES_RELPATH, DEFAULT_WIP_CAP, SUBSCRIPTION_PRESETS, WIP_LIFECYCLE_DIRS, } from "./constants.js";
3
+ import { hasArtifactSuffix, resolveAuditPath, resolveEvalPath, resolveLifecycleRoot, resolveProjectDefinitionPath, } from "../../layout/resolve.js";
4
+ import { readPlanPolicy } from "../../policy/plan-extensions.js";
5
+ import { CACHE_DIR_NAME, CACHE_SOURCE, DEFAULT_WIP_CAP, SUBSCRIPTION_PRESETS, WIP_LIFECYCLE_DIRS, } from "./constants.js";
4
6
  function loadProjectDefinition(projectRoot) {
5
- const path = join(resolve(projectRoot), "vbrief", "PROJECT-DEFINITION.vbrief.json");
7
+ const path = resolveProjectDefinitionPath(projectRoot);
6
8
  if (!existsSync(path))
7
9
  return null;
8
10
  try {
@@ -37,17 +39,17 @@ function countCacheEntries(projectRoot) {
37
39
  return count;
38
40
  }
39
41
  export function candidatesLogPath(projectRoot) {
40
- return join(resolve(projectRoot), ...CANDIDATES_RELPATH);
42
+ return resolveEvalPath(projectRoot, "candidates.jsonl");
41
43
  }
42
44
  function countWip(projectRoot) {
43
45
  let total = 0;
44
- const root = join(resolve(projectRoot), "vbrief");
46
+ const root = resolveLifecycleRoot(projectRoot);
45
47
  for (const sub of WIP_LIFECYCLE_DIRS) {
46
48
  const folder = join(root, sub);
47
49
  if (!existsSync(folder))
48
50
  continue;
49
51
  for (const child of readdirSync(folder, { withFileTypes: true })) {
50
- if (child.isFile() && child.name.endsWith(".vbrief.json"))
52
+ if (child.isFile() && hasArtifactSuffix(child.name))
51
53
  total += 1;
52
54
  }
53
55
  }
@@ -80,7 +82,7 @@ function summarizeScope(rules) {
80
82
  return [true, `custom (${rules.length} rule(s))`];
81
83
  }
82
84
  function countPendingDecisions(projectRoot) {
83
- const logPath = join(resolve(projectRoot), "vbrief", ".audit", "pending-human-decisions.jsonl");
85
+ const logPath = resolveAuditPath(projectRoot, "pending-human-decisions.jsonl");
84
86
  if (!existsSync(logPath))
85
87
  return 0;
86
88
  let text;
@@ -121,9 +123,7 @@ export function pendingDecisionsNudgeLine(count, threshold = 3) {
121
123
  export function detectPriorState(projectRoot) {
122
124
  const data = loadProjectDefinition(projectRoot) ?? {};
123
125
  const plan = data.plan;
124
- const policy = typeof plan === "object" && plan !== null && !Array.isArray(plan)
125
- ? plan.policy
126
- : null;
126
+ const policy = readPlanPolicy(plan);
127
127
  const rawScope = typeof policy === "object" && policy !== null && !Array.isArray(policy)
128
128
  ? policy.triageScope
129
129
  : null;
@@ -1,11 +1,12 @@
1
1
  import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
+ import { hasArtifactSuffix, resolveEvalPath, resolveLifecycleFolder, resolveProjectDefinitionPath, } from "../../layout/resolve.js";
4
+ import { readPlanPolicy } from "../../policy/plan-extensions.js";
3
5
  import { countVbriefWip, DEFAULT_WIP_CAP, resolveWipCap } from "../../policy/wip.js";
4
6
  import { countReconcilable } from "../reconcile/reconcile.js";
5
7
  import { computeDrift } from "../scope-drift/compute.js";
6
- import { SUMMARY_HISTORY_REL_PATH, shouldSuppressD2Emission } from "../summary/index.js";
8
+ import { shouldSuppressD2Emission } from "../summary/index.js";
7
9
  import { CACHE_DIR_NAME, CACHE_SOURCE, EMPTY_CACHE_LINE, MAX_LINE_CHARS, SUMMARY_HISTORY_SCHEMA, WIP_WARN_GLYPH, } from "./constants.js";
8
- const CANDIDATES_LOG_REL_PATH = "vbrief/.eval/candidates.jsonl";
9
10
  const FILESYSTEM_IN_FLIGHT_STATUS = "running";
10
11
  const TRIAGED_DECISIONS = new Set([
11
12
  "accept",
@@ -72,12 +73,12 @@ function latestDecisions(entries) {
72
73
  return out;
73
74
  }
74
75
  function countFilesystemInFlight(projectRoot) {
75
- const activeDir = join(resolve(projectRoot), "vbrief", "active");
76
+ const activeDir = resolveLifecycleFolder(projectRoot, "active");
76
77
  if (!existsSync(activeDir))
77
78
  return 0;
78
79
  let count = 0;
79
80
  for (const name of readdirSync(activeDir)) {
80
- if (!name.endsWith(".vbrief.json"))
81
+ if (!hasArtifactSuffix(name))
81
82
  continue;
82
83
  try {
83
84
  const data = JSON.parse(readFileSync(join(activeDir, name), "utf8"));
@@ -92,7 +93,7 @@ function countFilesystemInFlight(projectRoot) {
92
93
  return count;
93
94
  }
94
95
  function isTriageScopeConfigured(projectRoot) {
95
- const path = join(resolve(projectRoot), "vbrief", "PROJECT-DEFINITION.vbrief.json");
96
+ const path = resolveProjectDefinitionPath(projectRoot);
96
97
  if (!existsSync(path))
97
98
  return false;
98
99
  try {
@@ -100,7 +101,7 @@ function isTriageScopeConfigured(projectRoot) {
100
101
  const plan = data.plan;
101
102
  if (typeof plan !== "object" || plan === null || Array.isArray(plan))
102
103
  return false;
103
- const scope = plan.policy;
104
+ const scope = readPlanPolicy(plan);
104
105
  if (typeof scope !== "object" || scope === null || Array.isArray(scope))
105
106
  return false;
106
107
  const triageScope = scope.triageScope;
@@ -118,7 +119,7 @@ function utcIso() {
118
119
  export function computeSummary(projectRoot) {
119
120
  const root = resolve(projectRoot);
120
121
  const cacheRoot = join(root, CACHE_DIR_NAME);
121
- const logPath = join(root, CANDIDATES_LOG_REL_PATH);
122
+ const logPath = resolveEvalPath(root, "candidates.jsonl");
122
123
  const cached = iterCachedIssues(cacheRoot);
123
124
  const repos = [...new Set(cached.map(([r]) => r))].sort().slice(0, 8);
124
125
  const wipCapResult = resolveWipCap(root);
@@ -292,7 +293,7 @@ export function appendHistory(historyPath, result, line) {
292
293
  export function emitOneliner(projectRoot, options = {}) {
293
294
  const result = computeSummary(projectRoot);
294
295
  const line = formatSummary(result);
295
- const historyPath = join(resolve(projectRoot), SUMMARY_HISTORY_REL_PATH);
296
+ const historyPath = resolveEvalPath(projectRoot, "summary-history.jsonl");
296
297
  const applySuppression = options.applyD2Suppression !== false;
297
298
  if (applySuppression && shouldSuppressD2Emission(result, historyPath, { now: options.now })) {
298
299
  return line;
@@ -1,8 +1,10 @@
1
1
  import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
- import { AUDIT_LOG_REL_PATH, DEFAULT_RELIEF_AGE_DAYS, DEFAULT_WIP_CAP, PROJECT_DEFINITION_REL_PATH, SUBSCRIPTION_PRESETS, WELCOME_AUDIT_TAG, } from "./constants.js";
3
+ import { hasArtifactSuffix, resolveLifecycleFolder, resolveProjectDefinitionPath, } from "../../layout/resolve.js";
4
+ import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
5
+ import { AUDIT_LOG_REL_PATH, DEFAULT_RELIEF_AGE_DAYS, DEFAULT_WIP_CAP, SUBSCRIPTION_PRESETS, WELCOME_AUDIT_TAG, } from "./constants.js";
4
6
  function projectDefinitionPath(projectRoot) {
5
- return join(resolve(projectRoot), PROJECT_DEFINITION_REL_PATH);
7
+ return resolveProjectDefinitionPath(projectRoot);
6
8
  }
7
9
  function utcIso() {
8
10
  return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
@@ -34,10 +36,11 @@ export function writeTriageScope(projectRoot, rules, options = { presetLabel: "c
34
36
  throw new Error("PROJECT-DEFINITION 'plan' is not an object");
35
37
  }
36
38
  const planRec = plan;
37
- let policy = planRec.policy;
39
+ migrateLegacyPolicyKey(planRec);
40
+ let policy = planRec[PLAN_POLICY_KEY];
38
41
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
39
42
  policy = {};
40
- planRec.policy = policy;
43
+ planRec[PLAN_POLICY_KEY] = policy;
41
44
  }
42
45
  const policyRec = policy;
43
46
  const previous = policyRec.triageScope;
@@ -68,10 +71,11 @@ export function writeWipCap(projectRoot, wipCap, options = {}) {
68
71
  throw new Error("PROJECT-DEFINITION 'plan' is not an object");
69
72
  }
70
73
  const planRec = plan;
71
- let policy = planRec.policy;
74
+ migrateLegacyPolicyKey(planRec);
75
+ let policy = planRec[PLAN_POLICY_KEY];
72
76
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
73
77
  policy = {};
74
- planRec.policy = policy;
78
+ planRec[PLAN_POLICY_KEY] = policy;
75
79
  }
76
80
  const policyRec = policy;
77
81
  const previous = policyRec.wipCap;
@@ -124,16 +128,14 @@ function daysInPending(path, now) {
124
128
  }
125
129
  }
126
130
  export function previewWipRelief(projectRoot, olderThanDays = DEFAULT_RELIEF_AGE_DAYS) {
127
- const pendingDir = join(resolve(projectRoot), "vbrief", "pending");
131
+ const pendingDir = resolveLifecycleFolder(projectRoot, "pending");
128
132
  if (!existsSync(pendingDir)) {
129
133
  return { olderThanDays, eligibleCount: 0, eligibleFiles: [], skippedCount: 0 };
130
134
  }
131
135
  const now = new Date();
132
136
  const eligible = [];
133
137
  let skipped = 0;
134
- for (const name of [...readdirSync(pendingDir)]
135
- .filter((n) => n.endsWith(".vbrief.json"))
136
- .sort()) {
138
+ for (const name of [...readdirSync(pendingDir)].filter((n) => hasArtifactSuffix(n)).sort()) {
137
139
  const path = join(pendingDir, name);
138
140
  const days = daysInPending(path, now);
139
141
  if (days >= olderThanDays)
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { resolveAuditPath } from "../layout/resolve.js";
3
+ import { readPlanPolicy } from "../policy/plan-extensions.js";
3
4
  import { loadProjectDefinition } from "../policy/resolve.js";
4
5
  export const DEFAULT_CAPACITY_UNIT = "vbrief-count";
5
6
  export const CAPACITY_UNIT_COST = "cost";
@@ -139,7 +140,7 @@ export function resolveCapacityAllocation(projectRoot) {
139
140
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
140
141
  return defaultCapacityAllocation("default");
141
142
  }
142
- const policy = plan.policy;
143
+ const policy = readPlanPolicy(plan);
143
144
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
144
145
  return defaultCapacityAllocation("default");
145
146
  }
@@ -197,7 +198,7 @@ export function resolveAutonomy(projectRoot) {
197
198
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
198
199
  return defaultAutonomyPolicy("default");
199
200
  }
200
- const policy = plan.policy;
201
+ const policy = readPlanPolicy(plan);
201
202
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
202
203
  return defaultAutonomyPolicy("default");
203
204
  }
@@ -243,7 +244,7 @@ function parseIsoTs(value) {
243
244
  }
244
245
  }
245
246
  export function readDecisionEvents(projectRoot) {
246
- const path = join(projectRoot, "vbrief", ".audit", "pending-human-decisions.jsonl");
247
+ const path = resolveAuditPath(projectRoot, "pending-human-decisions.jsonl");
247
248
  if (!existsSync(path))
248
249
  return [];
249
250
  const out = [];
@@ -1,5 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { referenceTypeMatches } from "@deftai/directive-types";
4
+ import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
3
5
  import { CAPACITY_UNIT_COST, DEFAULT_CAPACITY_UNIT, DEFAULT_PENDING_DECISIONS_THRESHOLD, pendingDecisionsNudgeLine, recommendAutonomyLevel, resolveAutonomy, resolveCapacityAllocation, summarizeDecisionBacklog, } from "./capacity-policy.js";
4
6
  export const FORWARD_FOLDERS = ["pending", "active"];
5
7
  export const BACKWARD_FOLDER = "completed";
@@ -25,7 +27,7 @@ function planHasChildren(plan) {
25
27
  return false;
26
28
  return refs.some((ref) => typeof ref === "object" &&
27
29
  ref !== null &&
28
- ref.type === "x-vbrief/plan");
30
+ referenceTypeMatches(String(ref.type ?? ""), "plan"));
29
31
  }
30
32
  function coerceCost(value) {
31
33
  if (typeof value === "boolean")
@@ -96,7 +98,7 @@ export function iterVbriefPlans(vbriefRoot) {
96
98
  if (!existsSync(folderPath) || !statSync(folderPath).isDirectory())
97
99
  continue;
98
100
  for (const name of readdirSync(folderPath).sort()) {
99
- if (!name.endsWith(".vbrief.json"))
101
+ if (!hasArtifactSuffix(name))
100
102
  continue;
101
103
  const child = join(folderPath, name);
102
104
  try {
@@ -145,7 +147,7 @@ export function bucketDeficit(report, tally) {
145
147
  export function computeReport(projectRoot, options = {}) {
146
148
  const now = options.now ?? new Date();
147
149
  const allocation = options.allocation ?? resolveCapacityAllocation(projectRoot);
148
- const vbriefRoot = join(projectRoot, "vbrief");
150
+ const vbriefRoot = resolveLifecycleRoot(projectRoot);
149
151
  const records = iterVbriefPlans(vbriefRoot).map(({ folder, plan }) => classifyRecord(plan, folder, allocation, now));
150
152
  const tallies = new Map();
151
153
  for (const bucket of allocation.buckets) {
@@ -1,3 +1,3 @@
1
- /** Linear filename validator for YYYY-MM-DD-<slug>.vbrief.json (no nested regex). */
1
+ /** Linear filename validator for YYYY-MM-DD-<slug>.{vbrief,xbrief}.json (no nested regex). */
2
2
  export declare function isDatePrefixedVbriefFilename(name: string): boolean;
3
3
  //# sourceMappingURL=filename.d.ts.map
@@ -1,9 +1,10 @@
1
- /** Linear filename validator for YYYY-MM-DD-<slug>.vbrief.json (no nested regex). */
1
+ import { hasArtifactSuffix, stripArtifactSuffix } from "../layout/resolve.js";
2
+ /** Linear filename validator for YYYY-MM-DD-<slug>.{vbrief,xbrief}.json (no nested regex). */
2
3
  export function isDatePrefixedVbriefFilename(name) {
3
- const suffix = ".vbrief.json";
4
- if (!name.endsWith(suffix))
4
+ // Layout-aware (#2109 part 2a): accept either artifact suffix.
5
+ if (!hasArtifactSuffix(name))
5
6
  return false;
6
- const base = name.slice(0, -suffix.length);
7
+ const base = stripArtifactSuffix(name);
7
8
  if (base.length < 12)
8
9
  return false;
9
10
  const y = base.slice(0, 4);
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, statSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
+ import { hasArtifactSuffix, resolveLifecycleLayout, resolveProjectDefinitionPath, } from "../layout/resolve.js";
3
4
  import { checkSpecMigrationFidelity } from "../spec-authority/migration-fidelity.js";
4
5
  import { isFullSpecState } from "../spec-authority/resolver.js";
5
6
  import { isDatePrefixedVbriefFilename } from "./filename.js";
@@ -26,7 +27,10 @@ function isDeftFrameworkRoot(projectRoot) {
26
27
  export function validateStrategyOutput(projectRoot, strict = false) {
27
28
  const root = resolve(projectRoot);
28
29
  const errors = [];
29
- const vbriefDir = join(root, "vbrief");
30
+ // Layout-aware (#2109 part 2a): resolve the lifecycle dir/suffix dynamically;
31
+ // identical to vbrief/ on today's tree.
32
+ const layout = resolveLifecycleLayout(root);
33
+ const vbriefDir = layout.root;
30
34
  if (!existsSync(vbriefDir)) {
31
35
  if (strict) {
32
36
  errors.push("vbrief/ directory missing entirely. v0.20 strategies must emit at least " +
@@ -34,13 +38,18 @@ export function validateStrategyOutput(projectRoot, strict = false) {
34
38
  }
35
39
  return errors;
36
40
  }
37
- const projDef = join(vbriefDir, "PROJECT-DEFINITION.vbrief.json");
41
+ const projDef = resolveProjectDefinitionPath(root);
38
42
  if (!existsSync(projDef)) {
39
- errors.push("Missing vbrief/PROJECT-DEFINITION.vbrief.json. " +
43
+ errors.push(`Missing ${layout.artifactDir}/PROJECT-DEFINITION${layout.artifactSuffix}. ` +
40
44
  "All v0.20-conformant strategy output must include a complete project definition " +
41
45
  "(see v0-20-contract.md and task project:render).");
42
46
  }
43
- const specLegacy = join(vbriefDir, "specification.vbrief.json");
47
+ // The legacy-spec guard intentionally stays pinned to the literal legacy
48
+ // path (#2109 part 2a): its job is to detect a stranded pre-v0.20
49
+ // vbrief/specification.vbrief.json, which is a legacy artifact regardless of
50
+ // the active layout -- routing it through the resolver would make it
51
+ // self-cancelling once an xbrief tree is live.
52
+ const specLegacy = join(root, "vbrief", "specification.vbrief.json");
44
53
  if (existsSync(specLegacy) && !isDeftFrameworkRoot(root) && !isFullSpecState(root)) {
45
54
  errors.push("Legacy artifact vbrief/specification.vbrief.json present. " +
46
55
  "v0.20 strategies MUST NOT dual-write the old specification.vbrief.json " +
@@ -52,7 +61,7 @@ export function validateStrategyOutput(projectRoot, strict = false) {
52
61
  if (!existsSync(dpath) || !statSync(dpath).isDirectory())
53
62
  continue;
54
63
  for (const name of readdirSync(dpath).sort()) {
55
- if (!name.endsWith(".vbrief.json"))
64
+ if (!hasArtifactSuffix(name))
56
65
  continue;
57
66
  if (!isDatePrefixedVbriefFilename(name)) {
58
67
  errors.push(`Non-conformant filename in vbrief/${dname}/: ${name}. ` +
@@ -57,7 +57,7 @@ function githubIssueReference(params) {
57
57
  : `Issue #${cleanedNumber}`;
58
58
  return {
59
59
  uri: `${cleanedRepo}/issues/${cleanedNumber}`,
60
- type: "x-vbrief/github-issue",
60
+ type: "x-xbrief/github-issue",
61
61
  title: refTitle,
62
62
  };
63
63
  }
@@ -8,12 +8,19 @@ export const INTERNAL_REFERENCE_TYPES = new Set([
8
8
  "x-vbrief/plan",
9
9
  "x-vbrief/spec-section",
10
10
  "x-vbrief/user-request",
11
+ "x-xbrief/plan",
12
+ "x-xbrief/spec-section",
13
+ "x-xbrief/user-request",
11
14
  ]);
12
15
  export const EXTERNAL_REFERENCE_TYPES = new Set([
13
16
  "x-vbrief/github-issue",
14
17
  "x-vbrief/github-pr",
15
18
  "x-vbrief/jira-ticket",
16
19
  "x-vbrief/web-page",
20
+ "x-xbrief/github-issue",
21
+ "x-xbrief/github-pr",
22
+ "x-xbrief/jira-ticket",
23
+ "x-xbrief/web-page",
17
24
  ]);
18
25
  export const FOLDER_TO_STATUSES = {
19
26
  proposed: ["draft", "proposed"],
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { basename } from "node:path";
3
+ import { referenceTypeMatches } from "@deftai/directive-types";
3
4
  import { referenceWithDefaultTrust, slugify } from "./build.js";
4
5
  import { EMITTED_VBRIEF_VERSION } from "./constants.js";
5
6
  import { pythonJsonPretty } from "./json.js";
@@ -99,7 +100,7 @@ export function createSpeckitScopeVbrief(item, options) {
99
100
  }
100
101
  }
101
102
  const references = [
102
- referenceWithDefaultTrust({ type: "x-vbrief/plan", uri: specRef }),
103
+ referenceWithDefaultTrust({ type: "x-xbrief/plan", uri: specRef }),
103
104
  ];
104
105
  const itemRefs = item.references;
105
106
  if (Array.isArray(itemRefs)) {
@@ -107,7 +108,7 @@ export function createSpeckitScopeVbrief(item, options) {
107
108
  if (typeof ref === "object" &&
108
109
  ref !== null &&
109
110
  !Array.isArray(ref) &&
110
- ref.type !== "x-vbrief/plan") {
111
+ !referenceTypeMatches(String(ref.type ?? ""), "plan")) {
111
112
  references.push(referenceWithDefaultTrust(ref));
112
113
  }
113
114
  }
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
+ import { hasArtifactSuffix, resolveLifecycleFolder } from "../layout/resolve.js";
3
4
  import { countVbriefWip, resolveWipCap } from "../policy/wip.js";
4
5
  import { runTransition } from "../scope/transition.js";
5
6
  import { allScopeIds, asStrList, candidateDepGraph, candidateFromPath, markCycles, } from "./swarm-deps.js";
@@ -27,7 +28,7 @@ function candidateInCycle(candidate) {
27
28
  }
28
29
  export function reconcileGraph(projectRoot, options = {}) {
29
30
  const root = resolve(projectRoot);
30
- const proposedDir = join(root, "vbrief", "proposed");
31
+ const proposedDir = resolveLifecycleFolder(root, "proposed");
31
32
  if (!existsSync(proposedDir)) {
32
33
  return [
33
34
  2,
@@ -45,7 +46,7 @@ export function reconcileGraph(projectRoot, options = {}) {
45
46
  ];
46
47
  }
47
48
  const candidatePaths = readdirSync(proposedDir)
48
- .filter((f) => f.endsWith(".vbrief.json"))
49
+ .filter((f) => hasArtifactSuffix(f))
49
50
  .sort()
50
51
  .map((f) => join(proposedDir, f));
51
52
  const candidates = candidatePaths
@@ -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, stripArtifactSuffix } from "../layout/resolve.js";
3
4
  import { call } from "../scm/call.js";
4
5
  import { extractIssueRef } from "../triage/reconcile/parse-uri.js";
5
6
  import { depResolved, RESOLVED_FOLDERS } from "./graph.js";
@@ -83,7 +84,7 @@ function hasUnresolvedDeps(swarm, knownIds) {
83
84
  }
84
85
  export function reconcileLabels(projectRoot, options = {}) {
85
86
  const root = resolve(projectRoot);
86
- const vbriefDir = join(root, "vbrief");
87
+ const vbriefDir = resolveLifecycleRoot(root);
87
88
  if (!existsSync(vbriefDir)) {
88
89
  return [
89
90
  2,
@@ -111,7 +112,7 @@ export function reconcileLabels(projectRoot, options = {}) {
111
112
  if (!existsSync(folderPath))
112
113
  continue;
113
114
  const files = readdirSync(folderPath)
114
- .filter((f) => f.endsWith(".vbrief.json"))
115
+ .filter((f) => hasArtifactSuffix(f))
115
116
  .sort();
116
117
  for (const file of files) {
117
118
  const path = join(folderPath, file);
@@ -127,7 +128,7 @@ export function reconcileLabels(projectRoot, options = {}) {
127
128
  const plan = typeof data.plan === "object" && data.plan !== null && !Array.isArray(data.plan)
128
129
  ? data.plan
129
130
  : {};
130
- const storyId = String(plan.id ?? file.slice(0, -".vbrief.json".length));
131
+ const storyId = String(plan.id ?? stripArtifactSuffix(file));
131
132
  const [refRepo, number] = extractIssueRef(data);
132
133
  const effectiveRepo = refRepo ?? options.repo ?? null;
133
134
  if (number === null || effectiveRepo === null) {
@@ -6,7 +6,11 @@ const WIP_MARKERS = ["[wip]", "[in progress]", "[in-progress]", "[running]", "[a
6
6
  const BLOCKED_MARKERS = ["[blocked]"];
7
7
  const CANCELLED_MARKERS = ["[cancelled]", "[canceled]"];
8
8
  export const OVERRIDES_FILENAME = "migration-overrides.yaml";
9
- const GITHUB_ISSUE_REF_TYPES = new Set(["github-issue", "x-vbrief/github-issue"]);
9
+ const GITHUB_ISSUE_REF_TYPES = new Set([
10
+ "github-issue",
11
+ "x-vbrief/github-issue",
12
+ "x-xbrief/github-issue",
13
+ ]);
10
14
  const GITHUB_ISSUE_URI_RE = /https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/(\d+)/;
11
15
  const PHASE_TITLE_RE = /^(Phase\s+\d|IP[-\s]\d|Milestone\s+\d)/i;
12
16
  const STATUS_TO_FOLDER = {
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { join } from "node:path";
3
+ import { hasArtifactSuffix, resolveLifecycleRoot, stripArtifactSuffix } from "../layout/resolve.js";
3
4
  import { pyRepr } from "./py-repr.js";
4
5
  export const LIFECYCLE_FOLDERS = [
5
6
  "proposed",
@@ -47,17 +48,17 @@ function storyId(path, plan) {
47
48
  if (typeof value === "string" && value.trim())
48
49
  return value.trim();
49
50
  const name = path.split(/[/\\]/).pop() ?? "";
50
- return name.endsWith(".vbrief.json") ? name.slice(0, -".vbrief.json".length) : name;
51
+ return hasArtifactSuffix(name) ? stripArtifactSuffix(name) : name;
51
52
  }
52
53
  export function allScopeIds(projectRoot) {
53
54
  const ids = {};
54
- const vbriefDir = join(resolve(projectRoot), "vbrief");
55
+ const vbriefDir = resolveLifecycleRoot(projectRoot);
55
56
  for (const folder of LIFECYCLE_FOLDERS) {
56
57
  const folderPath = join(vbriefDir, folder);
57
58
  if (!existsSync(folderPath))
58
59
  continue;
59
60
  const files = readdirSync(folderPath)
60
- .filter((f) => f.endsWith(".vbrief.json"))
61
+ .filter((f) => hasArtifactSuffix(f))
61
62
  .sort();
62
63
  for (const file of files) {
63
64
  const path = join(folderPath, file);
@@ -68,7 +69,7 @@ export function allScopeIds(projectRoot) {
68
69
  const scopeId = storyId(path, plan);
69
70
  const status = String(plan.status ?? "");
70
71
  ids[scopeId] = [path, status];
71
- const stem = file.endsWith(".vbrief.json") ? file.slice(0, -".vbrief.json".length) : file;
72
+ const stem = hasArtifactSuffix(file) ? stripArtifactSuffix(file) : file;
72
73
  if (!ids[stem])
73
74
  ids[stem] = [path, status];
74
75
  }
@@ -2,7 +2,7 @@ import type { Child, ReconcileUmbrellasOutcome, UmbrellaClient } from "./types.j
2
2
  export declare const OPEN_FOLDERS: readonly ["proposed", "pending", "active"];
3
3
  export declare const CLOSED_FOLDERS: readonly ["completed", "cancelled"];
4
4
  export declare const LIFECYCLE_FOLDERS: readonly ["proposed", "pending", "active", "completed", "cancelled"];
5
- export declare const CHILD_REF_TYPE = "x-vbrief/plan";
5
+ export declare const CHILD_REF_TYPE = "x-xbrief/plan";
6
6
  export declare class UmbrellaScmError extends Error {
7
7
  name: string;
8
8
  }
@@ -1,11 +1,13 @@
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 { hasArtifactSuffix, resolveLifecycleRoot, stripArtifactSuffix } from "../layout/resolve.js";
3
5
  import { call } from "../scm/call.js";
4
6
  import { extractIssueRef } from "../triage/reconcile/parse-uri.js";
5
7
  export const OPEN_FOLDERS = ["proposed", "pending", "active"];
6
8
  export const CLOSED_FOLDERS = ["completed", "cancelled"];
7
9
  export const LIFECYCLE_FOLDERS = [...OPEN_FOLDERS, ...CLOSED_FOLDERS];
8
- export const CHILD_REF_TYPE = "x-vbrief/plan";
10
+ export const CHILD_REF_TYPE = "x-xbrief/plan";
9
11
  const SCM_SOURCE = "github-issue";
10
12
  const HEADER_RE = /^## Current shape \(as of pass-(\d+)\)/m;
11
13
  // ReDoS-hardened (#1782 s4 / CodeQL js/polynomial-redos): the original
@@ -63,14 +65,14 @@ export function buildChildIndex(vbriefDir) {
63
65
  if (!existsSync(folderPath))
64
66
  continue;
65
67
  const files = readdirSync(folderPath)
66
- .filter((f) => f.endsWith(".vbrief.json"))
68
+ .filter((f) => hasArtifactSuffix(f))
67
69
  .sort();
68
70
  for (const file of files) {
69
71
  const path = join(folderPath, file);
70
72
  const data = readJson(path);
71
73
  if (!data)
72
74
  continue;
73
- const fallbackId = file.slice(0, -".vbrief.json".length);
75
+ const fallbackId = stripArtifactSuffix(file);
74
76
  index[file] = childFromData(data, folder, fallbackId);
75
77
  }
76
78
  }
@@ -89,7 +91,7 @@ export function computeChildren(epicData, index) {
89
91
  if (typeof ref !== "object" || ref === null || Array.isArray(ref))
90
92
  continue;
91
93
  const rec = ref;
92
- if (rec.type !== CHILD_REF_TYPE)
94
+ if (!referenceTypeMatches(String(rec.type ?? ""), "plan"))
93
95
  continue;
94
96
  const name = basename(String(rec.uri ?? ""));
95
97
  const child = index[name];
@@ -331,7 +333,7 @@ export function nowIso(date = new Date()) {
331
333
  }
332
334
  export function reconcileUmbrellas(projectRoot, options = {}) {
333
335
  const root = resolve(projectRoot);
334
- const vbriefDir = join(root, "vbrief");
336
+ const vbriefDir = resolveLifecycleRoot(root);
335
337
  if (!existsSync(vbriefDir)) {
336
338
  return [
337
339
  2,
@@ -360,7 +362,7 @@ export function reconcileUmbrellas(projectRoot, options = {}) {
360
362
  if (!existsSync(folderPath))
361
363
  continue;
362
364
  const files = readdirSync(folderPath)
363
- .filter((f) => f.endsWith(".vbrief.json"))
365
+ .filter((f) => hasArtifactSuffix(f))
364
366
  .sort();
365
367
  for (const file of files) {
366
368
  const path = join(folderPath, file);
@@ -375,7 +377,7 @@ export function reconcileUmbrellas(projectRoot, options = {}) {
375
377
  : {};
376
378
  if (metadata.kind !== "epic")
377
379
  continue;
378
- const storyId = String(plan.id ?? file.slice(0, -".vbrief.json".length));
380
+ const storyId = String(plan.id ?? stripArtifactSuffix(file));
379
381
  const [refRepo, number] = extractIssueRef(data);
380
382
  const effectiveRepo = refRepo ?? options.repo ?? null;
381
383
  if (number === null || effectiveRepo === null) {
@@ -1,8 +1,7 @@
1
1
  export declare const DOC_CORE: Set<string>;
2
2
  export declare const PLAN_CORE: Set<string>;
3
3
  export declare const ITEM_CORE: Set<string>;
4
- export declare const EXTENSION_PREFIXES: readonly ["x-directive/", "x-vbrief/"];
5
- export declare const ALLOW_LIST: Set<string>;
4
+ export declare const EXTENSION_PREFIXES: readonly ["x-directive/", "x-vbrief/", "x-xbrief/"];
6
5
  export interface ConformanceFinding {
7
6
  readonly path: string;
8
7
  readonly level: string;