@deftai/directive-core 0.64.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 (165) 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 +3 -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 +17 -14
  23. package/dist/intake/markdown-scanners.d.ts +3 -1
  24. package/dist/intake/markdown-scanners.js +10 -3
  25. package/dist/intake/reconcile-issues.js +13 -4
  26. package/dist/layout/index.d.ts +2 -0
  27. package/dist/layout/index.js +2 -0
  28. package/dist/layout/resolve.d.ts +87 -0
  29. package/dist/layout/resolve.js +159 -0
  30. package/dist/lifecycle/lifecycle-hygiene.d.ts +1 -1
  31. package/dist/lifecycle/lifecycle-hygiene.js +8 -5
  32. package/dist/orchestration/judgment-policy.js +4 -6
  33. package/dist/orchestration/verify-judgment-gates.d.ts +1 -0
  34. package/dist/orchestration/verify-judgment-gates.js +4 -2
  35. package/dist/policy/autonomy.js +4 -6
  36. package/dist/policy/capacity.js +4 -6
  37. package/dist/policy/decisions.d.ts +1 -0
  38. package/dist/policy/decisions.js +4 -2
  39. package/dist/policy/index.d.ts +1 -0
  40. package/dist/policy/index.js +3 -1
  41. package/dist/policy/plan-extensions.d.ts +35 -0
  42. package/dist/policy/plan-extensions.js +57 -0
  43. package/dist/policy/resolve.d.ts +6 -2
  44. package/dist/policy/resolve.js +18 -8
  45. package/dist/policy/wip.js +6 -4
  46. package/dist/preflight-cache/evaluate.js +11 -45
  47. package/dist/release/native-steps.js +4 -3
  48. package/dist/render/framework-commands.js +2 -1
  49. package/dist/render/prd-render.js +2 -2
  50. package/dist/render/project-render.js +23 -5
  51. package/dist/render/roadmap-render.js +36 -8
  52. package/dist/render/scope-outlook.js +3 -2
  53. package/dist/scope/audit-log.js +3 -2
  54. package/dist/scope/capacity-stamp.js +2 -1
  55. package/dist/scope/decompose.js +6 -4
  56. package/dist/scope/decomposed-refs.js +2 -1
  57. package/dist/scope/demote.js +5 -4
  58. package/dist/scope/project-context.js +2 -1
  59. package/dist/scope/project-definition-sync.js +4 -3
  60. package/dist/scope/transition.js +6 -2
  61. package/dist/scope/undo.js +2 -1
  62. package/dist/scope/vbrief-ref.js +5 -3
  63. package/dist/session/resume-conditions.js +2 -1
  64. package/dist/session/ritual-sentinel.js +2 -1
  65. package/dist/session/staleness.js +2 -1
  66. package/dist/slice/record.js +4 -4
  67. package/dist/spec-authority/resolver.js +5 -5
  68. package/dist/swarm/complete-cohort.js +5 -4
  69. package/dist/swarm/launch.js +6 -7
  70. package/dist/swarm/readiness.js +22 -20
  71. package/dist/swarm/subagent-backend.js +2 -5
  72. package/dist/swarm/verify-review-clean.js +2 -1
  73. package/dist/triage/actions/candidates-log.d.ts +3 -0
  74. package/dist/triage/actions/candidates-log.js +8 -2
  75. package/dist/triage/bootstrap/gitignore.js +22 -13
  76. package/dist/triage/bootstrap/index.js +7 -4
  77. package/dist/triage/bulk/index.js +2 -1
  78. package/dist/triage/classify/index.js +13 -11
  79. package/dist/triage/queue/audit.js +3 -3
  80. package/dist/triage/queue/build-queue.js +6 -0
  81. package/dist/triage/queue/cache.js +12 -2
  82. package/dist/triage/queue/index.d.ts +1 -0
  83. package/dist/triage/queue/index.js +1 -0
  84. package/dist/triage/queue/ranking-labels.js +2 -1
  85. package/dist/triage/queue/scope-ignores-filter.d.ts +12 -0
  86. package/dist/triage/queue/scope-ignores-filter.js +50 -0
  87. package/dist/triage/queue/scope-walk.js +8 -6
  88. package/dist/triage/queue/types.d.ts +4 -0
  89. package/dist/triage/reconcile/audit.js +2 -1
  90. package/dist/triage/reconcile/parse-uri.js +2 -1
  91. package/dist/triage/reconcile/reconcile.js +8 -7
  92. package/dist/triage/refresh/extract.js +4 -2
  93. package/dist/triage/refresh/refresh.js +3 -2
  94. package/dist/triage/scope/mutations-core.js +13 -9
  95. package/dist/triage/scope/renderers.js +5 -3
  96. package/dist/triage/scope/resolve.js +6 -5
  97. package/dist/triage/scope/validate.js +3 -6
  98. package/dist/triage/scope-drift/add-ignore.js +4 -2
  99. package/dist/triage/scope-drift/scope-rules.js +3 -2
  100. package/dist/triage/subscribe/index.js +14 -8
  101. package/dist/triage/summary/index.d.ts +33 -0
  102. package/dist/triage/summary/index.js +180 -6
  103. package/dist/triage/summary/reconcilable.js +6 -4
  104. package/dist/triage/summary/scope-drift.js +2 -1
  105. package/dist/triage/welcome/default-mode.d.ts +1 -0
  106. package/dist/triage/welcome/default-mode.js +1 -0
  107. package/dist/triage/welcome/prior-state.js +9 -9
  108. package/dist/triage/welcome/summary.d.ts +2 -0
  109. package/dist/triage/welcome/summary.js +15 -8
  110. package/dist/triage/welcome/writers.js +12 -10
  111. package/dist/validate-content/capacity-policy.js +5 -4
  112. package/dist/validate-content/capacity-show.js +5 -3
  113. package/dist/validate-content/filename.d.ts +1 -1
  114. package/dist/validate-content/filename.js +5 -4
  115. package/dist/validate-content/validate-strategy-output.js +14 -5
  116. package/dist/vbrief-build/build.js +1 -1
  117. package/dist/vbrief-build/constants.js +7 -0
  118. package/dist/vbrief-build/speckit.js +3 -2
  119. package/dist/vbrief-reconcile/graph.js +3 -2
  120. package/dist/vbrief-reconcile/labels.js +4 -3
  121. package/dist/vbrief-reconcile/reconciliation.js +5 -1
  122. package/dist/vbrief-reconcile/swarm-deps.js +6 -5
  123. package/dist/vbrief-reconcile/umbrellas.d.ts +1 -1
  124. package/dist/vbrief-reconcile/umbrellas.js +9 -7
  125. package/dist/vbrief-validate/conformance.d.ts +1 -2
  126. package/dist/vbrief-validate/conformance.js +29 -14
  127. package/dist/vbrief-validate/constants.d.ts +18 -1
  128. package/dist/vbrief-validate/constants.js +32 -3
  129. package/dist/vbrief-validate/epic-links.js +3 -2
  130. package/dist/vbrief-validate/filename.js +5 -3
  131. package/dist/vbrief-validate/index.d.ts +3 -1
  132. package/dist/vbrief-validate/index.js +2 -1
  133. package/dist/vbrief-validate/main.js +17 -6
  134. package/dist/vbrief-validate/origin.js +1 -1
  135. package/dist/vbrief-validate/paths.js +2 -1
  136. package/dist/vbrief-validate/plan-hooks.js +4 -3
  137. package/dist/vbrief-validate/registry-status.d.ts +1 -0
  138. package/dist/vbrief-validate/registry-status.js +3 -1
  139. package/dist/vbrief-validate/roundtrip.d.ts +36 -0
  140. package/dist/vbrief-validate/roundtrip.js +168 -0
  141. package/dist/vbrief-validate/schema.d.ts +1 -1
  142. package/dist/vbrief-validate/schema.js +62 -19
  143. package/dist/vbrief-validate/validate-all.js +12 -4
  144. package/dist/verify-source/code-structure-validate.js +8 -4
  145. package/dist/verify-source/contract-drift.d.ts +33 -0
  146. package/dist/verify-source/contract-drift.js +170 -0
  147. package/dist/verify-source/index.d.ts +1 -0
  148. package/dist/verify-source/index.js +1 -0
  149. package/dist/xbrief-migrate/constants.d.ts +13 -0
  150. package/dist/xbrief-migrate/constants.js +14 -0
  151. package/dist/xbrief-migrate/detect.d.ts +11 -0
  152. package/dist/xbrief-migrate/detect.js +81 -0
  153. package/dist/xbrief-migrate/drift-gate.d.ts +56 -0
  154. package/dist/xbrief-migrate/drift-gate.js +205 -0
  155. package/dist/xbrief-migrate/index.d.ts +7 -0
  156. package/dist/xbrief-migrate/index.js +7 -0
  157. package/dist/xbrief-migrate/migrate-project.d.ts +33 -0
  158. package/dist/xbrief-migrate/migrate-project.js +189 -0
  159. package/dist/xbrief-migrate/signpost.d.ts +5 -0
  160. package/dist/xbrief-migrate/signpost.js +19 -0
  161. package/dist/xbrief-migrate/transforms.d.ts +46 -0
  162. package/dist/xbrief-migrate/transforms.js +182 -0
  163. package/dist/xbrief-migrate/version.d.ts +8 -0
  164. package/dist/xbrief-migrate/version.js +25 -0
  165. package/package.json +11 -3
