@deftai/directive-core 0.65.0 → 0.66.1

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 (152) 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.d.ts +24 -0
  23. package/dist/intake/issue-ingest.js +113 -16
  24. package/dist/intake/reconcile-issues.js +11 -4
  25. package/dist/layout/index.d.ts +2 -0
  26. package/dist/layout/index.js +2 -0
  27. package/dist/layout/resolve.d.ts +87 -0
  28. package/dist/layout/resolve.js +159 -0
  29. package/dist/lifecycle/lifecycle-hygiene.d.ts +1 -1
  30. package/dist/lifecycle/lifecycle-hygiene.js +8 -5
  31. package/dist/orchestration/judgment-policy.js +4 -6
  32. package/dist/orchestration/verify-judgment-gates.d.ts +1 -0
  33. package/dist/orchestration/verify-judgment-gates.js +4 -2
  34. package/dist/policy/autonomy.js +4 -6
  35. package/dist/policy/capacity.js +4 -6
  36. package/dist/policy/decisions.d.ts +1 -0
  37. package/dist/policy/decisions.js +4 -2
  38. package/dist/policy/index.d.ts +1 -0
  39. package/dist/policy/index.js +3 -1
  40. package/dist/policy/plan-extensions.d.ts +35 -0
  41. package/dist/policy/plan-extensions.js +57 -0
  42. package/dist/policy/resolve.d.ts +6 -2
  43. package/dist/policy/resolve.js +18 -8
  44. package/dist/policy/wip.js +6 -4
  45. package/dist/preflight-cache/evaluate.js +8 -5
  46. package/dist/release/native-steps.js +4 -3
  47. package/dist/render/framework-commands.js +2 -1
  48. package/dist/render/prd-render.js +2 -2
  49. package/dist/render/project-render.js +23 -5
  50. package/dist/render/roadmap-render.js +36 -8
  51. package/dist/render/scope-outlook.js +3 -2
  52. package/dist/scope/audit-log.js +3 -2
  53. package/dist/scope/capacity-stamp.js +2 -1
  54. package/dist/scope/decompose.js +6 -4
  55. package/dist/scope/decomposed-refs.js +2 -1
  56. package/dist/scope/demote.js +5 -4
  57. package/dist/scope/project-context.js +2 -1
  58. package/dist/scope/project-definition-sync.js +4 -3
  59. package/dist/scope/transition.js +6 -2
  60. package/dist/scope/undo.js +2 -1
  61. package/dist/scope/vbrief-ref.js +5 -3
  62. package/dist/session/resume-conditions.js +2 -1
  63. package/dist/session/ritual-sentinel.js +2 -1
  64. package/dist/session/staleness.js +2 -1
  65. package/dist/slice/record.js +4 -4
  66. package/dist/spec-authority/resolver.js +5 -5
  67. package/dist/swarm/complete-cohort.js +5 -4
  68. package/dist/swarm/launch.js +6 -7
  69. package/dist/swarm/readiness.js +22 -20
  70. package/dist/swarm/subagent-backend.js +2 -5
  71. package/dist/swarm/verify-review-clean.js +2 -1
  72. package/dist/triage/actions/candidates-log.d.ts +3 -0
  73. package/dist/triage/actions/candidates-log.js +8 -2
  74. package/dist/triage/bootstrap/gitignore.js +22 -13
  75. package/dist/triage/bootstrap/index.js +7 -4
  76. package/dist/triage/bulk/index.js +2 -1
  77. package/dist/triage/classify/index.js +13 -11
  78. package/dist/triage/queue/audit.js +3 -3
  79. package/dist/triage/queue/cache.js +3 -2
  80. package/dist/triage/queue/ranking-labels.js +2 -1
  81. package/dist/triage/queue/scope-walk.js +8 -6
  82. package/dist/triage/reconcile/audit.js +2 -1
  83. package/dist/triage/reconcile/parse-uri.js +2 -1
  84. package/dist/triage/reconcile/reconcile.js +8 -7
  85. package/dist/triage/refresh/extract.js +4 -2
  86. package/dist/triage/refresh/refresh.js +3 -2
  87. package/dist/triage/scope/mutations-core.js +13 -9
  88. package/dist/triage/scope/renderers.js +5 -3
  89. package/dist/triage/scope/resolve.js +6 -5
  90. package/dist/triage/scope/validate.js +3 -6
  91. package/dist/triage/scope-drift/add-ignore.js +4 -2
  92. package/dist/triage/scope-drift/scope-rules.js +3 -2
  93. package/dist/triage/subscribe/index.js +13 -7
  94. package/dist/triage/summary/index.js +7 -5
  95. package/dist/triage/summary/reconcilable.js +6 -4
  96. package/dist/triage/summary/scope-drift.js +2 -1
  97. package/dist/triage/welcome/prior-state.js +9 -9
  98. package/dist/triage/welcome/summary.js +9 -8
  99. package/dist/triage/welcome/writers.js +12 -10
  100. package/dist/validate-content/capacity-policy.js +5 -4
  101. package/dist/validate-content/capacity-show.js +5 -3
  102. package/dist/validate-content/filename.d.ts +1 -1
  103. package/dist/validate-content/filename.js +5 -4
  104. package/dist/validate-content/validate-strategy-output.js +14 -5
  105. package/dist/vbrief-build/build.js +1 -1
  106. package/dist/vbrief-build/constants.js +7 -0
  107. package/dist/vbrief-build/speckit.js +3 -2
  108. package/dist/vbrief-reconcile/graph.js +3 -2
  109. package/dist/vbrief-reconcile/labels.js +4 -3
  110. package/dist/vbrief-reconcile/reconciliation.js +5 -1
  111. package/dist/vbrief-reconcile/swarm-deps.js +6 -5
  112. package/dist/vbrief-reconcile/umbrellas.d.ts +1 -1
  113. package/dist/vbrief-reconcile/umbrellas.js +9 -7
  114. package/dist/vbrief-validate/conformance.d.ts +1 -2
  115. package/dist/vbrief-validate/conformance.js +29 -14
  116. package/dist/vbrief-validate/constants.d.ts +18 -1
  117. package/dist/vbrief-validate/constants.js +32 -3
  118. package/dist/vbrief-validate/epic-links.js +3 -2
  119. package/dist/vbrief-validate/filename.js +5 -3
  120. package/dist/vbrief-validate/index.d.ts +3 -1
  121. package/dist/vbrief-validate/index.js +2 -1
  122. package/dist/vbrief-validate/main.js +17 -6
  123. package/dist/vbrief-validate/origin.js +1 -1
  124. package/dist/vbrief-validate/paths.js +2 -1
  125. package/dist/vbrief-validate/plan-hooks.js +4 -3
  126. package/dist/vbrief-validate/registry-status.d.ts +1 -0
  127. package/dist/vbrief-validate/registry-status.js +3 -1
  128. package/dist/vbrief-validate/roundtrip.d.ts +36 -0
  129. package/dist/vbrief-validate/roundtrip.js +168 -0
  130. package/dist/vbrief-validate/schema.d.ts +1 -1
  131. package/dist/vbrief-validate/schema.js +62 -19
  132. package/dist/vbrief-validate/validate-all.js +12 -4
  133. package/dist/verify-source/code-structure-validate.js +8 -4
  134. package/dist/verify-source/contract-drift.d.ts +7 -3
  135. package/dist/verify-source/contract-drift.js +40 -22
  136. package/dist/xbrief-migrate/constants.d.ts +13 -0
  137. package/dist/xbrief-migrate/constants.js +14 -0
  138. package/dist/xbrief-migrate/detect.d.ts +11 -0
  139. package/dist/xbrief-migrate/detect.js +81 -0
  140. package/dist/xbrief-migrate/drift-gate.d.ts +56 -0
  141. package/dist/xbrief-migrate/drift-gate.js +205 -0
  142. package/dist/xbrief-migrate/index.d.ts +7 -0
  143. package/dist/xbrief-migrate/index.js +7 -0
  144. package/dist/xbrief-migrate/migrate-project.d.ts +33 -0
  145. package/dist/xbrief-migrate/migrate-project.js +189 -0
  146. package/dist/xbrief-migrate/signpost.d.ts +5 -0
  147. package/dist/xbrief-migrate/signpost.js +19 -0
  148. package/dist/xbrief-migrate/transforms.d.ts +46 -0
  149. package/dist/xbrief-migrate/transforms.js +182 -0
  150. package/dist/xbrief-migrate/version.d.ts +8 -0
  151. package/dist/xbrief-migrate/version.js +25 -0
  152. package/package.json +11 -3
