@deftai/directive-core 0.55.1 → 0.56.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 (74) hide show
  1. package/dist/cache/errors.d.ts +4 -1
  2. package/dist/cache/errors.js +8 -2
  3. package/dist/cache/fetch.d.ts +50 -0
  4. package/dist/cache/fetch.js +224 -3
  5. package/dist/cache/main.js +10 -5
  6. package/dist/cache/operations.js +6 -4
  7. package/dist/codebase/map-fresh.js +7 -1
  8. package/dist/deposit/copy-tree.d.ts +16 -0
  9. package/dist/deposit/copy-tree.js +58 -0
  10. package/dist/deposit/resolve-content.d.ts +28 -0
  11. package/dist/deposit/resolve-content.js +70 -0
  12. package/dist/doctor/checks.d.ts +6 -0
  13. package/dist/doctor/checks.js +37 -0
  14. package/dist/doctor/constants.d.ts +2 -0
  15. package/dist/doctor/constants.js +8 -0
  16. package/dist/engine-version.d.ts +3 -0
  17. package/dist/engine-version.js +23 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +3 -1
  20. package/dist/init-deposit/gitignore.d.ts +51 -0
  21. package/dist/init-deposit/gitignore.js +200 -0
  22. package/dist/init-deposit/index.d.ts +6 -0
  23. package/dist/init-deposit/index.js +6 -0
  24. package/dist/init-deposit/init-deposit.d.ts +44 -0
  25. package/dist/init-deposit/init-deposit.js +197 -0
  26. package/dist/init-deposit/legacy-detect.d.ts +61 -0
  27. package/dist/init-deposit/legacy-detect.js +223 -0
  28. package/dist/init-deposit/migrate.d.ts +88 -0
  29. package/dist/init-deposit/migrate.js +196 -0
  30. package/dist/init-deposit/refresh.d.ts +50 -0
  31. package/dist/init-deposit/refresh.js +292 -0
  32. package/dist/init-deposit/scaffold.d.ts +41 -0
  33. package/dist/init-deposit/scaffold.js +753 -0
  34. package/dist/legacy-bridge/bridge-drift.d.ts +70 -0
  35. package/dist/legacy-bridge/bridge-drift.js +171 -0
  36. package/dist/legacy-bridge/freeze-gate.d.ts +79 -0
  37. package/dist/legacy-bridge/freeze-gate.js +168 -0
  38. package/dist/legacy-bridge/index.d.ts +4 -0
  39. package/dist/legacy-bridge/index.js +4 -0
  40. package/dist/legacy-bridge/sot.d.ts +45 -0
  41. package/dist/legacy-bridge/sot.js +49 -0
  42. package/dist/preflight-cache/evaluate.d.ts +11 -0
  43. package/dist/preflight-cache/evaluate.js +63 -13
  44. package/dist/release-e2e/constants.d.ts +2 -0
  45. package/dist/release-e2e/constants.js +2 -0
  46. package/dist/release-e2e/flags.js +5 -1
  47. package/dist/release-e2e/legacy-bridge-leg.d.ts +134 -0
  48. package/dist/release-e2e/legacy-bridge-leg.js +460 -0
  49. package/dist/release-e2e/main.d.ts +2 -1
  50. package/dist/release-e2e/main.js +17 -1
  51. package/dist/release-e2e/npm-ops.d.ts +3 -1
  52. package/dist/release-e2e/npm-ops.js +5 -3
  53. package/dist/release-e2e/types.d.ts +9 -0
  54. package/dist/render/index.d.ts +1 -1
  55. package/dist/render/index.js +1 -1
  56. package/dist/render/project-render.js +4 -3
  57. package/dist/render/roadmap-render.d.ts +4 -2
  58. package/dist/render/roadmap-render.js +14 -7
  59. package/dist/swarm/routing-verify.js +2 -2
  60. package/dist/swarm/subagent-backend.d.ts +16 -0
  61. package/dist/swarm/subagent-backend.js +14 -0
  62. package/dist/triage/actions/candidates-log.d.ts +12 -1
  63. package/dist/triage/actions/candidates-log.js +44 -7
  64. package/dist/triage/actions/index.d.ts +25 -1
  65. package/dist/triage/actions/index.js +94 -3
  66. package/dist/triage/summary/index.d.ts +1 -3
  67. package/dist/triage/summary/index.js +6 -57
  68. package/dist/triage/summary/reconcilable.js +4 -33
  69. package/dist/triage/welcome/default-mode.d.ts +1 -0
  70. package/dist/triage/welcome/default-mode.js +6 -0
  71. package/dist/vbrief-validate/project-definition.js +4 -69
  72. package/dist/vbrief-validate/registry-status.d.ts +20 -0
  73. package/dist/vbrief-validate/registry-status.js +85 -0
  74. package/package.json +11 -2
