@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,6 +1,7 @@
1
1
  /**
2
2
  * Judgment-gate policy resolution (#1419). Port of scripts/policy.py judgment-gate section.
3
3
  */
4
+ import { readPlanPolicy } from "../policy/plan-extensions.js";
4
5
  import { loadProjectDefinition } from "../policy/resolve.js";
5
6
  export const GATE_CLASSES = new Set(["mechanical", "declared"]);
6
7
  export const GATE_TIERS = new Set(["auto", "review", "block"]);
@@ -175,12 +176,9 @@ function defaultPolicy(source, error = null) {
175
176
  return { gates: [], disabled: [], source, error };
176
177
  }
177
178
  function getPolicyBlock(data) {
178
- const plan = data.plan;
179
- if (typeof plan === "object" && plan !== null && !Array.isArray(plan)) {
180
- const policy = plan.policy;
181
- if (typeof policy === "object" && policy !== null && !Array.isArray(policy)) {
182
- return policy;
183
- }
179
+ const policy = readPlanPolicy(data.plan);
180
+ if (typeof policy === "object" && policy !== null && !Array.isArray(policy)) {
181
+ return policy;
184
182
  }
185
183
  return {};
186
184
  }
@@ -2,6 +2,7 @@
2
2
  * Risk-tiered judgment-gate engine (#1419 Slice 3). Port of scripts/verify_judgment_gates.py.
3
3
  */
4
4
  import { type JudgmentGatesPolicy } from "./judgment-policy.js";
5
+ /** Display/back-compat constant; resolution flows through resolveAuditDir (#2109). */
5
6
  export declare const AUDIT_DIR_REL = "vbrief/.audit";
6
7
  export declare const CLEARANCE_LOG_NAME = "judgment-gate-clearances.jsonl";
7
8
  export declare const UNIVERSAL_SOURCE = "universal";
@@ -5,8 +5,10 @@ import { execFileSync } from "node:child_process";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
6
  import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
7
7
  import { join, resolve } from "node:path";
8
+ import { resolveAuditDir } from "../layout/resolve.js";
8
9
  import { resolveJudgmentGates } from "./judgment-policy.js";
9
10
  import { matchAny } from "./pathspec.js";
11
+ /** Display/back-compat constant; resolution flows through resolveAuditDir (#2109). */
10
12
  export const AUDIT_DIR_REL = "vbrief/.audit";
11
13
  export const CLEARANCE_LOG_NAME = "judgment-gate-clearances.jsonl";
12
14
  export const UNIVERSAL_SOURCE = "universal";
@@ -131,7 +133,7 @@ export function reportBlocking(report) {
131
133
  return report.outcomes.filter(outcomeBlocking);
132
134
  }
133
135
  export function clearanceLogPath(projectRoot) {
134
- return join(projectRoot, AUDIT_DIR_REL, CLEARANCE_LOG_NAME);
136
+ return join(resolveAuditDir(projectRoot), CLEARANCE_LOG_NAME);
135
137
  }
136
138
  function utcNowIso(now) {
137
139
  return (now ?? new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
@@ -161,7 +163,7 @@ export function readClearances(projectRoot, logPath) {
161
163
  }
162
164
  export function recordClearance(projectRoot, options) {
163
165
  const path = options.log_path ?? clearanceLogPath(projectRoot);
164
- mkdirSync(join(projectRoot, AUDIT_DIR_REL), { recursive: true });
166
+ mkdirSync(resolveAuditDir(projectRoot), { recursive: true });
165
167
  const entry = {
166
168
  clearance_id: randomUUID(),
167
169
  timestamp: utcNowIso(options.now),
@@ -1,3 +1,4 @@
1
+ import { readPlanPolicy } from "./plan-extensions.js";
1
2
  import { loadProjectDefinition } from "./resolve.js";
2
3
  export const AUTONOMY_LEVELS = ["observe", "escalate", "execute"];
3
4
  export const DEFAULT_AUTONOMY_LEVEL = "escalate";
@@ -81,12 +82,9 @@ export function validateAutonomy(value) {
81
82
  return errors;
82
83
  }
83
84
  function getPolicyBlock(data) {
84
- const plan = data.plan;
85
- if (typeof plan === "object" && plan !== null && !Array.isArray(plan)) {
86
- const policy = plan.policy;
87
- if (typeof policy === "object" && policy !== null && !Array.isArray(policy)) {
88
- return policy;
89
- }
85
+ const policy = readPlanPolicy(data.plan);
86
+ if (typeof policy === "object" && policy !== null && !Array.isArray(policy)) {
87
+ return policy;
90
88
  }
91
89
  return {};
92
90
  }
@@ -1,3 +1,4 @@
1
+ import { readPlanPolicy } from "../policy/plan-extensions.js";
1
2
  import { loadProjectDefinition } from "../policy/resolve.js";
2
3
  export const DEFAULT_CAPACITY_UNIT = "vbrief-count";
3
4
  export const CAPACITY_UNIT_COST = "cost";
@@ -124,12 +125,9 @@ export function validateCapacityAllocation(value) {
124
125
  return errors;
125
126
  }
126
127
  function getPolicyBlock(data) {
127
- const plan = data.plan;
128
- if (typeof plan === "object" && plan !== null && !Array.isArray(plan)) {
129
- const policy = plan.policy;
130
- if (typeof policy === "object" && policy !== null && !Array.isArray(policy)) {
131
- return policy;
132
- }
128
+ const policy = readPlanPolicy(data.plan);
129
+ if (typeof policy === "object" && policy !== null && !Array.isArray(policy)) {
130
+ return policy;
133
131
  }
134
132
  return {};
135
133
  }
@@ -1,3 +1,4 @@
1
+ /** Display/back-compat constant; resolution flows through resolveAuditDir (#2109). */
1
2
  export declare const PENDING_DECISIONS_AUDIT_DIR_REL = "vbrief/.audit";
2
3
  export declare const PENDING_DECISIONS_LOG_NAME = "pending-human-decisions.jsonl";
3
4
  export declare const DECISION_STATUS_PENDING = "pending";
@@ -1,12 +1,14 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { join } from "node:path";
3
+ import { resolveAuditDir } from "../layout/resolve.js";
4
+ /** Display/back-compat constant; resolution flows through resolveAuditDir (#2109). */
3
5
  export const PENDING_DECISIONS_AUDIT_DIR_REL = "vbrief/.audit";
4
6
  export const PENDING_DECISIONS_LOG_NAME = "pending-human-decisions.jsonl";
5
7
  export const DECISION_STATUS_PENDING = "pending";
6
8
  export const DECISION_STATUS_RESOLVED = "resolved";
7
9
  export const DEFAULT_PENDING_DECISIONS_THRESHOLD = 5;
8
10
  export function pendingDecisionsLogPath(projectRoot) {
9
- return join(resolve(projectRoot), PENDING_DECISIONS_AUDIT_DIR_REL, PENDING_DECISIONS_LOG_NAME);
11
+ return join(resolveAuditDir(projectRoot), PENDING_DECISIONS_LOG_NAME);
10
12
  }
11
13
  function parseIsoTs(value) {
12
14
  if (typeof value !== "string" || value.length === 0) {
@@ -2,6 +2,7 @@ export * from "./autonomy.js";
2
2
  export * from "./capacity.js";
3
3
  export * from "./decisions.js";
4
4
  export * from "./disclosure.js";
5
+ export * from "./plan-extensions.js";
5
6
  export * from "./resolve.js";
6
7
  export * from "./wip.js";
7
8
  export declare const FIELD_ALLOW_DIRECT_COMMITS = "plan.policy.allowDirectCommitsToMaster";
@@ -1,9 +1,11 @@
1
+ import { readPlanPolicy } from "./plan-extensions.js";
1
2
  import { coerceLegacyNarrative, LEGACY_NARRATIVE_KEY, loadProjectDefinition } from "./resolve.js";
2
3
  import { DEFAULT_WIP_CAP } from "./wip.js";
3
4
  export * from "./autonomy.js";
4
5
  export * from "./capacity.js";
5
6
  export * from "./decisions.js";
6
7
  export * from "./disclosure.js";
8
+ export * from "./plan-extensions.js";
7
9
  export * from "./resolve.js";
8
10
  export * from "./wip.js";
9
11
  export const FIELD_ALLOW_DIRECT_COMMITS = "plan.policy.allowDirectCommitsToMaster";
@@ -39,7 +41,7 @@ function getPlan(data) {
39
41
  return {};
40
42
  }
41
43
  function getPolicyBlock(data) {
42
- const policy = getPlan(data).policy;
44
+ const policy = readPlanPolicy(getPlan(data));
43
45
  if (typeof policy === "object" && policy !== null && !Array.isArray(policy)) {
44
46
  return policy;
45
47
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Namespaced directive-config keys on a vBRIEF `plan` object (#1650 / #2034).
3
+ *
4
+ * "Category B" keys (`policy`, `completedNote`) are directive's OWN engine
5
+ * config -- not generic xbrief/consumer-extension data -- so they live under
6
+ * the `x-directive/` extension prefix that the conformance gate already
7
+ * accepts (#1620 EXTENSION_PREFIXES). Once the corpus is namespaced the
8
+ * conformance allow-list is removed, so any future bare `plan.policy` /
9
+ * `plan.completedNote` fails the gate.
10
+ *
11
+ * Read accessors accept the legacy bare key as a back-compat fallback so an
12
+ * un-migrated consumer PROJECT-DEFINITION keeps resolving while the operator
13
+ * migrates; writers always emit (and fold a legacy block into) the namespaced
14
+ * key.
15
+ */
16
+ /** Namespaced directive policy block key. */
17
+ export declare const PLAN_POLICY_KEY = "x-directive/policy";
18
+ /** Namespaced directive scope-completion note key. */
19
+ export declare const PLAN_COMPLETED_NOTE_KEY = "x-directive/completedNote";
20
+ /** Legacy bare policy key (pre-#1650); read-side fallback / write-side migration source. */
21
+ export declare const LEGACY_PLAN_POLICY_KEY = "policy";
22
+ /** Legacy bare completedNote key (pre-#1650); read-side fallback / write-side migration source. */
23
+ export declare const LEGACY_PLAN_COMPLETED_NOTE_KEY = "completedNote";
24
+ /** Read the directive policy block from a plan (namespaced first, bare fallback). */
25
+ export declare function readPlanPolicy(plan: unknown): unknown;
26
+ /** Read the directive completedNote from a plan (namespaced first, bare fallback). */
27
+ export declare function readPlanCompletedNote(plan: unknown): unknown;
28
+ /**
29
+ * Rename a legacy bare `policy` key to the namespaced key in place when only
30
+ * the bare key is present. No-op when the namespaced key already exists or no
31
+ * policy key is present. Used by writers so a mutation never strands a legacy
32
+ * bare block alongside the namespaced one.
33
+ */
34
+ export declare function migrateLegacyPolicyKey(plan: Record<string, unknown>): void;
35
+ //# sourceMappingURL=plan-extensions.d.ts.map
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Namespaced directive-config keys on a vBRIEF `plan` object (#1650 / #2034).
3
+ *
4
+ * "Category B" keys (`policy`, `completedNote`) are directive's OWN engine
5
+ * config -- not generic xbrief/consumer-extension data -- so they live under
6
+ * the `x-directive/` extension prefix that the conformance gate already
7
+ * accepts (#1620 EXTENSION_PREFIXES). Once the corpus is namespaced the
8
+ * conformance allow-list is removed, so any future bare `plan.policy` /
9
+ * `plan.completedNote` fails the gate.
10
+ *
11
+ * Read accessors accept the legacy bare key as a back-compat fallback so an
12
+ * un-migrated consumer PROJECT-DEFINITION keeps resolving while the operator
13
+ * migrates; writers always emit (and fold a legacy block into) the namespaced
14
+ * key.
15
+ */
16
+ /** Namespaced directive policy block key. */
17
+ export const PLAN_POLICY_KEY = "x-directive/policy";
18
+ /** Namespaced directive scope-completion note key. */
19
+ export const PLAN_COMPLETED_NOTE_KEY = "x-directive/completedNote";
20
+ /** Legacy bare policy key (pre-#1650); read-side fallback / write-side migration source. */
21
+ export const LEGACY_PLAN_POLICY_KEY = "policy";
22
+ /** Legacy bare completedNote key (pre-#1650); read-side fallback / write-side migration source. */
23
+ export const LEGACY_PLAN_COMPLETED_NOTE_KEY = "completedNote";
24
+ function asPlanObject(plan) {
25
+ return typeof plan === "object" && plan !== null && !Array.isArray(plan)
26
+ ? plan
27
+ : null;
28
+ }
29
+ function readPlanExtension(plan, namespacedKey, legacyKey) {
30
+ const planObj = asPlanObject(plan);
31
+ if (planObj === null) {
32
+ return undefined;
33
+ }
34
+ const namespaced = planObj[namespacedKey];
35
+ return namespaced !== undefined ? namespaced : planObj[legacyKey];
36
+ }
37
+ /** Read the directive policy block from a plan (namespaced first, bare fallback). */
38
+ export function readPlanPolicy(plan) {
39
+ return readPlanExtension(plan, PLAN_POLICY_KEY, LEGACY_PLAN_POLICY_KEY);
40
+ }
41
+ /** Read the directive completedNote from a plan (namespaced first, bare fallback). */
42
+ export function readPlanCompletedNote(plan) {
43
+ return readPlanExtension(plan, PLAN_COMPLETED_NOTE_KEY, LEGACY_PLAN_COMPLETED_NOTE_KEY);
44
+ }
45
+ /**
46
+ * Rename a legacy bare `policy` key to the namespaced key in place when only
47
+ * the bare key is present. No-op when the namespaced key already exists or no
48
+ * policy key is present. Used by writers so a mutation never strands a legacy
49
+ * bare block alongside the namespaced one.
50
+ */
51
+ export function migrateLegacyPolicyKey(plan) {
52
+ if (plan[PLAN_POLICY_KEY] === undefined && plan[LEGACY_PLAN_POLICY_KEY] !== undefined) {
53
+ plan[PLAN_POLICY_KEY] = plan[LEGACY_PLAN_POLICY_KEY];
54
+ delete plan[LEGACY_PLAN_POLICY_KEY];
55
+ }
56
+ }
57
+ //# sourceMappingURL=plan-extensions.js.map
@@ -1,4 +1,4 @@
1
- /** Filesystem-relative location of the project-definition vBRIEF. */
1
+ /** Filesystem-relative location of the project-definition vBRIEF (display/back-compat). */
2
2
  export declare const PROJECT_DEFINITION_REL_PATH = "vbrief/PROJECT-DEFINITION.vbrief.json";
3
3
  /** Environment variable emergency bypass for branch protection (#747). */
4
4
  export declare const ENV_BYPASS = "DEFT_ALLOW_DEFAULT_BRANCH_COMMIT";
@@ -13,7 +13,11 @@ export interface PolicyResult {
13
13
  readonly deprecationWarning: string | null;
14
14
  readonly error: string | null;
15
15
  }
16
- /** Resolve absolute path to PROJECT-DEFINITION.vbrief.json. */
16
+ /**
17
+ * Resolve absolute path to PROJECT-DEFINITION. Layout-aware (#2109 part 2a):
18
+ * resolves PROJECT-DEFINITION.xbrief.json when the xbrief layout is present,
19
+ * else PROJECT-DEFINITION.vbrief.json (unchanged on today's tree).
20
+ */
17
21
  export declare function projectDefinitionPath(projectRoot: string): string;
18
22
  /** Best-effort coerce a legacy narrative value to a boolean. */
19
23
  export declare function coerceLegacyNarrative(value: unknown): {
@@ -1,6 +1,8 @@
1
1
  import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve as pathResolve } from "node:path";
3
- /** Filesystem-relative location of the project-definition vBRIEF. */
3
+ import { resolveProjectDefinitionPath } from "../layout/resolve.js";
4
+ import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
5
+ /** Filesystem-relative location of the project-definition vBRIEF (display/back-compat). */
4
6
  export const PROJECT_DEFINITION_REL_PATH = "vbrief/PROJECT-DEFINITION.vbrief.json";
5
7
  /** Environment variable emergency bypass for branch protection (#747). */
6
8
  export const ENV_BYPASS = "DEFT_ALLOW_DEFAULT_BRANCH_COMMIT";
@@ -9,9 +11,13 @@ export const LEGACY_NARRATIVE_KEY = "Allow direct commits to master";
9
11
  /** Audit log relative path (#746). */
10
12
  export const AUDIT_LOG_REL_PATH = "meta/policy-changes.log";
11
13
  const TRUTHY = new Set(["1", "true", "yes", "on"]);
12
- /** Resolve absolute path to PROJECT-DEFINITION.vbrief.json. */
14
+ /**
15
+ * Resolve absolute path to PROJECT-DEFINITION. Layout-aware (#2109 part 2a):
16
+ * resolves PROJECT-DEFINITION.xbrief.json when the xbrief layout is present,
17
+ * else PROJECT-DEFINITION.vbrief.json (unchanged on today's tree).
18
+ */
13
19
  export function projectDefinitionPath(projectRoot) {
14
- return join(pathResolve(projectRoot), PROJECT_DEFINITION_REL_PATH);
20
+ return resolveProjectDefinitionPath(pathResolve(projectRoot));
15
21
  }
16
22
  function envBypassActive() {
17
23
  const raw = process.env[ENV_BYPASS] ?? "";
@@ -109,7 +115,7 @@ export function resolvePolicy(projectRoot) {
109
115
  };
110
116
  }
111
117
  const planObj = plan;
112
- const policyBlock = planObj.policy;
118
+ const policyBlock = readPlanPolicy(planObj);
113
119
  if (typeof policyBlock === "object" &&
114
120
  policyBlock !== null &&
115
121
  !Array.isArray(policyBlock) &&
@@ -188,15 +194,19 @@ export function setPolicy(projectRoot, options) {
188
194
  }
189
195
  }
190
196
  const plan = data.plan;
191
- if (typeof plan.policy !== "object" || plan.policy === null || Array.isArray(plan.policy)) {
192
- if (plan.policy === undefined) {
193
- plan.policy = {};
197
+ migrateLegacyPolicyKey(plan);
198
+ const existingPolicy = plan[PLAN_POLICY_KEY];
199
+ if (typeof existingPolicy !== "object" ||
200
+ existingPolicy === null ||
201
+ Array.isArray(existingPolicy)) {
202
+ if (existingPolicy === undefined) {
203
+ plan[PLAN_POLICY_KEY] = {};
194
204
  }
195
205
  else {
196
206
  throw new Error("plan.policy is not an object");
197
207
  }
198
208
  }
199
- const policyBlock = plan.policy;
209
+ const policyBlock = plan[PLAN_POLICY_KEY];
200
210
  const previous = policyBlock.allowDirectCommitsToMaster;
201
211
  policyBlock.allowDirectCommitsToMaster = Boolean(allowDirectCommits);
202
212
  let legacyDropped = false;
@@ -1,5 +1,7 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
- import { join, resolve as pathResolve } from "node:path";
2
+ import { join } from "node:path";
3
+ import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
4
+ import { readPlanPolicy } from "./plan-extensions.js";
3
5
  import { loadProjectDefinition } from "./resolve.js";
4
6
  /** Framework default WIP cap (#1124 / umbrella #1119). */
5
7
  export const DEFAULT_WIP_CAP = 10;
@@ -43,7 +45,7 @@ export function resolveWipCap(projectRoot) {
43
45
  error: "PROJECT-DEFINITION 'plan' is not an object",
44
46
  };
45
47
  }
46
- const policyBlock = plan.policy;
48
+ const policyBlock = readPlanPolicy(plan);
47
49
  if (typeof policyBlock !== "object" ||
48
50
  policyBlock === null ||
49
51
  Array.isArray(policyBlock) ||
@@ -63,7 +65,7 @@ export function resolveWipCap(projectRoot) {
63
65
  /** Count *.vbrief.json files in vbrief/pending/ + vbrief/active/ (#1124). */
64
66
  export function countVbriefWip(projectRoot) {
65
67
  let total = 0;
66
- const vbriefRoot = join(pathResolve(projectRoot), "vbrief");
68
+ const vbriefRoot = resolveLifecycleRoot(projectRoot);
67
69
  for (const sub of WIP_LIFECYCLE_DIRS) {
68
70
  const folder = join(vbriefRoot, sub);
69
71
  if (!existsSync(folder)) {
@@ -71,7 +73,7 @@ export function countVbriefWip(projectRoot) {
71
73
  }
72
74
  const entries = readdirSync(folder, { withFileTypes: true });
73
75
  for (const entry of entries) {
74
- if (entry.isFile() && entry.name.endsWith(".vbrief.json")) {
76
+ if (entry.isFile() && hasArtifactSuffix(entry.name)) {
75
77
  total += 1;
76
78
  }
77
79
  }
@@ -14,6 +14,8 @@ import { execFileSync } from "node:child_process";
14
14
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
15
15
  import { join, relative } from "node:path";
16
16
  import { probeCacheDrift } from "../cache/fetch.js";
17
+ import { resolveEvalPath, resolveProjectDefinitionPath } from "../layout/resolve.js";
18
+ import { readPlanPolicy } from "../policy/plan-extensions.js";
17
19
  import { latestDecisionForIssue as auditLatestDecisionForIssue } from "../triage/actions/candidates-log.js";
18
20
  // ---------------------------------------------------------------------------
19
21
  // Public constants (mirror preflight_cache.py)
@@ -176,7 +178,7 @@ function metaEntryIsTerminalClosed(metaPath) {
176
178
  return false;
177
179
  }
178
180
  function loadScopeRules(projectRoot) {
179
- const defPath = join(projectRoot, "vbrief", "PROJECT-DEFINITION.vbrief.json");
181
+ const defPath = resolveProjectDefinitionPath(projectRoot);
180
182
  if (!existsSync(defPath))
181
183
  return null;
182
184
  try {
@@ -186,7 +188,7 @@ function loadScopeRules(projectRoot) {
186
188
  const plan = data.plan;
187
189
  if (plan === null || typeof plan !== "object" || Array.isArray(plan))
188
190
  return null;
189
- const policy = plan.policy;
191
+ const policy = readPlanPolicy(plan);
190
192
  if (policy === null || typeof policy !== "object" || Array.isArray(policy))
191
193
  return null;
192
194
  const scope = policy.triageScope;
@@ -325,7 +327,8 @@ export function evaluate(projectRoot, options = {}) {
325
327
  (envMaxAgeParsed !== null && !Number.isNaN(envMaxAgeParsed) ? envMaxAgeParsed : null) ??
326
328
  DEFAULT_MAX_AGE_HOURS;
327
329
  const cacheRoot = join(projectRoot, CACHE_DIR_NAME);
328
- const candidatesPath = join(projectRoot, CANDIDATES_RELPATH);
330
+ // Layout-aware (#2109 part 2a): resolve under xbrief/.eval when migrated.
331
+ const candidatesPath = resolveEvalPath(projectRoot, "candidates.jsonl");
329
332
  // Step 1: Resolve repo slug
330
333
  const resolvedRepo = resolveRepo(projectRoot, cacheRoot, source, options.repo ?? null);
331
334
  // Step 2: Check cache dir exists
@@ -357,7 +360,7 @@ export function evaluate(projectRoot, options = {}) {
357
360
  return {
358
361
  code: 2,
359
362
  message: [
360
- `❌ deft cache-fresh: ${CANDIDATES_RELPATH} not found at ${projectRoot}.`,
363
+ `❌ deft cache-fresh: ${candidatesPath} not found.`,
361
364
  REMEDIATION_NO_CANDIDATES,
362
365
  ].join("\n"),
363
366
  };
@@ -511,7 +514,7 @@ function evaluateForIssue(repo, issueNumber, candidatesPath, cacheRoot, source,
511
514
  return {
512
515
  code: 1,
513
516
  message: [
514
- `❌ deft cache-fresh: issue #${issueNumber} has no triage decision in ${CANDIDATES_RELPATH}.`,
517
+ `❌ deft cache-fresh: issue #${issueNumber} has no triage decision in ${candidatesPath}.`,
515
518
  ` Recovery: \`deft triage:accept -- --repo ${repo} --issue ${issueNumber}\` before dispatching an implementation agent.`,
516
519
  ].join("\n"),
517
520
  };
@@ -6,11 +6,12 @@ import { spawnSync } from "node:child_process";
6
6
  import { dirname, join } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { fetchIssueStates, isTerminalLifecyclePath, reconcile, scanVbriefDir, } from "../intake/reconcile-issues.js";
9
+ import { resolveLifecycleFolder, resolveLifecycleRoot } from "../layout/resolve.js";
9
10
  import { renderRoadmap } from "../render/roadmap-render.js";
10
11
  const BUILD_DIST_RUNNER = join(dirname(fileURLToPath(import.meta.url)), "build-dist-runner.js");
11
12
  export function refreshRoadmapNative(projectRoot) {
12
- const pending = join(projectRoot, "vbrief", "pending");
13
- const completed = join(projectRoot, "vbrief", "completed");
13
+ const pending = resolveLifecycleFolder(projectRoot, "pending");
14
+ const completed = resolveLifecycleFolder(projectRoot, "completed");
14
15
  const roadmap = join(projectRoot, "ROADMAP.md");
15
16
  const [ok, msg] = renderRoadmap(pending, roadmap, completed);
16
17
  if (!ok) {
@@ -19,7 +20,7 @@ export function refreshRoadmapNative(projectRoot) {
19
20
  return [true, "ROADMAP.md re-rendered"];
20
21
  }
21
22
  export function checkVbriefLifecycleSyncNative(projectRoot, repo) {
22
- const vbriefDir = join(projectRoot, "vbrief");
23
+ const vbriefDir = resolveLifecycleRoot(projectRoot);
23
24
  try {
24
25
  const issueToVbriefs = scanVbriefDir(vbriefDir);
25
26
  const issueStateMap = fetchIssueStates(repo, new Set(issueToVbriefs.keys()), {
@@ -3,6 +3,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { cmdDoctor } from "../doctor/main.js";
6
+ import { resolveLifecycleRoot } from "../layout/resolve.js";
6
7
  function spec(name, entrypoint, opts = {}) {
7
8
  return { name, entrypoint, ...opts };
8
9
  }
@@ -232,7 +233,7 @@ function argvForSpec(commandSpec, argv, projectRoot, frameworkRoot) {
232
233
  if (commandSpec.frameworkRootArg)
233
234
  resolved.push(commandSpec.frameworkRootArg, frameworkRoot);
234
235
  if (commandSpec.vbriefDirArg)
235
- resolved.push(commandSpec.vbriefDirArg, join(projectRoot, "vbrief"));
236
+ resolved.push(commandSpec.vbriefDirArg, resolveLifecycleRoot(projectRoot));
236
237
  if (commandSpec.rootArg)
237
238
  resolved.push(commandSpec.rootArg, frameworkRoot);
238
239
  resolved.push(...normalizeTaskSeparator(argv));
@@ -52,8 +52,8 @@ export function renderPrd(specPath, outputPath, options = {}) {
52
52
  }
53
53
  }
54
54
  lines.push("---\n" +
55
- "*This document is auto-generated from `vbrief/specification.vbrief.json` " +
56
- "via `task prd:render`. Do not edit directly.*\n");
55
+ "*This document is auto-generated from the specification artifact " +
56
+ "(e.g. `xbrief/specification.xbrief.json`) via `task prd:render`. Do not edit directly.*\n");
57
57
  writeFileSync(outputPath, lines.join("\n"), "utf8");
58
58
  process.stdout.write(`PRD.md written to ${outputPath}\n`);
59
59
  }
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { join, resolve } from "node:path";
3
+ import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
3
4
  import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
4
5
  import { deriveRegistryItemStatus, registryMetadataReferencesFromScope, } from "../vbrief-validate/registry-status.js";
5
6
  import { PROJECT_LIFECYCLE_FOLDERS, SKELETON_NARRATIVES } from "./constants.js";
@@ -13,7 +14,7 @@ export function scanLifecycleFolders(vbriefDir) {
13
14
  let files;
14
15
  try {
15
16
  files = readdirSync(folder)
16
- .filter((n) => n.endsWith(".vbrief.json"))
17
+ .filter((n) => hasArtifactSuffix(n))
17
18
  .sort();
18
19
  }
19
20
  catch {
@@ -260,11 +261,28 @@ export function acknowledgeProjectDefinitionStaleness(vbriefDir, options = {}) {
260
261
  export function main(argv) {
261
262
  const acknowledge = argv[0] === "--acknowledge-staleness";
262
263
  const rest = acknowledge ? argv.slice(1) : argv;
263
- if (rest.length > 1) {
264
- process.stderr.write("Usage: project_render.py [--acknowledge-staleness] [vbrief_dir]\n");
264
+ let projectRoot;
265
+ const positional = [];
266
+ for (let i = 0; i < rest.length; i += 1) {
267
+ const arg = rest[i];
268
+ if (arg === "--project-root") {
269
+ projectRoot = rest[i + 1];
270
+ i += 1;
271
+ }
272
+ else if (arg.startsWith("--project-root=")) {
273
+ projectRoot = arg.slice("--project-root=".length);
274
+ }
275
+ else {
276
+ positional.push(arg);
277
+ }
278
+ }
279
+ if (positional.length > 1) {
280
+ process.stderr.write("Usage: project-render [--acknowledge-staleness] [--project-root <dir>] [vbrief_dir]\n");
265
281
  return 2;
266
282
  }
267
- const vbriefDir = rest[0] ?? "vbrief";
283
+ const vbriefDir = projectRoot !== undefined
284
+ ? resolveLifecycleRoot(resolve(projectRoot))
285
+ : (positional[0] ?? "vbrief");
268
286
  const [ok, message] = acknowledge
269
287
  ? acknowledgeProjectDefinitionStaleness(vbriefDir)
270
288
  : renderProjectDefinition(vbriefDir);
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
3
4
  import { MIGRATOR_METADATA_KEY, ROADMAP_BANNER } from "./constants.js";
4
5
  import { phaseSortKey } from "./text-utils.js";
5
6
  function scopeMetadataRank(plan) {
@@ -32,7 +33,7 @@ function loadVbriefs(folder) {
32
33
  let files;
33
34
  try {
34
35
  files = readdirSync(folder)
35
- .filter((n) => n.endsWith(".vbrief.json"))
36
+ .filter((n) => hasArtifactSuffix(n))
36
37
  .sort();
37
38
  }
38
39
  catch {
@@ -363,10 +364,10 @@ export function renderRoadmap(pendingDir, outPath, completedDir) {
363
364
  export function checkDrift(pendingDir, roadmapPath, completedDir) {
364
365
  const expected = renderRoadmapToBuffer(pendingDir, completedDir);
365
366
  if (!existsSync(roadmapPath)) {
366
- const hasPending = existsSync(pendingDir) && readdirSync(pendingDir).some((n) => n.endsWith(".vbrief.json"));
367
+ const hasPending = existsSync(pendingDir) && readdirSync(pendingDir).some((n) => hasArtifactSuffix(n));
367
368
  const inferredCompleted = resolveCompletedDir(pendingDir, completedDir);
368
369
  const hasCompleted = existsSync(inferredCompleted) &&
369
- readdirSync(inferredCompleted).some((n) => n.endsWith(".vbrief.json"));
370
+ readdirSync(inferredCompleted).some((n) => hasArtifactSuffix(n));
370
371
  if (!hasPending && !hasCompleted) {
371
372
  return [true, "✓ No ROADMAP.md needed (no pending or completed vBRIEFs)"];
372
373
  }
@@ -379,10 +380,37 @@ export function checkDrift(pendingDir, roadmapPath, completedDir) {
379
380
  }
380
381
  /** CLI entry (mirrors ``scripts/roadmap_render.main``). */
381
382
  export function main(argv) {
382
- const positional = argv.filter((a) => !a.startsWith("--"));
383
- const pendingDir = positional[0] ?? join(process.cwd(), "vbrief", "pending");
384
- const outPath = positional[1] ?? join(process.cwd(), "ROADMAP.md");
385
- if (argv.includes("--check")) {
383
+ let projectRoot;
384
+ let check = false;
385
+ const positional = [];
386
+ for (let i = 0; i < argv.length; i += 1) {
387
+ const arg = argv[i];
388
+ if (arg === "--project-root") {
389
+ projectRoot = argv[i + 1];
390
+ i += 1;
391
+ }
392
+ else if (arg.startsWith("--project-root=")) {
393
+ projectRoot = arg.slice("--project-root=".length);
394
+ }
395
+ else if (arg === "--check") {
396
+ check = true;
397
+ }
398
+ else {
399
+ positional.push(arg);
400
+ }
401
+ }
402
+ let pendingDir;
403
+ let outPath;
404
+ if (projectRoot !== undefined) {
405
+ const lifecycleRoot = resolveLifecycleRoot(resolve(projectRoot));
406
+ pendingDir = join(lifecycleRoot, "pending");
407
+ outPath = positional[0] ?? join(resolve(projectRoot), "ROADMAP.md");
408
+ }
409
+ else {
410
+ pendingDir = positional[0] ?? join(process.cwd(), "vbrief", "pending");
411
+ outPath = positional[1] ?? join(process.cwd(), "ROADMAP.md");
412
+ }
413
+ if (check) {
386
414
  const [ok, msg] = checkDrift(pendingDir, outPath);
387
415
  process.stdout.write(`${msg}\n`);
388
416
  return ok ? 0 : 1;
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { hasArtifactSuffix, stripArtifactSuffix } from "../layout/resolve.js";
3
4
  import { PROPOSED_DISCLAIMER_LINES, PROPOSED_STATUS_FILTER } from "../spec-authority/constants.js";
4
5
  import { SCOPE_SUMMARY_NARRATIVES } from "./constants.js";
5
6
  function readEdgeEndpoints(edge) {
@@ -14,7 +15,7 @@ function loadScopeVbriefs(folder) {
14
15
  let entries;
15
16
  try {
16
17
  entries = readdirSync(folder)
17
- .filter((n) => n.endsWith(".vbrief.json"))
18
+ .filter((n) => hasArtifactSuffix(n))
18
19
  .sort();
19
20
  }
20
21
  catch {
@@ -24,7 +25,7 @@ function loadScopeVbriefs(folder) {
24
25
  for (const name of entries) {
25
26
  try {
26
27
  const data = JSON.parse(readFileSync(join(folder, name), "utf8"));
27
- const stem = name.endsWith(".vbrief.json") ? name.slice(0, -".vbrief.json".length) : name;
28
+ const stem = stripArtifactSuffix(name);
28
29
  out.push([stem, data]);
29
30
  }
30
31
  catch {
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync, writeSync, } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
- import { AUDIT_LOG_REL_PATH } from "./constants.js";
4
+ import { resolveEvalPath } from "../layout/resolve.js";
5
5
  import { utcNowIso } from "./vbrief-json.js";
6
6
  export class ScopeAuditLogError extends Error {
7
7
  constructor(message) {
@@ -94,7 +94,8 @@ export function newDecisionId() {
94
94
  return randomUUID();
95
95
  }
96
96
  export function canonicalLogPath(projectRoot) {
97
- return join(resolve(projectRoot), AUDIT_LOG_REL_PATH);
97
+ // Layout-aware (#2109 part 2a): xbrief/.eval when migrated, else vbrief/.eval.
98
+ return resolveEvalPath(resolve(projectRoot), "scope-lifecycle.jsonl");
98
99
  }
99
100
  function withAppendLock(logPath, fn) {
100
101
  const lockPath = `${logPath}.lock`;