@@ -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 = [];
@@ -298,6 +304,6 @@ export function subscribe(projectRoot, options = {}) {
298
304
  export function unsubscribe(projectRoot, options = {}) {
299
305
  return mutate(projectRoot, { op: "unsubscribe", ...options });
300
306
  }
301
- export const RECONCILE_HINT = " Reconciliation: run `task triage:bootstrap -- --resume` to " +
307
+ export const RECONCILE_HINT = " Reconciliation: run `task triage:bootstrap` to " +
302
308
  "backfill / mark out-of-scope cached entries.";
303
309
  //# sourceMappingURL=index.js.map
@@ -8,6 +8,8 @@ export declare const CANDIDATES_LOG_REL_PATH = "vbrief/.eval/candidates.jsonl";
8
8
  export { latestDecisions, readAuditLog } from "../actions/candidates-log.js";
9
9
  export declare const SUMMARY_HISTORY_REL_PATH = "vbrief/.eval/summary-history.jsonl";
10
10
  export declare const SUMMARY_HISTORY_SCHEMA = "deft.triage.summary.v1";
11
+ /** D2 repeat-suppression window for session-start triage one-liner (#1122 / #1279). */
12
+ export declare const D2_SUPPRESSION_WINDOW_HOURS = 4;
11
13
  export declare const EMPTY_CACHE_LINE = "[triage] cache empty -- run task triage:bootstrap";
12
14
  export declare const WIP_LIFECYCLE_DIRS: readonly ["pending", "active"];
13
15
  export declare const FILESYSTEM_IN_FLIGHT_FOLDER = "active";
@@ -45,6 +47,10 @@ export declare function countFilesystemInFlight(projectRoot: string): number;
45
47
  export declare function isTriageScopeExplicitlyConfigured(projectRoot: string): boolean;
46
48
  /** Read `plan.policy.wipCap`; always returns an integer cap. */
47
49
  export declare function resolveWipCapInt(projectRoot: string): number;
50
+ /** Read `raw.json` state for a cached github-issue entry; defaults to open when absent. */
51
+ export declare function readCachedIssueState(cacheRoot: string, repo: string, issueNumber: number): string;
52
+ /** True when a cached issue should participate in summary classification counts. */
53
+ export declare function isCachedIssueOpen(cacheRoot: string, repo: string, issueNumber: number): boolean;
48
54
  export declare function computeSummary(projectRoot: string, options?: ComputeSummaryOptions): SummaryResult;
49
55
  export declare function formatOneLiner(result: SummaryResult, options?: {
50
56
  maxChars?: number;
@@ -58,6 +64,33 @@ export declare function summaryResultToRecord(result: SummaryResult, options: {
58
64
  emittedAt: string;
59
65
  line: string;
60
66
  }): Record<string, unknown>;
67
+ /** Structured fields that drive operator-visible triage summary output (#1279). */
68
+ export type SuppressionKeyFields = {
69
+ readonly cache_empty: boolean;
70
+ readonly untriaged: number;
71
+ readonly stale_defer: number;
72
+ readonly in_flight: number;
73
+ readonly in_flight_filesystem: number;
74
+ readonly in_flight_cache_scoped: number;
75
+ readonly triage_scope_configured: boolean;
76
+ readonly wip_count: number;
77
+ readonly wip_cap: number;
78
+ readonly repos: readonly string[];
79
+ readonly scope_drift: number;
80
+ readonly reconcilable: number;
81
+ };
82
+ export declare function suppressionKeyFieldsFromResult(result: SummaryResult): SuppressionKeyFields;
83
+ /** Extract D2 suppression key fields from a summary-history JSONL record. */
84
+ export declare function suppressionKeyFieldsFromRecord(record: Record<string, unknown>): SuppressionKeyFields | null;
85
+ /** Canonical stable hash for D2 suppression comparison (#1279). */
86
+ export declare function suppressionKey(result: SummaryResult): string;
87
+ export declare function suppressionKeyFromRecord(record: Record<string, unknown>): string | null;
88
+ /** Read the most recent JSONL record from summary-history.jsonl. */
89
+ export declare function readLastHistoryRecord(historyPath: string): Record<string, unknown> | null;
90
+ /** True when D2 should skip stdout emission (key unchanged within 4h). */
91
+ export declare function shouldSuppressD2Emission(result: SummaryResult, historyPath: string, options?: {
92
+ now?: Date;
93
+ }): boolean;
61
94
  /** Match Python `json.dumps(..., sort_keys=True, ensure_ascii=False)` spacing. */
62
95
  export declare function pythonStyleStringify(value: unknown): string;
63
96
  /** Append a single JSONL record to summary-history.jsonl. Never throws. */
@@ -1,5 +1,8 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
2
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";
3
6
  import { loadProjectDefinition, PROJECT_DEFINITION_REL_PATH } from "../../policy/resolve.js";
4
7
  import { countVbriefWip, DEFAULT_WIP_CAP, resolveWipCap } from "../../policy/wip.js";
5
8
  import { AUDIT_LOG_REL_PATH, latestDecisions, readAuditLog } from "../actions/candidates-log.js";
@@ -16,6 +19,8 @@ export const CANDIDATES_LOG_REL_PATH = AUDIT_LOG_REL_PATH;
16
19
  export { latestDecisions, readAuditLog } from "../actions/candidates-log.js";
17
20
  export const SUMMARY_HISTORY_REL_PATH = "vbrief/.eval/summary-history.jsonl";
18
21
  export const SUMMARY_HISTORY_SCHEMA = "deft.triage.summary.v1";
22
+ /** D2 repeat-suppression window for session-start triage one-liner (#1122 / #1279). */
23
+ export const D2_SUPPRESSION_WINDOW_HOURS = 4;
19
24
  export const EMPTY_CACHE_LINE = "[triage] cache empty -- run task triage:bootstrap";
20
25
  export const WIP_LIFECYCLE_DIRS = ["pending", "active"];
21
26
  export const FILESYSTEM_IN_FLIGHT_FOLDER = "active";
@@ -90,13 +95,13 @@ export function iterCachedIssues(cacheRoot) {
90
95
  // ---------------------------------------------------------------------------
91
96
  /** Count filesystem-truth in-flight vBRIEFs (#1270). */
92
97
  export function countFilesystemInFlight(projectRoot) {
93
- const folder = join(pathResolve(projectRoot), "vbrief", FILESYSTEM_IN_FLIGHT_FOLDER);
98
+ const folder = resolveLifecycleFolder(projectRoot, FILESYSTEM_IN_FLIGHT_FOLDER);
94
99
  if (!existsSync(folder)) {
95
100
  return 0;
96
101
  }
97
102
  let total = 0;
98
103
  for (const entry of readdirSync(folder, { withFileTypes: true })) {
99
- if (!entry.isFile() || !entry.name.endsWith(".vbrief.json")) {
104
+ if (!entry.isFile() || !hasArtifactSuffix(entry.name)) {
100
105
  continue;
101
106
  }
102
107
  try {
@@ -121,7 +126,7 @@ export function countFilesystemInFlight(projectRoot) {
121
126
  }
122
127
  /** True iff `plan.policy.triageScope` is a non-empty list of dict rules. */
123
128
  export function isTriageScopeExplicitlyConfigured(projectRoot) {
124
- const path = join(pathResolve(projectRoot), PROJECT_DEFINITION_REL_PATH);
129
+ const path = resolveProjectDefinitionPath(projectRoot);
125
130
  if (!existsSync(path)) {
126
131
  return false;
127
132
  }
@@ -134,7 +139,7 @@ export function isTriageScopeExplicitlyConfigured(projectRoot) {
134
139
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
135
140
  return false;
136
141
  }
137
- const policy = plan.policy;
142
+ const policy = readPlanPolicy(plan);
138
143
  if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
139
144
  return false;
140
145
  }
@@ -155,13 +160,36 @@ export function resolveWipCapInt(projectRoot) {
155
160
  function decisionKey(repo, issueNumber) {
156
161
  return `${repo}\0${issueNumber}`;
157
162
  }
163
+ /** Read `raw.json` state for a cached github-issue entry; defaults to open when absent. */
164
+ export function readCachedIssueState(cacheRoot, repo, issueNumber) {
165
+ const [owner, name] = repo.split("/", 2);
166
+ const rawPath = join(cacheRoot, CACHE_SOURCE, owner ?? "", name ?? "", String(issueNumber), "raw.json");
167
+ if (!existsSync(rawPath)) {
168
+ return "open";
169
+ }
170
+ try {
171
+ const raw = JSON.parse(readFileSync(rawPath, { encoding: "utf8" }));
172
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
173
+ return "open";
174
+ }
175
+ const stateRaw = raw.state ?? "open";
176
+ return typeof stateRaw === "string" ? stateRaw.toLowerCase() : "open";
177
+ }
178
+ catch {
179
+ return "open";
180
+ }
181
+ }
182
+ /** True when a cached issue should participate in summary classification counts. */
183
+ export function isCachedIssueOpen(cacheRoot, repo, issueNumber) {
184
+ return readCachedIssueState(cacheRoot, repo, issueNumber) !== "closed";
185
+ }
158
186
  // ---------------------------------------------------------------------------
159
187
  // compute / format / persist
160
188
  // ---------------------------------------------------------------------------
161
189
  export function computeSummary(projectRoot, options = {}) {
162
190
  const root = pathResolve(projectRoot);
163
191
  const resolvedCacheRoot = options.cacheRoot ?? join(root, CACHE_DIR_NAME);
164
- const resolvedLogPath = options.auditLogPath ?? join(root, CANDIDATES_LOG_REL_PATH);
192
+ const resolvedLogPath = options.auditLogPath ?? resolveEvalPath(root, "candidates.jsonl");
165
193
  const cached = iterCachedIssues(resolvedCacheRoot);
166
194
  const repos = [...new Set(cached.map(([repo]) => repo))].sort();
167
195
  const wipCap = resolveWipCapInt(root);
@@ -191,6 +219,9 @@ export function computeSummary(projectRoot, options = {}) {
191
219
  let staleDefer = 0;
192
220
  const noDecisionKeys = [];
193
221
  for (const [repo, issueNumber] of cached) {
222
+ if (!isCachedIssueOpen(resolvedCacheRoot, repo, issueNumber)) {
223
+ continue;
224
+ }
194
225
  const decision = decisions.get(decisionKey(repo, issueNumber));
195
226
  if (decision === undefined || decision === "reset" || !TRIAGED_DECISIONS.has(decision)) {
196
227
  untriaged += 1;
@@ -319,6 +350,11 @@ export function summaryResultToRecord(result, options) {
319
350
  schema: SUMMARY_HISTORY_SCHEMA,
320
351
  emitted_at: options.emittedAt,
321
352
  line: options.line,
353
+ ...suppressionKeyFieldsFromResult(result),
354
+ };
355
+ }
356
+ export function suppressionKeyFieldsFromResult(result) {
357
+ return {
322
358
  cache_empty: result.cacheEmpty,
323
359
  untriaged: result.untriaged,
324
360
  stale_defer: result.staleDefer,
@@ -328,11 +364,149 @@ export function summaryResultToRecord(result, options) {
328
364
  triage_scope_configured: result.triageScopeConfigured,
329
365
  wip_count: result.wipCount,
330
366
  wip_cap: result.wipCap,
331
- repos: [...result.repos],
367
+ repos: [...result.repos].sort(),
332
368
  scope_drift: result.scopeDrift,
333
369
  reconcilable: result.reconcilable,
334
370
  };
335
371
  }
372
+ function readNumericField(record, key) {
373
+ const value = record[key];
374
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
375
+ }
376
+ function readBooleanField(record, key) {
377
+ const value = record[key];
378
+ return typeof value === "boolean" ? value : null;
379
+ }
380
+ function readStringArrayField(record, key) {
381
+ const value = record[key];
382
+ if (!Array.isArray(value)) {
383
+ return null;
384
+ }
385
+ if (!value.every((entry) => typeof entry === "string")) {
386
+ return null;
387
+ }
388
+ return [...value].sort();
389
+ }
390
+ /** Extract D2 suppression key fields from a summary-history JSONL record. */
391
+ export function suppressionKeyFieldsFromRecord(record) {
392
+ const cacheEmpty = readBooleanField(record, "cache_empty");
393
+ const untriaged = readNumericField(record, "untriaged");
394
+ const staleDefer = readNumericField(record, "stale_defer");
395
+ const inFlight = readNumericField(record, "in_flight");
396
+ const inFlightFilesystem = readNumericField(record, "in_flight_filesystem");
397
+ const inFlightCacheScoped = readNumericField(record, "in_flight_cache_scoped");
398
+ const triageScopeConfigured = readBooleanField(record, "triage_scope_configured");
399
+ const wipCount = readNumericField(record, "wip_count");
400
+ const wipCap = readNumericField(record, "wip_cap");
401
+ const repos = readStringArrayField(record, "repos");
402
+ const scopeDrift = readNumericField(record, "scope_drift");
403
+ const reconcilable = readNumericField(record, "reconcilable");
404
+ if (cacheEmpty === null ||
405
+ untriaged === null ||
406
+ staleDefer === null ||
407
+ inFlight === null ||
408
+ inFlightFilesystem === null ||
409
+ inFlightCacheScoped === null ||
410
+ triageScopeConfigured === null ||
411
+ wipCount === null ||
412
+ wipCap === null ||
413
+ repos === null ||
414
+ scopeDrift === null ||
415
+ reconcilable === null) {
416
+ return null;
417
+ }
418
+ return {
419
+ cache_empty: cacheEmpty,
420
+ untriaged,
421
+ stale_defer: staleDefer,
422
+ in_flight: inFlight,
423
+ in_flight_filesystem: inFlightFilesystem,
424
+ in_flight_cache_scoped: inFlightCacheScoped,
425
+ triage_scope_configured: triageScopeConfigured,
426
+ wip_count: wipCount,
427
+ wip_cap: wipCap,
428
+ repos,
429
+ scope_drift: scopeDrift,
430
+ reconcilable,
431
+ };
432
+ }
433
+ /** Canonical stable hash for D2 suppression comparison (#1279). */
434
+ export function suppressionKey(result) {
435
+ return createHash("sha256")
436
+ .update(pythonStyleStringify(suppressionKeyFieldsFromResult(result)))
437
+ .digest("hex");
438
+ }
439
+ export function suppressionKeyFromRecord(record) {
440
+ const fields = suppressionKeyFieldsFromRecord(record);
441
+ if (fields === null) {
442
+ return null;
443
+ }
444
+ return createHash("sha256").update(pythonStyleStringify(fields)).digest("hex");
445
+ }
446
+ function parseHistoryEmittedAt(record) {
447
+ const raw = record.emitted_at;
448
+ if (typeof raw !== "string" || raw.trim().length === 0) {
449
+ return null;
450
+ }
451
+ let candidate = raw.trim();
452
+ if (candidate.endsWith("Z")) {
453
+ candidate = `${candidate.slice(0, -1)}+00:00`;
454
+ }
455
+ const parsed = new Date(candidate);
456
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
457
+ }
458
+ /** Read the most recent JSONL record from summary-history.jsonl. */
459
+ export function readLastHistoryRecord(historyPath) {
460
+ if (!existsSync(historyPath)) {
461
+ return null;
462
+ }
463
+ let text;
464
+ try {
465
+ text = readFileSync(historyPath, { encoding: "utf8" });
466
+ }
467
+ catch {
468
+ return null;
469
+ }
470
+ const lines = text.split("\n").filter((line) => line.trim().length > 0);
471
+ if (lines.length === 0) {
472
+ return null;
473
+ }
474
+ const last = lines[lines.length - 1];
475
+ if (last === undefined) {
476
+ return null;
477
+ }
478
+ try {
479
+ const parsed = JSON.parse(last);
480
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
481
+ return parsed;
482
+ }
483
+ }
484
+ catch {
485
+ return null;
486
+ }
487
+ return null;
488
+ }
489
+ /** True when D2 should skip stdout emission (key unchanged within 4h). */
490
+ export function shouldSuppressD2Emission(result, historyPath, options = {}) {
491
+ const prior = readLastHistoryRecord(historyPath);
492
+ if (prior === null) {
493
+ return false;
494
+ }
495
+ const emittedAt = parseHistoryEmittedAt(prior);
496
+ if (emittedAt === null) {
497
+ return false;
498
+ }
499
+ const now = options.now ?? new Date();
500
+ const ageMs = now.getTime() - emittedAt.getTime();
501
+ if (ageMs < 0 || ageMs >= D2_SUPPRESSION_WINDOW_HOURS * 3_600_000) {
502
+ return false;
503
+ }
504
+ const priorKey = suppressionKeyFromRecord(prior);
505
+ if (priorKey === null) {
506
+ return false;
507
+ }
508
+ return priorKey === suppressionKey(result);
509
+ }
336
510
  function sortKeysDeep(value) {
337
511
  if (value === null || typeof value !== "object") {
338
512
  return value;
@@ -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
  }
@@ -16,6 +16,7 @@ export interface DefaultModeOptions {
16
16
  readonly writeHistory?: boolean;
17
17
  readonly taskPrefix?: string | null;
18
18
  readonly selfHealFn?: (projectRoot: string) => void;
19
+ readonly now?: Date;
19
20
  }
20
21
  /** Non-interactive default mode (#1309). */
21
22
  export declare function runDefaultMode(projectRoot: string, options?: DefaultModeOptions): WelcomeOutcome;
@@ -37,6 +37,7 @@ export function runDefaultMode(projectRoot, options = {}) {
37
37
  heal(projectRoot);
38
38
  emitOneliner(projectRoot, {
39
39
  writeHistory: options.writeHistory !== false,
40
+ now: options.now,
40
41
  output: out,
41
42
  });
42
43
  const state = detectPriorState(projectRoot);
@@ -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;
@@ -19,5 +19,7 @@ export declare function appendHistory(historyPath: string, result: SummaryResult
19
19
  export declare function emitOneliner(projectRoot: string, options?: {
20
20
  writeHistory?: boolean;
21
21
  output?: (line: string) => void;
22
+ now?: Date;
23
+ applyD2Suppression?: boolean;
22
24
  }): string;
23
25
  //# sourceMappingURL=summary.d.ts.map