@@ -1,3 +1,4 @@
1
+ import { readPlanPolicy } from "../../policy/plan-extensions.js";
1
2
  import { REFERENCED_BY_VBRIEF_SCOPES, SLICED_FROM_SCOPES, VALID_IGNORE_KEYS, VALID_IGNORE_RULES, VALID_RULE_TYPES, } from "./constants.js";
2
3
  import { parseDurationMs } from "./duration.js";
3
4
  import { validateMilestoneRule } from "./milestone.js";
@@ -238,9 +239,7 @@ function pointerForError(err, rawList) {
238
239
  }
239
240
  export function validateTriageScopeOnPlan(plan, filepath) {
240
241
  const out = [];
241
- const policy = typeof plan === "object" && plan !== null && !Array.isArray(plan)
242
- ? plan.policy
243
- : undefined;
242
+ const policy = readPlanPolicy(plan);
244
243
  const rawScope = typeof policy === "object" && policy !== null && !Array.isArray(policy)
245
244
  ? policy.triageScope
246
245
  : undefined;
@@ -254,9 +253,7 @@ export function validateTriageScopeOnPlan(plan, filepath) {
254
253
  }
255
254
  export function validateTriageScopeIgnoresOnPlan(plan, filepath) {
256
255
  const out = [];
257
- const policy = typeof plan === "object" && plan !== null && !Array.isArray(plan)
258
- ? plan.policy
259
- : undefined;
256
+ const policy = readPlanPolicy(plan);
260
257
  const raw = typeof policy === "object" && policy !== null && !Array.isArray(policy)
261
258
  ? policy.triageScopeIgnores
262
259
  : undefined;
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdtempSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { dirname, join, resolve } from "node:path";
4
+ import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
4
5
  const PROJECT_DEFINITION_REL_PATH = "vbrief/PROJECT-DEFINITION.vbrief.json";
5
6
  function projectDefinitionPath(projectRoot) {
6
7
  return join(resolve(projectRoot), PROJECT_DEFINITION_REL_PATH);
@@ -54,10 +55,11 @@ export function addIgnore(projectRoot, options) {
54
55
  throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
55
56
  }
56
57
  const planRec = plan;
57
- let policy = planRec.policy;
58
+ migrateLegacyPolicyKey(planRec);
59
+ let policy = planRec[PLAN_POLICY_KEY];
58
60
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
59
61
  policy = {};
60
- planRec.policy = policy;
62
+ planRec[PLAN_POLICY_KEY] = policy;
61
63
  }
62
64
  const policyRec = policy;
63
65
  const raw = Array.isArray(policyRec.triageScopeIgnores)
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
+ import { readPlanPolicy } from "../../policy/plan-extensions.js";
3
4
  const PROJECT_DEFINITION_REL_PATH = "vbrief/PROJECT-DEFINITION.vbrief.json";
4
5
  const DEFAULT_TRIAGE_SCOPE = [{ rule: "all-open" }];
5
6
  function loadProjectDefinition(projectRoot) {
@@ -25,7 +26,7 @@ export function resolveScopeRules(projectRoot, projectDefinition) {
25
26
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
26
27
  return DEFAULT_TRIAGE_SCOPE.map((r) => ({ ...r }));
27
28
  }
28
- const policy = plan.policy;
29
+ const policy = readPlanPolicy(plan);
29
30
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
30
31
  return DEFAULT_TRIAGE_SCOPE.map((r) => ({ ...r }));
31
32
  }
@@ -47,7 +48,7 @@ export function resolveScopeIgnores(projectRoot, projectDefinition) {
47
48
  const plan = data.plan;
48
49
  if (typeof plan !== "object" || plan === null || Array.isArray(plan))
49
50
  return out;
50
- const policy = plan.policy;
51
+ const policy = readPlanPolicy(plan);
51
52
  if (typeof policy !== "object" || policy === null || Array.isArray(policy))
52
53
  return out;
53
54
  const raw = policy.triageScopeIgnores;
@@ -1,6 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
3
- import { basename, join, resolve } from "node:path";
3
+ import { basename, join } from "node:path";
4
+ import { resolveEvalPath, resolveProjectDefinitionPath } from "../../layout/resolve.js";
5
+ import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
4
6
  export const SUBSCRIPTION_HISTORY_REL_PATH = "vbrief/.eval/subscription-history.jsonl";
5
7
  export const SUBSCRIPTION_HISTORY_SCHEMA = "deft.triage.subscription-change.v1";
6
8
  export const PROJECT_DEFINITION_REL_PATH = "vbrief/PROJECT-DEFINITION.vbrief.json";
@@ -15,7 +17,7 @@ export class ProjectDefinitionIOError extends Error {
15
17
  }
16
18
  }
17
19
  function projectDefinitionPath(projectRoot) {
18
- return join(resolve(projectRoot), PROJECT_DEFINITION_REL_PATH);
20
+ return resolveProjectDefinitionPath(projectRoot);
19
21
  }
20
22
  export function loadProjectDefinitionForMutation(projectRoot) {
21
23
  const path = projectDefinitionPath(projectRoot);
@@ -87,7 +89,7 @@ function resolveActor(actor) {
87
89
  }
88
90
  }
89
91
  export function recordSubscriptionChange(projectRoot, options) {
90
- const historyPath = join(resolve(projectRoot), SUBSCRIPTION_HISTORY_REL_PATH);
92
+ const historyPath = resolveEvalPath(projectRoot, "subscription-history.jsonl");
91
93
  const record = {
92
94
  schema: SUBSCRIPTION_HISTORY_SCHEMA,
93
95
  change_id: randomUUID(),
@@ -249,15 +251,19 @@ function mutate(projectRoot, options) {
249
251
  throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
250
252
  }
251
253
  const plan = data.plan;
252
- if (typeof plan.policy !== "object" || plan.policy === null || Array.isArray(plan.policy)) {
253
- if (plan.policy === undefined) {
254
- plan.policy = {};
254
+ migrateLegacyPolicyKey(plan);
255
+ const existingPolicy = plan[PLAN_POLICY_KEY];
256
+ if (typeof existingPolicy !== "object" ||
257
+ existingPolicy === null ||
258
+ Array.isArray(existingPolicy)) {
259
+ if (existingPolicy === undefined) {
260
+ plan[PLAN_POLICY_KEY] = {};
255
261
  }
256
262
  else {
257
263
  throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan.policy' key`);
258
264
  }
259
265
  }
260
- const policy = plan.policy;
266
+ const policy = plan[PLAN_POLICY_KEY];
261
267
  if (!Array.isArray(policy.triageScope)) {
262
268
  if (policy.triageScope === undefined) {
263
269
  policy.triageScope = [];
@@ -1,6 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
3
3
  import { join, resolve as pathResolve } from "node:path";
4
+ import { hasArtifactSuffix, resolveEvalPath, resolveLifecycleFolder, resolveProjectDefinitionPath, } from "../../layout/resolve.js";
5
+ import { readPlanPolicy } from "../../policy/plan-extensions.js";
4
6
  import { loadProjectDefinition, PROJECT_DEFINITION_REL_PATH } from "../../policy/resolve.js";
5
7
  import { countVbriefWip, DEFAULT_WIP_CAP, resolveWipCap } from "../../policy/wip.js";
6
8
  import { AUDIT_LOG_REL_PATH, latestDecisions, readAuditLog } from "../actions/candidates-log.js";
@@ -93,13 +95,13 @@ export function iterCachedIssues(cacheRoot) {
93
95
  // ---------------------------------------------------------------------------
94
96
  /** Count filesystem-truth in-flight vBRIEFs (#1270). */
95
97
  export function countFilesystemInFlight(projectRoot) {
96
- const folder = join(pathResolve(projectRoot), "vbrief", FILESYSTEM_IN_FLIGHT_FOLDER);
98
+ const folder = resolveLifecycleFolder(projectRoot, FILESYSTEM_IN_FLIGHT_FOLDER);
97
99
  if (!existsSync(folder)) {
98
100
  return 0;
99
101
  }
100
102
  let total = 0;
101
103
  for (const entry of readdirSync(folder, { withFileTypes: true })) {
102
- if (!entry.isFile() || !entry.name.endsWith(".vbrief.json")) {
104
+ if (!entry.isFile() || !hasArtifactSuffix(entry.name)) {
103
105
  continue;
104
106
  }
105
107
  try {
@@ -124,7 +126,7 @@ export function countFilesystemInFlight(projectRoot) {
124
126
  }
125
127
  /** True iff `plan.policy.triageScope` is a non-empty list of dict rules. */
126
128
  export function isTriageScopeExplicitlyConfigured(projectRoot) {
127
- const path = join(pathResolve(projectRoot), PROJECT_DEFINITION_REL_PATH);
129
+ const path = resolveProjectDefinitionPath(projectRoot);
128
130
  if (!existsSync(path)) {
129
131
  return false;
130
132
  }
@@ -137,7 +139,7 @@ export function isTriageScopeExplicitlyConfigured(projectRoot) {
137
139
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
138
140
  return false;
139
141
  }
140
- const policy = plan.policy;
142
+ const policy = readPlanPolicy(plan);
141
143
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
142
144
  return false;
143
145
  }
@@ -187,7 +189,7 @@ export function isCachedIssueOpen(cacheRoot, repo, issueNumber) {
187
189
  export function computeSummary(projectRoot, options = {}) {
188
190
  const root = pathResolve(projectRoot);
189
191
  const resolvedCacheRoot = options.cacheRoot ?? join(root, CACHE_DIR_NAME);
190
- const resolvedLogPath = options.auditLogPath ?? join(root, CANDIDATES_LOG_REL_PATH);
192
+ const resolvedLogPath = options.auditLogPath ?? resolveEvalPath(root, "candidates.jsonl");
191
193
  const cached = iterCachedIssues(resolvedCacheRoot);
192
194
  const repos = [...new Set(cached.map(([repo]) => repo))].sort();
193
195
  const wipCap = resolveWipCapInt(root);
@@ -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