@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,8 @@
1
1
  import { readdirSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
+ import { referenceTypeMatches } from "@deftai/directive-types";
4
+ import { hasArtifactSuffix, resolveLifecycleRoot, resolveProjectDefinitionPath, } from "../../layout/resolve.js";
5
+ import { readPlanPolicy } from "../../policy/plan-extensions.js";
3
6
  import { pyRepr } from "../../scm/py-format.js";
4
7
  function pythonTypeName(value) {
5
8
  if (value === null) {
@@ -252,8 +255,7 @@ export function validateHoldMarkers(markers) {
252
255
  return { errors, warnings };
253
256
  }
254
257
  export function projectDefinitionPath(projectRoot) {
255
- const root = projectRoot ?? process.cwd();
256
- return join(root, PROJECT_DEFINITION_REL_PATH);
258
+ return resolveProjectDefinitionPath(projectRoot ?? process.cwd());
257
259
  }
258
260
  function loadProjectDefinition(projectRoot) {
259
261
  const path = projectDefinitionPath(projectRoot);
@@ -276,7 +278,7 @@ function consumerRulesFromProject(data) {
276
278
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
277
279
  return [];
278
280
  }
279
- const policy = plan.policy;
281
+ const policy = readPlanPolicy(plan);
280
282
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
281
283
  return [];
282
284
  }
@@ -294,7 +296,7 @@ function holdMarkersFromProject(data) {
294
296
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
295
297
  return null;
296
298
  }
297
- const policy = plan.policy;
299
+ const policy = readPlanPolicy(plan);
298
300
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
299
301
  return null;
300
302
  }
@@ -518,13 +520,13 @@ export function classifyIssue(issue, options = {}) {
518
520
  }
519
521
  /** Return issue numbers referenced by pending/ or active/ scope vBRIEFs. */
520
522
  export function extractReferencedIssues(projectRoot, lifecycleFolders = ["pending", "active"]) {
521
- const root = join(projectRoot ?? process.cwd(), "vbrief");
523
+ const root = resolveLifecycleRoot(projectRoot ?? process.cwd());
522
524
  const referenced = new Set();
523
525
  for (const folder of lifecycleFolders) {
524
526
  const folderPath = join(root, folder);
525
527
  let entries;
526
528
  try {
527
- entries = readdirSync(folderPath).filter((f) => f.endsWith(".vbrief.json"));
529
+ entries = readdirSync(folderPath).filter((f) => hasArtifactSuffix(f));
528
530
  }
529
531
  catch {
530
532
  continue;
@@ -549,7 +551,7 @@ export function extractReferencedIssues(projectRoot, lifecycleFolders = ["pendin
549
551
  continue;
550
552
  }
551
553
  const r = ref;
552
- if (r.type !== "x-vbrief/github-issue") {
554
+ if (!referenceTypeMatches(String(r.type ?? ""), "github-issue")) {
553
555
  continue;
554
556
  }
555
557
  const uri = r.uri;
@@ -652,7 +654,7 @@ export function validateTriageAutoClassifyOnPlan(plan, filepath) {
652
654
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
653
655
  return out;
654
656
  }
655
- const policy = plan.policy;
657
+ const policy = readPlanPolicy(plan);
656
658
  const raw = typeof policy === "object" && policy !== null && !Array.isArray(policy)
657
659
  ? policy.triageAutoClassify
658
660
  : undefined;
@@ -671,7 +673,7 @@ export function validateTriageHoldMarkersOnPlan(plan, filepath) {
671
673
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
672
674
  return out;
673
675
  }
674
- const policy = plan.policy;
676
+ const policy = readPlanPolicy(plan);
675
677
  const raw = typeof policy === "object" && policy !== null && !Array.isArray(policy)
676
678
  ? policy.triageHoldMarkers
677
679
  : undefined;
@@ -696,7 +698,7 @@ export function validateProject(projectRoot) {
696
698
  return {
697
699
  code: 0,
698
700
  stdout: "OK: no PROJECT-DEFINITION at " +
699
- `${join(root, "vbrief", "PROJECT-DEFINITION.vbrief.json")} -- ` +
701
+ `${resolveProjectDefinitionPath(root)} -- ` +
700
702
  "framework defaults apply with no consumer overrides.\n",
701
703
  stderr: "",
702
704
  };
@@ -709,7 +711,7 @@ export function validateProject(projectRoot) {
709
711
  stderr: "FAIL: PROJECT-DEFINITION.plan is not an object\n",
710
712
  };
711
713
  }
712
- const rel = join(root, "vbrief", "PROJECT-DEFINITION.vbrief.json");
714
+ const rel = resolveProjectDefinitionPath(root);
713
715
  const classifyErrs = validateTriageAutoClassifyOnPlan(plan, rel);
714
716
  const holderErrs = validateTriageHoldMarkersOnPlan(plan, rel);
715
717
  const errors = [...classifyErrs, ...holderErrs];
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
3
- import { DEFAULT_AUDIT_LOG_REL_PATH } from "./constants.js";
2
+ import { resolve } from "node:path";
3
+ import { resolveEvalPath } from "../../layout/resolve.js";
4
4
  /** Resolve audit log path; mirrors candidates_log._resolve_path (framework root). */
5
5
  export function resolveAuditLogPath(options = {}) {
6
6
  if (options.auditLogPath !== null && options.auditLogPath !== undefined) {
@@ -12,7 +12,7 @@ export function resolveAuditLogPath(options = {}) {
12
12
  : envRoot.length > 0
13
13
  ? resolve(envRoot)
14
14
  : process.cwd();
15
- return join(root, DEFAULT_AUDIT_LOG_REL_PATH);
15
+ return resolveEvalPath(root, "candidates.jsonl");
16
16
  }
17
17
  /** Read audit log entries, optionally filtered by repo. */
18
18
  export function readAuditEntries(repo, options = {}) {
@@ -1,7 +1,8 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
+ import { resolveEvalPath } from "../../layout/resolve.js";
3
4
  import { extractAuthor, extractMilestone } from "../scope-drift/cache-walker.js";
4
- import { CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE, DEFAULT_SLICES_LOG_REL_PATH, } from "./constants.js";
5
+ import { CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE } from "./constants.js";
5
6
  import { hasActiveScopeIgnores, isRawIssueScopeIgnored, resolveScopeIgnores, } from "./scope-ignores-filter.js";
6
7
  import { blockedByIssueNumber, rankByIssueNumber } from "./scope-walk.js";
7
8
  function cachedState(issue) {
@@ -22,7 +23,7 @@ export function resolveSlicesLogPath(options = {}) {
22
23
  : envRoot.length > 0
23
24
  ? resolve(envRoot)
24
25
  : process.cwd();
25
- return join(root, DEFAULT_SLICES_LOG_REL_PATH);
26
+ return resolveEvalPath(root, "slices.jsonl");
26
27
  }
27
28
  /** Read slices.jsonl records. */
28
29
  export function loadSliceRecords(options = {}) {
@@ -1,3 +1,4 @@
1
+ import { readPlanPolicy } from "../../policy/plan-extensions.js";
1
2
  import { DEFAULT_TRIAGE_RANKING_LABELS, PROJECT_DEFINITION_REL_PATH } from "./constants.js";
2
3
  import { loadProjectDefinition } from "./project.js";
3
4
  /** Resolve effective plan.policy.triageRankingLabels list. */
@@ -10,7 +11,7 @@ export function resolveRankingLabels(projectRoot) {
10
11
  if (typeof plan !== "object" || plan === null) {
11
12
  return [...DEFAULT_TRIAGE_RANKING_LABELS];
12
13
  }
13
- const policy = plan.policy;
14
+ const policy = readPlanPolicy(plan);
14
15
  if (typeof policy !== "object" || policy === null) {
15
16
  return [...DEFAULT_TRIAGE_RANKING_LABELS];
16
17
  }
@@ -1,5 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { join } from "node:path";
3
+ import { referenceTypeMatches } from "@deftai/directive-types";
4
+ import { hasArtifactSuffix, resolveLifecycleFolder, resolveLifecycleRoot, } from "../../layout/resolve.js";
3
5
  function loadPlan(path) {
4
6
  try {
5
7
  const data = JSON.parse(readFileSync(path, { encoding: "utf8" }));
@@ -25,7 +27,7 @@ export function issueNumbersFromPlan(plan) {
25
27
  continue;
26
28
  }
27
29
  const typed = ref;
28
- if (typed.type !== "x-vbrief/github-issue") {
30
+ if (!referenceTypeMatches(String(typed.type ?? ""), "github-issue")) {
29
31
  continue;
30
32
  }
31
33
  const uri = typed.uri;
@@ -82,11 +84,11 @@ function dependsOnIds(plan) {
82
84
  }
83
85
  function completedPlanIds(projectRoot) {
84
86
  const out = new Set();
85
- const base = join(resolve(projectRoot), "vbrief", "completed");
87
+ const base = resolveLifecycleFolder(projectRoot, "completed");
86
88
  if (!existsSync(base)) {
87
89
  return out;
88
90
  }
89
- for (const name of readdirSync(base).filter((entry) => entry.endsWith(".vbrief.json"))) {
91
+ for (const name of readdirSync(base).filter((entry) => hasArtifactSuffix(entry))) {
90
92
  const plan = loadPlan(join(base, name));
91
93
  if (plan === null) {
92
94
  continue;
@@ -111,14 +113,14 @@ export function scopeIsBlocked(plan, completedIds) {
111
113
  return deps.length > 0 && deps.some((dep) => !completedIds.has(dep));
112
114
  }
113
115
  function walkScopeFolders(projectRoot, folders, visitor) {
114
- const base = join(resolve(projectRoot), "vbrief");
116
+ const base = resolveLifecycleRoot(projectRoot);
115
117
  for (const folder of folders) {
116
118
  const folderDir = join(base, folder);
117
119
  if (!existsSync(folderDir)) {
118
120
  continue;
119
121
  }
120
122
  for (const name of readdirSync(folderDir)
121
- .filter((entry) => entry.endsWith(".vbrief.json"))
123
+ .filter((entry) => hasArtifactSuffix(entry))
122
124
  .sort()) {
123
125
  const plan = loadPlan(join(folderDir, name));
124
126
  if (plan !== null) {
@@ -1,12 +1,13 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { hasArtifactSuffix } from "../../layout/resolve.js";
3
4
  import { extractIssueRef } from "./parse-uri.js";
4
5
  export function scanLifecycleRefs(folder) {
5
6
  const results = [];
6
7
  if (!existsSync(folder))
7
8
  return results;
8
9
  const files = readdirSync(folder)
9
- .filter((name) => name.endsWith(".vbrief.json"))
10
+ .filter((name) => hasArtifactSuffix(name))
10
11
  .sort();
11
12
  for (const name of files) {
12
13
  const path = join(folder, name);
@@ -1,3 +1,4 @@
1
+ import { referenceTypeMatches } from "@deftai/directive-types";
1
2
  /** Parse (repo, issue_number) from a github-issue reference URI. */
2
3
  export function parseGithubIssueUri(uri) {
3
4
  if (typeof uri !== "string")
@@ -32,7 +33,7 @@ export function extractIssueRef(data) {
32
33
  if (typeof ref !== "object" || ref === null || Array.isArray(ref))
33
34
  continue;
34
35
  const rec = ref;
35
- if (rec.type !== "x-vbrief/github-issue")
36
+ if (!referenceTypeMatches(String(rec.type ?? ""), "github-issue"))
36
37
  continue;
37
38
  const [repo, number] = parseGithubIssueUri(rec.uri);
38
39
  if (number !== null)
@@ -1,9 +1,10 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { appendFileSync, mkdirSync } from "node:fs";
4
- import { dirname, join, resolve } from "node:path";
4
+ import { dirname, resolve } from "node:path";
5
+ import { resolveEvalPath, resolveLifecycleFolder } from "../../layout/resolve.js";
5
6
  import { auditKey, existingAuditRefs, scanLifecycleRefs } from "./audit.js";
6
- import { AUDIT_LOG_RELPATH, BACKFILL_FOLDERS, RECONCILE_ACTOR, } from "./types.js";
7
+ import { BACKFILL_FOLDERS, RECONCILE_ACTOR, } from "./types.js";
7
8
  const GIT_ORIGIN_RE = /^(?:https?:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:|ssh:\/\/git@github\.com[:/])([A-Za-z0-9][A-Za-z0-9._-]*)\/([A-Za-z0-9][A-Za-z0-9._-]*?)(?:\.git)?\/?\s*$/;
8
9
  export function inferRepoFromGit(cwd) {
9
10
  try {
@@ -32,13 +33,13 @@ function newDecisionId() {
32
33
  }
33
34
  export function findReconcilable(projectRoot, options = {}) {
34
35
  const root = resolve(projectRoot);
35
- const auditPath = options.auditLogPath ?? join(root, AUDIT_LOG_RELPATH);
36
+ const auditPath = options.auditLogPath ?? resolveEvalPath(root, "candidates.jsonl");
36
37
  const existing = existingAuditRefs(auditPath);
37
38
  const defaultRepo = options.defaultRepo ?? null;
38
39
  const items = [];
39
40
  const seen = new Set();
40
41
  for (const folderName of BACKFILL_FOLDERS) {
41
- const folderPath = join(root, "vbrief", folderName);
42
+ const folderPath = resolveLifecycleFolder(root, folderName);
42
43
  for (const [refRepo, number, path] of scanLifecycleRefs(folderPath)) {
43
44
  const effectiveRepo = refRepo ?? defaultRepo;
44
45
  if (effectiveRepo === null)
@@ -63,7 +64,7 @@ function countSkippedExisting(projectRoot, defaultRepo, auditPath) {
63
64
  let count = 0;
64
65
  const root = resolve(projectRoot);
65
66
  for (const folderName of BACKFILL_FOLDERS) {
66
- for (const [refRepo, number] of scanLifecycleRefs(join(root, "vbrief", folderName))) {
67
+ for (const [refRepo, number] of scanLifecycleRefs(resolveLifecycleFolder(root, folderName))) {
67
68
  const effectiveRepo = refRepo ?? defaultRepo;
68
69
  if (effectiveRepo === null)
69
70
  continue;
@@ -86,7 +87,7 @@ function countNoRepo(projectRoot, defaultRepo, auditPath) {
86
87
  let count = 0;
87
88
  const root = resolve(projectRoot);
88
89
  for (const folderName of BACKFILL_FOLDERS) {
89
- for (const [refRepo, number] of scanLifecycleRefs(join(root, "vbrief", folderName))) {
90
+ for (const [refRepo, number] of scanLifecycleRefs(resolveLifecycleFolder(root, folderName))) {
90
91
  if ((refRepo ?? defaultRepo) === null && !existingNumbers.has(number))
91
92
  count += 1;
92
93
  }
@@ -115,7 +116,7 @@ export function reconcile(projectRoot, options = {}) {
115
116
  if (defaultRepo === null) {
116
117
  defaultRepo = inferRepoFromGit(root);
117
118
  }
118
- const auditPath = options.auditLogPath ?? join(root, AUDIT_LOG_RELPATH);
119
+ const auditPath = options.auditLogPath ?? resolveEvalPath(root, "candidates.jsonl");
119
120
  const dryRun = options.dryRun ?? false;
120
121
  const result = {
121
122
  projectRoot: root,
@@ -1,11 +1,13 @@
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 } from "../../layout/resolve.js";
3
5
  const ISSUE_URL_RE = /github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)/i;
4
6
  export function iterActiveVbriefs(activeDir) {
5
7
  if (!existsSync(activeDir))
6
8
  return [];
7
9
  return readdirSync(activeDir)
8
- .filter((name) => name.endsWith(".vbrief.json"))
10
+ .filter((name) => hasArtifactSuffix(name))
9
11
  .sort()
10
12
  .map((name) => join(activeDir, name));
11
13
  }
@@ -30,7 +32,7 @@ export function extractIssueRefs(vbriefPath) {
30
32
  if (typeof ref !== "object" || ref === null || Array.isArray(ref))
31
33
  continue;
32
34
  const rec = ref;
33
- if (rec.type !== "x-vbrief/github-issue")
35
+ if (!referenceTypeMatches(String(rec.type ?? ""), "github-issue"))
34
36
  continue;
35
37
  const uri = String(rec.uri ?? "");
36
38
  const match = ISSUE_URL_RE.exec(uri);
@@ -1,4 +1,5 @@
1
- import { join, resolve } from "node:path";
1
+ import { resolve } from "node:path";
2
+ import { resolveLifecycleFolder } from "../../layout/resolve.js";
2
3
  import { detectDrift } from "./drift.js";
3
4
  import { iterActiveVbriefs } from "./extract.js";
4
5
  import { PROMPT_OPTIONS } from "./types.js";
@@ -20,7 +21,7 @@ function defaultAuditWriter(repo, issueNumber, annotation, log) {
20
21
  }
21
22
  export function refreshActive(projectRoot, options = {}) {
22
23
  const root = resolve(projectRoot);
23
- const activeDir = options.activeDir ?? join(root, "vbrief", "active");
24
+ const activeDir = options.activeDir ?? resolveLifecycleFolder(root, "active");
24
25
  const log = options.log ?? ((line) => process.stdout.write(`${line}\n`));
25
26
  const inputFn = options.inputFn ?? (() => "");
26
27
  const refreshLocal = options.refreshLocal ?? (() => { });
@@ -2,14 +2,16 @@ import { randomUUID } from "node:crypto";
2
2
  import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
- import { PROJECT_DEFINITION_REL_PATH, SUBSCRIPTION_HISTORY_REL_PATH, SUBSCRIPTION_HISTORY_SCHEMA, } from "./constants.js";
5
+ import { resolveEvalPath, resolveProjectDefinitionPath } from "../../layout/resolve.js";
6
+ import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
7
+ import { SUBSCRIPTION_HISTORY_SCHEMA } from "./constants.js";
6
8
  import { pyStrRepr } from "./python-repr.js";
7
9
  import { utcIso } from "./time.js";
8
10
  export class ProjectDefinitionIOError extends Error {
9
11
  name = "ProjectDefinitionIOError";
10
12
  }
11
13
  export function projectDefinitionPath(projectRoot) {
12
- return join(projectRoot, PROJECT_DEFINITION_REL_PATH);
14
+ return resolveProjectDefinitionPath(projectRoot);
13
15
  }
14
16
  export function loadProjectDefinitionForMutation(projectRoot) {
15
17
  const path = projectDefinitionPath(projectRoot);
@@ -58,7 +60,7 @@ function resolveActor(actor) {
58
60
  }
59
61
  }
60
62
  export function recordSubscriptionChange(projectRoot, options) {
61
- const historyPath = join(projectRoot, SUBSCRIPTION_HISTORY_REL_PATH);
63
+ const historyPath = resolveEvalPath(projectRoot, "subscription-history.jsonl");
62
64
  const record = {
63
65
  schema: SUBSCRIPTION_HISTORY_SCHEMA,
64
66
  change_id: randomUUID(),
@@ -132,9 +134,10 @@ export function subscribe(projectRoot, options) {
132
134
  throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
133
135
  }
134
136
  const planRec = plan;
135
- if (planRec.policy === undefined)
136
- planRec.policy = {};
137
- const policy = planRec.policy;
137
+ migrateLegacyPolicyKey(planRec);
138
+ if (planRec[PLAN_POLICY_KEY] === undefined)
139
+ planRec[PLAN_POLICY_KEY] = {};
140
+ const policy = planRec[PLAN_POLICY_KEY];
138
141
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
139
142
  throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan.policy' key`);
140
143
  }
@@ -179,9 +182,10 @@ export function addIgnore(projectRoot, label) {
179
182
  throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan' key`);
180
183
  }
181
184
  const planRec = plan;
182
- if (planRec.policy === undefined)
183
- planRec.policy = {};
184
- const policy = planRec.policy;
185
+ migrateLegacyPolicyKey(planRec);
186
+ if (planRec[PLAN_POLICY_KEY] === undefined)
187
+ planRec[PLAN_POLICY_KEY] = {};
188
+ const policy = planRec[PLAN_POLICY_KEY];
185
189
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
186
190
  throw new Error(`PROJECT-DEFINITION at ${path} has a non-object 'plan.policy' key`);
187
191
  }
@@ -1,10 +1,12 @@
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, resolveLifecycleRoot } from "../../layout/resolve.js";
3
5
  import { subscriptionHash } from "./normalize.js";
4
6
  import { pyListRepr } from "./python-repr.js";
5
7
  const LIFECYCLE_FOLDERS = ["proposed", "pending", "active", "completed", "cancelled"];
6
8
  export function extractReferencedIssues(projectRoot, lifecycleFolders = LIFECYCLE_FOLDERS) {
7
- const root = join(projectRoot, "vbrief");
9
+ const root = resolveLifecycleRoot(projectRoot);
8
10
  const anySet = new Set();
9
11
  const activeSet = new Set();
10
12
  if (!existsSync(root))
@@ -14,7 +16,7 @@ export function extractReferencedIssues(projectRoot, lifecycleFolders = LIFECYCL
14
16
  if (!existsSync(folderPath))
15
17
  continue;
16
18
  for (const name of readdirSync(folderPath)) {
17
- if (!name.endsWith(".vbrief.json"))
19
+ if (!hasArtifactSuffix(name))
18
20
  continue;
19
21
  const vbriefPath = join(folderPath, name);
20
22
  let data;
@@ -36,7 +38,7 @@ export function extractReferencedIssues(projectRoot, lifecycleFolders = LIFECYCL
36
38
  if (typeof ref !== "object" || ref === null || Array.isArray(ref))
37
39
  continue;
38
40
  const rec = ref;
39
- if (rec.type !== "x-vbrief/github-issue")
41
+ if (!referenceTypeMatches(String(rec.type ?? ""), "github-issue"))
40
42
  continue;
41
43
  const uri = rec.uri;
42
44
  if (typeof uri !== "string")
@@ -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
  import { DEFAULT_TRIAGE_SCOPE, PROJECT_DEFINITION_REL_PATH } from "./constants.js";
4
5
  export function projectDefinitionPath(projectRoot) {
5
6
  return join(resolve(projectRoot), PROJECT_DEFINITION_REL_PATH);
@@ -24,7 +25,7 @@ export function isDefaultApplied(data) {
24
25
  const plan = data.plan;
25
26
  if (typeof plan !== "object" || plan === null || Array.isArray(plan))
26
27
  return true;
27
- const policy = plan.policy;
28
+ const policy = readPlanPolicy(plan);
28
29
  if (typeof policy !== "object" || policy === null || Array.isArray(policy))
29
30
  return true;
30
31
  const scope = policy.triageScope;
@@ -36,7 +37,7 @@ export function getRawScope(data) {
36
37
  const plan = data.plan;
37
38
  if (typeof plan !== "object" || plan === null || Array.isArray(plan))
38
39
  return undefined;
39
- const policy = plan.policy;
40
+ const policy = readPlanPolicy(plan);
40
41
  if (typeof policy !== "object" || policy === null || Array.isArray(policy))
41
42
  return undefined;
42
43
  return policy.triageScope;
@@ -47,7 +48,7 @@ export function getRawIgnores(data) {
47
48
  const plan = data.plan;
48
49
  if (typeof plan !== "object" || plan === null || Array.isArray(plan))
49
50
  return [];
50
- const policy = plan.policy;
51
+ const policy = readPlanPolicy(plan);
51
52
  if (typeof policy !== "object" || policy === null || Array.isArray(policy))
52
53
  return [];
53
54
  const raw = policy.triageScopeIgnores;
@@ -65,7 +66,7 @@ export function resolveScopeRules(projectRoot, projectDefinition) {
65
66
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
66
67
  return DEFAULT_TRIAGE_SCOPE.map((r) => ({ ...r }));
67
68
  }
68
- const policy = plan.policy;
69
+ const policy = readPlanPolicy(plan);
69
70
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
70
71
  return DEFAULT_TRIAGE_SCOPE.map((r) => ({ ...r }));
71
72
  }
@@ -91,7 +92,7 @@ export function resolveScopeIgnores(projectRoot, projectDefinition) {
91
92
  const plan = data.plan;
92
93
  if (typeof plan !== "object" || plan === null || Array.isArray(plan))
93
94
  return out;
94
- const policy = plan.policy;
95
+ const policy = readPlanPolicy(plan);
95
96
  if (typeof policy !== "object" || policy === null || Array.isArray(policy))
96
97
  return out;
97
98
  const raw = policy.triageScopeIgnores;
@@ -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);