@@ -241,10 +241,13 @@ function renderScopeItem(vbriefData) {
241
241
  parts.push(`\`[${status}]\``);
242
242
  return [`- ${parts.join(" -- ")}`];
243
243
  }
244
- /** Generate ROADMAP.md content (mirrors ``scripts/roadmap_render.generate_roadmap_content``). */
245
- export function generateRoadmapContent(pendingDir, completedDir) {
244
+ function resolveCompletedDir(pendingDir, completedDir) {
245
+ return completedDir ?? join(dirname(pendingDir), "completed");
246
+ }
247
+ /** Single render-to-buffer entry used by both write and --check (mirrors ``scripts/roadmap_render.generate_roadmap_content``). */
248
+ export function renderRoadmapToBuffer(pendingDir, completedDir) {
246
249
  const vbriefs = loadVbriefs(pendingDir);
247
- const resolvedCompleted = completedDir ?? join(dirname(pendingDir), "completed");
250
+ const resolvedCompleted = resolveCompletedDir(pendingDir, completedDir);
248
251
  const completedVbriefs = loadVbriefs(resolvedCompleted);
249
252
  const lines = [ROADMAP_BANNER, "# Roadmap\n"];
250
253
  if (vbriefs.length === 0 && completedVbriefs.length === 0) {
@@ -343,9 +346,13 @@ export function generateRoadmapContent(pendingDir, completedDir) {
343
346
  }
344
347
  return `${lines.join("\n")}\n`;
345
348
  }
349
+ /** @deprecated Prefer ``renderRoadmapToBuffer`` — kept for existing imports and parity harnesses. */
350
+ export function generateRoadmapContent(pendingDir, completedDir) {
351
+ return renderRoadmapToBuffer(pendingDir, completedDir);
352
+ }
346
353
  export function renderRoadmap(pendingDir, outPath, completedDir) {
347
354
  try {
348
- const content = generateRoadmapContent(pendingDir, completedDir);
355
+ const content = renderRoadmapToBuffer(pendingDir, completedDir);
349
356
  writeFileSync(outPath, content, "utf8");
350
357
  return [true, `✓ Rendered ROADMAP.md to ${outPath}`];
351
358
  }
@@ -353,11 +360,11 @@ export function renderRoadmap(pendingDir, outPath, completedDir) {
353
360
  return [false, `✗ Failed to write ${outPath}: ${String(exc)}`];
354
361
  }
355
362
  }
356
- export function checkDrift(pendingDir, roadmapPath) {
357
- const expected = generateRoadmapContent(pendingDir);
363
+ export function checkDrift(pendingDir, roadmapPath, completedDir) {
364
+ const expected = renderRoadmapToBuffer(pendingDir, completedDir);
358
365
  if (!existsSync(roadmapPath)) {
359
366
  const hasPending = existsSync(pendingDir) && readdirSync(pendingDir).some((n) => n.endsWith(".vbrief.json"));
360
- const inferredCompleted = join(dirname(pendingDir), "completed");
367
+ const inferredCompleted = resolveCompletedDir(pendingDir, completedDir);
361
368
  const hasCompleted = existsSync(inferredCompleted) &&
362
369
  readdirSync(inferredCompleted).some((n) => n.endsWith(".vbrief.json"));
363
370
  if (!hasPending && !hasCompleted) {
@@ -73,7 +73,7 @@ export function verifyRouting(options) {
73
73
  if (undecided.length === 0 && invalid.length === 0) {
74
74
  return {
75
75
  exitCode: EXIT_OK,
76
- report: `[deft routing] provider '${provider}': all ${roles.length} gated role(s) decided.`,
76
+ report: `[deft routing] provider '${provider}': all ${roles.length} gated role(s) decided.\n[deft routing] NOTE: plan.policy.swarmSubagentBackend enum is deprecated (#1891); use 'task swarm:routing-set' / .deft/routing.local.json instead.`,
77
77
  };
78
78
  }
79
79
  const parts = [];
@@ -85,7 +85,7 @@ export function verifyRouting(options) {
85
85
  }
86
86
  return {
87
87
  exitCode: EXIT_OK,
88
- report: `[deft routing] provider '${provider}' -- ${parts.join("; ")}. Decide before swarm dispatch: ${ROUTING_SET_CMD}`,
88
+ report: `[deft routing] provider '${provider}' -- ${parts.join("; ")}. Decide before swarm dispatch: ${ROUTING_SET_CMD}\n[deft routing] NOTE: plan.policy.swarmSubagentBackend enum is deprecated (#1891); use 'task swarm:routing-set' / .deft/routing.local.json instead.`,
89
89
  };
90
90
  }
91
91
  if (undecided.length > 0) {
@@ -1,11 +1,27 @@
1
+ /**
2
+ * @deprecated This module is superseded by per-role operator model routing
3
+ * (`.deft/routing.local.json`) introduced in #1739 / #1863.
4
+ *
5
+ * Use `task swarm:routing-set` and `task verify:routing` instead.
6
+ * See `packages/core/src/swarm/routing.ts` for the current implementation.
7
+ *
8
+ * The enum and associated helpers remain functional for consumers that have not
9
+ * yet migrated; they will be removed in a future major cleanup tracked by #1860.
10
+ *
11
+ * @see {@link https://github.com/deftai/directive/issues/1739} Superseding PR
12
+ * @see {@link https://github.com/deftai/directive/issues/1860} Hard deletion tracking
13
+ */
1
14
  import { LEAF_CODING_WORKER_ROLE } from "./constants.js";
15
+ /** @deprecated Superseded by `.deft/routing.local.json` routing (#1739). Use `task swarm:routing-set`. */
2
16
  export declare const KNOWN_SUBAGENT_BACKEND_IDS: Set<string>;
17
+ /** @deprecated Superseded by `.deft/routing.local.json` routing (#1739). Use `task swarm:routing-set`. */
3
18
  export interface SubagentBackendDescriptor {
4
19
  readonly backend_id: string;
5
20
  readonly display_name: string;
6
21
  readonly roles: readonly string[];
7
22
  readonly available: boolean;
8
23
  }
24
+ /** @deprecated Superseded by `.deft/routing.local.json` routing (#1739). Use `task swarm:routing-set`. */
9
25
  export interface SwarmSubagentBackendResult {
10
26
  readonly backend_id: string | null;
11
27
  readonly source: string;
@@ -1,6 +1,20 @@
1
+ /**
2
+ * @deprecated This module is superseded by per-role operator model routing
3
+ * (`.deft/routing.local.json`) introduced in #1739 / #1863.
4
+ *
5
+ * Use `task swarm:routing-set` and `task verify:routing` instead.
6
+ * See `packages/core/src/swarm/routing.ts` for the current implementation.
7
+ *
8
+ * The enum and associated helpers remain functional for consumers that have not
9
+ * yet migrated; they will be removed in a future major cleanup tracked by #1860.
10
+ *
11
+ * @see {@link https://github.com/deftai/directive/issues/1739} Superseding PR
12
+ * @see {@link https://github.com/deftai/directive/issues/1860} Hard deletion tracking
13
+ */
1
14
  import { loadProjectDefinition } from "../policy/resolve.js";
2
15
  import { LEAF_CODING_WORKER_ROLE, SUBAGENT_BACKEND_SET_CMD } from "./constants.js";
3
16
  const TRUTHY = new Set(["1", "true", "yes", "on"]);
17
+ /** @deprecated Superseded by `.deft/routing.local.json` routing (#1739). Use `task swarm:routing-set`. */
4
18
  export const KNOWN_SUBAGENT_BACKEND_IDS = new Set(["composer", "grok-build", "cursor-cloud"]);
5
19
  const SUBAGENT_BACKEND_CATALOG = {
6
20
  composer: {
@@ -1,5 +1,16 @@
1
- import type { CandidatesLog } from "./types.js";
1
+ import type { AuditEntry, CandidatesLog } from "./types.js";
2
2
  export declare const AUDIT_LOG_REL_PATH = "vbrief/.eval/candidates.jsonl";
3
+ /** Read every well-formed audit-log row in insertion order (#1698 shared reader). */
4
+ export declare function readAuditLog(logPath: string, repo?: string | null): AuditEntry[];
5
+ /** Return every entry for ``(repo, issue_number)`` in insertion order. */
6
+ export declare function findByIssue(issueNumber: number, repo: string, logPath: string): AuditEntry[];
7
+ /**
8
+ * Canonical latest decision for ``(repo, issue_number)`` — no actor filter.
9
+ * Backfilled ``agent:bootstrap`` / ``agent:reconcile`` entries count equally.
10
+ */
11
+ export declare function latestDecisionForIssue(issueNumber: number, repo: string, logPath: string): AuditEntry | null;
12
+ /** Collapse audit-log entries to ``{(repo, issue_number): decision}`` by timestamp. */
13
+ export declare function latestDecisions(entries: Iterable<AuditEntry | Record<string, unknown>>): Map<string, string>;
3
14
  /** Filesystem-backed candidates audit log (mirrors ``scripts/candidates_log.py``). */
4
15
  export declare function createCandidatesLog(defaultProjectRoot: string): CandidatesLog;
5
16
  /** Remove the audit-log line whose ``decision_id`` matches (rollback path). */
@@ -113,7 +113,8 @@ function stableStringify(entry) {
113
113
  }
114
114
  return JSON.stringify(sorted);
115
115
  }
116
- function readAll(repo, logPath) {
116
+ /** Read every well-formed audit-log row in insertion order (#1698 shared reader). */
117
+ export function readAuditLog(logPath, repo = null) {
117
118
  if (!existsSync(logPath)) {
118
119
  return [];
119
120
  }
@@ -132,7 +133,47 @@ function readAll(repo, logPath) {
132
133
  continue;
133
134
  out.push(row);
134
135
  }
135
- catch { }
136
+ catch {
137
+ // tolerate malformed lines — mirrors candidates_log.read_all
138
+ }
139
+ }
140
+ return out;
141
+ }
142
+ /** Return every entry for ``(repo, issue_number)`` in insertion order. */
143
+ export function findByIssue(issueNumber, repo, logPath) {
144
+ return readAuditLog(logPath, repo).filter((row) => row.issue_number === issueNumber);
145
+ }
146
+ /**
147
+ * Canonical latest decision for ``(repo, issue_number)`` — no actor filter.
148
+ * Backfilled ``agent:bootstrap`` / ``agent:reconcile`` entries count equally.
149
+ */
150
+ export function latestDecisionForIssue(issueNumber, repo, logPath) {
151
+ const rows = findByIssue(issueNumber, repo, logPath);
152
+ if (rows.length === 0)
153
+ return null;
154
+ rows.sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
155
+ return rows.at(-1) ?? null;
156
+ }
157
+ /** Collapse audit-log entries to ``{(repo, issue_number): decision}`` by timestamp. */
158
+ export function latestDecisions(entries) {
159
+ const rows = [];
160
+ for (const entry of entries) {
161
+ const repo = entry.repo;
162
+ const issueNumber = entry.issue_number;
163
+ const decision = entry.decision;
164
+ const timestamp = entry.timestamp;
165
+ if (typeof repo === "string" &&
166
+ typeof issueNumber === "number" &&
167
+ Number.isInteger(issueNumber) &&
168
+ typeof decision === "string" &&
169
+ typeof timestamp === "string") {
170
+ rows.push([timestamp, repo, issueNumber, decision]);
171
+ }
172
+ }
173
+ rows.sort((a, b) => a[0].localeCompare(b[0]));
174
+ const out = new Map();
175
+ for (const [_ts, repo, n, decision] of rows) {
176
+ out.set(`${repo}\0${n}`, decision);
136
177
  }
137
178
  return out;
138
179
  }
@@ -148,11 +189,7 @@ export function createCandidatesLog(defaultProjectRoot) {
148
189
  },
149
190
  latestDecision(issueNumber, repo, options) {
150
191
  const logPath = resolveLogPath(defaultProjectRoot, options?.path);
151
- const rows = readAll(repo, logPath).filter((row) => row.issue_number === issueNumber);
152
- if (rows.length === 0)
153
- return null;
154
- rows.sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
155
- return rows.at(-1) ?? null;
192
+ return latestDecisionForIssue(issueNumber, repo, logPath);
156
193
  },
157
194
  newDecisionId() {
158
195
  return randomUUID();
@@ -1,5 +1,5 @@
1
1
  import type { AcceptOptions, AuditEntry, DeferOptions, RejectOptions, TriageActionsDeps } from "./types.js";
2
- export { AUDIT_LOG_REL_PATH, createCandidatesLog, resolveAuditLogPath, rollbackAuditEntry, } from "./candidates-log.js";
2
+ export { AUDIT_LOG_REL_PATH, createCandidatesLog, findByIssue, latestDecisionForIssue, latestDecisions, readAuditLog, resolveAuditLogPath, rollbackAuditEntry, } from "./candidates-log.js";
3
3
  export { CandidatesLogError, ResumeGrammarError, TriageError, UpstreamCloseError, } from "./errors.js";
4
4
  export { parseResumeOn } from "./resume-on.js";
5
5
  export type { AcceptOptions, AuditEntry, CandidatesLog, DeferOptions, IssueIngest, RejectOptions, ScmRunner, TriageActionsDeps, } from "./types.js";
@@ -8,12 +8,36 @@ export declare const REJECTED_LABEL_COLOR = "B60205";
8
8
  export declare const REJECTED_LABEL_DESCRIPTION = "Issue rejected during deft triage";
9
9
  /** Default dependency bundle for production CLI use. */
10
10
  export declare function createDefaultDeps(projectRoot: string): TriageActionsDeps;
11
+ /** Return audit entries for ``issueNumber`` ordered ascending by timestamp. */
12
+ export declare function history(issueNumber: number, repo: string, _deps: TriageActionsDeps, options?: {
13
+ projectRoot?: string;
14
+ }): AuditEntry[];
11
15
  /** Record an accept audit entry and delegate vBRIEF authoring to issue_ingest. */
12
16
  export declare function accept(issueNumber: number, repo: string, deps: TriageActionsDeps, options?: AcceptOptions): string;
13
17
  /** Close upstream, best-effort label, record reject audit entry. */
14
18
  export declare function reject(issueNumber: number, repo: string, reason: string, deps: TriageActionsDeps, options?: RejectOptions): string;
15
19
  /** Record a defer audit entry (#1123 -- structured reason + resume_on). */
16
20
  export declare function deferAction(issueNumber: number, repo: string, reason: string | null | undefined, deps: TriageActionsDeps, options?: DeferOptions): string;
21
+ /** Record a needs-ac audit entry and best-effort post an AC-request comment upstream. */
22
+ export declare function needsAc(issueNumber: number, repo: string, deps: TriageActionsDeps, options?: {
23
+ actor?: string | null;
24
+ comment?: string | null;
25
+ projectRoot?: string;
26
+ }): string;
27
+ /** Validate duplicate target in cache and record a mark-duplicate audit entry. */
28
+ export declare function markDuplicate(issueNumber: number, repo: string, ofN: number, deps: TriageActionsDeps, options?: {
29
+ actor?: string | null;
30
+ projectRoot?: string;
31
+ }): string;
32
+ /** Return the latest decision for ``issueNumber`` in ``repo`` (null if none). */
33
+ export declare function status(issueNumber: number, repo: string, deps: TriageActionsDeps, options?: {
34
+ projectRoot?: string;
35
+ }): AuditEntry | null;
36
+ /** Record a reset audit entry referencing the prior decision_id (history is never deleted). */
37
+ export declare function reset(issueNumber: number, repo: string, deps: TriageActionsDeps, options?: {
38
+ actor?: string | null;
39
+ projectRoot?: string;
40
+ }): string;
17
41
  /** Format a decision entry for CLI ``status`` / ``history`` output. */
18
42
  export declare function formatDecision(entry: AuditEntry | null): string;
19
43
  //# sourceMappingURL=index.d.ts.map
@@ -1,12 +1,14 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { CacheNotFoundError } from "../../cache/errors.js";
5
+ import { cacheGet } from "../../cache/operations.js";
4
6
  import { call } from "../../scm/call.js";
5
7
  import { ScmStubError } from "../../scm/errors.js";
6
- import { createCandidatesLog, resolveAuditLogPath, rollbackAuditEntry } from "./candidates-log.js";
8
+ import { createCandidatesLog, findByIssue, resolveAuditLogPath, rollbackAuditEntry, } from "./candidates-log.js";
7
9
  import { TriageError, UpstreamCloseError } from "./errors.js";
8
10
  import { parseResumeOn } from "./resume-on.js";
9
- export { AUDIT_LOG_REL_PATH, createCandidatesLog, resolveAuditLogPath, rollbackAuditEntry, } from "./candidates-log.js";
11
+ export { AUDIT_LOG_REL_PATH, createCandidatesLog, findByIssue, latestDecisionForIssue, latestDecisions, readAuditLog, resolveAuditLogPath, rollbackAuditEntry, } from "./candidates-log.js";
10
12
  export { CandidatesLogError, ResumeGrammarError, TriageError, UpstreamCloseError, } from "./errors.js";
11
13
  export { parseResumeOn } from "./resume-on.js";
12
14
  export const REJECTED_LABEL = "triage-rejected";
@@ -14,6 +16,8 @@ export const REJECTED_LABEL_COLOR = "B60205";
14
16
  export const REJECTED_LABEL_DESCRIPTION = "Issue rejected during deft triage";
15
17
  const TERMINAL_DECISIONS = new Set(["accept", "reject", "mark-duplicate"]);
16
18
  const DEFAULT_ACTOR = "agent:triage";
19
+ const DEFAULT_NEEDS_AC_COMMENT = "This issue lacks acceptance criteria. Please add a Test/Acceptance " +
20
+ "narrative before this can be triaged. (deft #845)";
17
21
  function resolveDeftRoot() {
18
22
  if (process.env.DEFT_ROOT !== undefined && process.env.DEFT_ROOT.length > 0) {
19
23
  return resolve(process.env.DEFT_ROOT);
@@ -88,13 +92,24 @@ function defaultIssueIngest(deftRoot) {
88
92
  /** Default dependency bundle for production CLI use. */
89
93
  export function createDefaultDeps(projectRoot) {
90
94
  const deftRoot = resolveDeftRoot();
91
- return {
95
+ const deps = {
92
96
  candidatesLog: createCandidatesLog(projectRoot),
93
97
  issueIngest: defaultIssueIngest(deftRoot),
94
98
  scm: defaultScmRunner(),
95
99
  nowIso: defaultNowIso,
96
100
  stderr: (message) => process.stderr.write(`${message}\n`),
97
101
  };
102
+ if (process.env.DEFT_TRIAGE_ACTIONS_PARITY === "1") {
103
+ return {
104
+ ...deps,
105
+ scm: {
106
+ call() {
107
+ throw new UpstreamCloseError("gh disabled for parity");
108
+ },
109
+ },
110
+ };
111
+ }
112
+ return deps;
98
113
  }
99
114
  function buildEntry(deps, decision, issueNumber, repo, actor, extras = {}) {
100
115
  const entry = {
@@ -111,6 +126,16 @@ function buildEntry(deps, decision, issueNumber, repo, actor, extras = {}) {
111
126
  function logPathFor(projectRoot) {
112
127
  return resolveAuditLogPath(projectRoot);
113
128
  }
129
+ function cacheRootFor(projectRoot) {
130
+ return join(projectRoot, ".deft-cache");
131
+ }
132
+ /** Return audit entries for ``issueNumber`` ordered ascending by timestamp. */
133
+ export function history(issueNumber, repo, _deps, options = {}) {
134
+ const projectRoot = options.projectRoot ?? process.cwd();
135
+ const entries = findByIssue(issueNumber, repo, logPathFor(projectRoot));
136
+ entries.sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
137
+ return entries;
138
+ }
114
139
  function isIdempotentRepeat(deps, issueNumber, repo, decision, projectRoot, linkedTo) {
115
140
  if (!TERMINAL_DECISIONS.has(decision))
116
141
  return null;
@@ -277,6 +302,72 @@ export function deferAction(issueNumber, repo, reason, deps, options = {}) {
277
302
  const logPath = logPathFor(projectRoot);
278
303
  return deps.candidatesLog.append(entry, { path: logPath });
279
304
  }
305
+ /** Record a needs-ac audit entry and best-effort post an AC-request comment upstream. */
306
+ export function needsAc(issueNumber, repo, deps, options = {}) {
307
+ const projectRoot = options.projectRoot ?? process.cwd();
308
+ const body = options.comment ?? DEFAULT_NEEDS_AC_COMMENT;
309
+ const actor = resolveActor(options.actor);
310
+ const entry = buildEntry(deps, "needs-ac", issueNumber, repo, actor, { reason: body });
311
+ const logPath = logPathFor(projectRoot);
312
+ const decisionId = deps.candidatesLog.append(entry, { path: logPath });
313
+ const writeErr = deps.stderr ?? ((message) => process.stderr.write(`${message}\n`));
314
+ try {
315
+ runGh(deps, ["issue", "comment", String(issueNumber), "--repo", repo, "--body", body]);
316
+ }
317
+ catch (exc) {
318
+ writeErr(`triage_actions: needs-ac comment not posted for #${issueNumber} (${repo}): ${exc instanceof Error ? exc.message : String(exc)}`);
319
+ }
320
+ return decisionId;
321
+ }
322
+ /** Validate duplicate target in cache and record a mark-duplicate audit entry. */
323
+ export function markDuplicate(issueNumber, repo, ofN, deps, options = {}) {
324
+ const projectRoot = options.projectRoot ?? process.cwd();
325
+ if (ofN === issueNumber) {
326
+ throw new TriageError(`mark-duplicate target #${ofN} cannot equal source #${issueNumber}`);
327
+ }
328
+ const key = `${repo}/${ofN}`;
329
+ try {
330
+ cacheGet("github-issue", key, { allowStale: true, cacheRoot: cacheRootFor(projectRoot) });
331
+ }
332
+ catch (exc) {
333
+ if (exc instanceof CacheNotFoundError) {
334
+ throw new TriageError(`mark-duplicate target #${ofN} not found in cache for ${repo}: ${exc.message}`);
335
+ }
336
+ throw exc;
337
+ }
338
+ const prior = isIdempotentRepeat(deps, issueNumber, repo, "mark-duplicate", projectRoot, ofN);
339
+ if (prior !== null) {
340
+ return prior.decision_id;
341
+ }
342
+ const actor = resolveActor(options.actor);
343
+ const entry = buildEntry(deps, "mark-duplicate", issueNumber, repo, actor, { linked_to: ofN });
344
+ const logPath = logPathFor(projectRoot);
345
+ return deps.candidatesLog.append(entry, { path: logPath });
346
+ }
347
+ /** Return the latest decision for ``issueNumber`` in ``repo`` (null if none). */
348
+ export function status(issueNumber, repo, deps, options = {}) {
349
+ const projectRoot = options.projectRoot ?? process.cwd();
350
+ return deps.candidatesLog.latestDecision(issueNumber, repo, {
351
+ path: logPathFor(projectRoot),
352
+ });
353
+ }
354
+ /** Record a reset audit entry referencing the prior decision_id (history is never deleted). */
355
+ export function reset(issueNumber, repo, deps, options = {}) {
356
+ const projectRoot = options.projectRoot ?? process.cwd();
357
+ const logPath = logPathFor(projectRoot);
358
+ const prior = deps.candidatesLog.latestDecision(issueNumber, repo, { path: logPath });
359
+ if (prior === null) {
360
+ throw new TriageError(`cannot reset #${issueNumber}: no prior decision recorded for ${repo}`);
361
+ }
362
+ if (prior.decision === "reset") {
363
+ return prior.decision_id;
364
+ }
365
+ const actor = resolveActor(options.actor);
366
+ const entry = buildEntry(deps, "reset", issueNumber, repo, actor, {
367
+ prior_decision_id: prior.decision_id,
368
+ });
369
+ return deps.candidatesLog.append(entry, { path: logPath });
370
+ }
280
371
  /** Format a decision entry for CLI ``status`` / ``history`` output. */
281
372
  export function formatDecision(entry) {
282
373
  if (entry === null) {
@@ -5,6 +5,7 @@ export { DEFAULT_WIP_CAP, PROJECT_DEFINITION_REL_PATH };
5
5
  export declare const CACHE_DIR_NAME = ".deft-cache";
6
6
  export declare const CACHE_SOURCE = "github-issue";
7
7
  export declare const CANDIDATES_LOG_REL_PATH = "vbrief/.eval/candidates.jsonl";
8
+ export { latestDecisions, readAuditLog } from "../actions/candidates-log.js";
8
9
  export declare const SUMMARY_HISTORY_REL_PATH = "vbrief/.eval/summary-history.jsonl";
9
10
  export declare const SUMMARY_HISTORY_SCHEMA = "deft.triage.summary.v1";
10
11
  export declare const EMPTY_CACHE_LINE = "[triage] cache empty -- run task triage:bootstrap";
@@ -38,9 +39,6 @@ export declare function utcIso(date?: Date): string;
38
39
  export declare function isPosIntDirName(name: string): boolean;
39
40
  /** Walk `<cache_root>/github-issue/<owner>/<repo>/<N>/` cache entries. */
40
41
  export declare function iterCachedIssues(cacheRoot: string): Array<[string, number]>;
41
- export declare function readAuditLog(logPath: string): Record<string, unknown>[];
42
- /** Collapse audit-log entries to `{(repo, issue_number): decision}`. */
43
- export declare function latestDecisions(entries: Iterable<Record<string, unknown>>): Map<string, string>;
44
42
  /** Count filesystem-truth in-flight vBRIEFs (#1270). */
45
43
  export declare function countFilesystemInFlight(projectRoot: string): number;
46
44
  /** True iff `plan.policy.triageScope` is a non-empty list of dict rules. */
@@ -2,6 +2,7 @@ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from
2
2
  import { join, resolve as pathResolve } from "node:path";
3
3
  import { loadProjectDefinition, PROJECT_DEFINITION_REL_PATH } from "../../policy/resolve.js";
4
4
  import { countVbriefWip, DEFAULT_WIP_CAP, resolveWipCap } from "../../policy/wip.js";
5
+ import { AUDIT_LOG_REL_PATH, latestDecisions, readAuditLog } from "../actions/candidates-log.js";
5
6
  import { countReconcilable } from "./reconcilable.js";
6
7
  import { computeScopeDriftTotal } from "./scope-drift.js";
7
8
  // ---------------------------------------------------------------------------
@@ -11,7 +12,8 @@ export const MAX_LINE_CHARS = 120;
11
12
  export { DEFAULT_WIP_CAP, PROJECT_DEFINITION_REL_PATH };
12
13
  export const CACHE_DIR_NAME = ".deft-cache";
13
14
  export const CACHE_SOURCE = "github-issue";
14
- export const CANDIDATES_LOG_REL_PATH = "vbrief/.eval/candidates.jsonl";
15
+ export const CANDIDATES_LOG_REL_PATH = AUDIT_LOG_REL_PATH;
16
+ export { latestDecisions, readAuditLog } from "../actions/candidates-log.js";
15
17
  export const SUMMARY_HISTORY_REL_PATH = "vbrief/.eval/summary-history.jsonl";
16
18
  export const SUMMARY_HISTORY_SCHEMA = "deft.triage.summary.v1";
17
19
  export const EMPTY_CACHE_LINE = "[triage] cache empty -- run task triage:bootstrap";
@@ -83,62 +85,6 @@ export function iterCachedIssues(cacheRoot) {
83
85
  }
84
86
  return out;
85
87
  }
86
- export function readAuditLog(logPath) {
87
- if (!existsSync(logPath)) {
88
- return [];
89
- }
90
- let text;
91
- try {
92
- text = readFileSync(logPath, { encoding: "utf8" });
93
- }
94
- catch {
95
- return [];
96
- }
97
- const out = [];
98
- for (const raw of text.split("\n")) {
99
- const stripped = raw.trim();
100
- if (stripped.length === 0) {
101
- continue;
102
- }
103
- try {
104
- const obj = JSON.parse(stripped);
105
- if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) {
106
- out.push(obj);
107
- }
108
- }
109
- catch {
110
- // tolerate malformed lines
111
- }
112
- }
113
- return out;
114
- }
115
- /** Collapse audit-log entries to `{(repo, issue_number): decision}`. */
116
- export function latestDecisions(entries) {
117
- const rows = [];
118
- for (const entry of entries) {
119
- const repo = entry.repo;
120
- const issueNumber = entry.issue_number;
121
- const decision = entry.decision;
122
- const timestamp = entry.timestamp;
123
- if (typeof repo !== "string" ||
124
- typeof issueNumber !== "number" ||
125
- !Number.isInteger(issueNumber) ||
126
- typeof decision !== "string" ||
127
- typeof timestamp !== "string") {
128
- continue;
129
- }
130
- rows.push([timestamp, repo, issueNumber, decision]);
131
- }
132
- rows.sort((a, b) => a[0].localeCompare(b[0]));
133
- const out = new Map();
134
- for (const [_ts, repo, n, decision] of rows) {
135
- out.set(`${repo}\0${n}`, decision);
136
- }
137
- return out;
138
- }
139
- function decisionKey(repo, issueNumber) {
140
- return `${repo}\0${issueNumber}`;
141
- }
142
88
  // ---------------------------------------------------------------------------
143
89
  // vBRIEF counters
144
90
  // ---------------------------------------------------------------------------
@@ -206,6 +152,9 @@ export function isTriageScopeExplicitlyConfigured(projectRoot) {
206
152
  export function resolveWipCapInt(projectRoot) {
207
153
  return resolveWipCap(projectRoot).cap;
208
154
  }
155
+ function decisionKey(repo, issueNumber) {
156
+ return `${repo}\0${issueNumber}`;
157
+ }
209
158
  // ---------------------------------------------------------------------------
210
159
  // compute / format / persist
211
160
  // ---------------------------------------------------------------------------
@@ -1,8 +1,9 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { AUDIT_LOG_REL_PATH, readAuditLog } from "../actions/candidates-log.js";
3
4
  /** Lifecycle folders scanned for reconcilable vBRIEFs (#1468). */
4
5
  export const BACKFILL_FOLDERS = ["proposed", "pending", "active"];
5
- export const CANDIDATES_LOG_REL_PATH = "vbrief/.eval/candidates.jsonl";
6
+ export const CANDIDATES_LOG_REL_PATH = AUDIT_LOG_REL_PATH;
6
7
  function parseGithubIssueUri(uri) {
7
8
  if (typeof uri !== "string" || uri.length === 0) {
8
9
  return [null, null];
@@ -47,38 +48,8 @@ function extractIssueRef(data) {
47
48
  }
48
49
  function existingAuditRefs(auditPath) {
49
50
  const seen = new Set();
50
- if (!existsSync(auditPath)) {
51
- return seen;
52
- }
53
- let text;
54
- try {
55
- text = readFileSync(auditPath, { encoding: "utf8" });
56
- }
57
- catch {
58
- return seen;
59
- }
60
- for (const raw of text.split("\n")) {
61
- const stripped = raw.trim();
62
- if (stripped.length === 0) {
63
- continue;
64
- }
65
- try {
66
- const entry = JSON.parse(stripped);
67
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
68
- continue;
69
- }
70
- const obj = entry;
71
- const repo = obj.repo;
72
- const issueNumber = obj.issue_number;
73
- if (typeof repo === "string" &&
74
- typeof issueNumber === "number" &&
75
- Number.isInteger(issueNumber)) {
76
- seen.add(`${repo}\0${issueNumber}`);
77
- }
78
- }
79
- catch {
80
- // tolerate malformed lines
81
- }
51
+ for (const entry of readAuditLog(auditPath)) {
52
+ seen.add(auditKey(entry.repo, entry.issue_number));
82
53
  }
83
54
  return seen;
84
55
  }
@@ -15,6 +15,7 @@ export interface DefaultModeOptions {
15
15
  readonly output?: (line: string) => void;
16
16
  readonly writeHistory?: boolean;
17
17
  readonly taskPrefix?: string | null;
18
+ readonly selfHealFn?: (projectRoot: string) => void;
18
19
  }
19
20
  /** Non-interactive default mode (#1309). */
20
21
  export declare function runDefaultMode(projectRoot: string, options?: DefaultModeOptions): WelcomeOutcome;
@@ -1,4 +1,5 @@
1
1
  import { resolve } from "node:path";
2
+ import { maybeSelfHealCache } from "../../cache/fetch.js";
2
3
  import { FIRST_TIME_NUDGE, INCOMPLETE_NUDGE_TEMPLATE } from "./constants.js";
3
4
  import { classifyOnboarding, detectPriorState } from "./prior-state.js";
4
5
  import { emitOneliner } from "./summary.js";
@@ -29,6 +30,11 @@ export function runDefaultMode(projectRoot, options = {}) {
29
30
  exitCode: 0,
30
31
  bootstrapAction: null,
31
32
  };
33
+ const heal = options.selfHealFn ??
34
+ ((root) => {
35
+ maybeSelfHealCache(resolve(root));
36
+ });
37
+ heal(projectRoot);
32
38
  emitOneliner(projectRoot, {
33
39
  writeHistory: options.writeHistory !== false,
34
40
  output: out,