ai-foreman 1.7.6 → 1.7.8

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 (69) hide show
  1. package/README.md +26 -1
  2. package/dist/activity.d.ts +3 -0
  3. package/dist/activity.js +31 -6
  4. package/dist/adapters/claude.d.ts +61 -6
  5. package/dist/adapters/claude.js +544 -36
  6. package/dist/adapters/codex.d.ts +28 -1
  7. package/dist/adapters/codex.js +398 -27
  8. package/dist/adapters/recovering.d.ts +13 -2
  9. package/dist/adapters/recovering.js +32 -4
  10. package/dist/adapters/sessionFailure.d.ts +18 -0
  11. package/dist/adapters/sessionFailure.js +51 -0
  12. package/dist/adapters/types.d.ts +117 -9
  13. package/dist/agentRun.d.ts +4 -1
  14. package/dist/agentRun.js +96 -9
  15. package/dist/branch/currentGuard.d.ts +38 -0
  16. package/dist/branch/currentGuard.js +71 -0
  17. package/dist/branch/planner.d.ts +1 -1
  18. package/dist/branch/planner.js +2 -7
  19. package/dist/branch/prefix.d.ts +8 -0
  20. package/dist/branch/prefix.js +26 -0
  21. package/dist/branch/resume.d.ts +2 -0
  22. package/dist/branch/resume.js +8 -0
  23. package/dist/branch/runner.d.ts +14 -2
  24. package/dist/branch/runner.js +77 -17
  25. package/dist/buildRuns.d.ts +13 -4
  26. package/dist/buildRuns.js +85 -19
  27. package/dist/cli/handoffs.d.ts +2 -0
  28. package/dist/cli/handoffs.js +74 -0
  29. package/dist/cli/start.js +1079 -43
  30. package/dist/cli/tickets.js +199 -3
  31. package/dist/continuity.d.ts +92 -0
  32. package/dist/continuity.js +415 -0
  33. package/dist/foreman.d.ts +5 -1
  34. package/dist/foreman.js +41 -6
  35. package/dist/handoffs.d.ts +69 -0
  36. package/dist/handoffs.js +546 -0
  37. package/dist/log.d.ts +1 -1
  38. package/dist/permissions/policy.d.ts +5 -1
  39. package/dist/permissions/policy.js +48 -1
  40. package/dist/qaReview.d.ts +6 -6
  41. package/dist/qaReview.js +24 -14
  42. package/dist/sessionAvailability.d.ts +15 -0
  43. package/dist/sessionAvailability.js +57 -0
  44. package/dist/sessionIdentity.d.ts +33 -0
  45. package/dist/sessionIdentity.js +132 -0
  46. package/dist/sessionLifecycle.d.ts +198 -5
  47. package/dist/sessionLifecycle.js +1373 -20
  48. package/dist/statusReporter.d.ts +84 -8
  49. package/dist/statusReporter.js +270 -14
  50. package/dist/ticketPlanning.js +8 -0
  51. package/dist/ticketPopulation.js +88 -12
  52. package/dist/tickets/commands.js +20 -12
  53. package/dist/tickets/groupReset.d.ts +25 -0
  54. package/dist/tickets/groupReset.js +279 -0
  55. package/dist/tickets/groups.d.ts +36 -0
  56. package/dist/tickets/groups.js +68 -0
  57. package/dist/tickets/importer.js +27 -3
  58. package/dist/tickets/recommendations.js +31 -7
  59. package/dist/tickets/reset.d.ts +1 -0
  60. package/dist/tickets/reset.js +1 -0
  61. package/dist/tickets/setupConfig.d.ts +1 -0
  62. package/dist/tickets/setupConfig.js +10 -2
  63. package/dist/tickets/stateDb.d.ts +34 -0
  64. package/dist/tickets/stateDb.js +207 -0
  65. package/dist/tickets/ticketLoader.js +5 -2
  66. package/dist/tickets/validate.js +4 -0
  67. package/dist/workflowDb.d.ts +145 -3
  68. package/dist/workflowDb.js +571 -10
  69. package/package.json +32 -4
package/dist/buildRuns.js CHANGED
@@ -1,10 +1,13 @@
1
- import { createHash, randomUUID } from "node:crypto";
1
+ import { randomUUID } from "node:crypto";
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
3
3
  import { hostname } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
5
  import { execFileSync } from "node:child_process";
6
6
  import Database from "better-sqlite3";
7
7
  import { WorkflowDb } from "./workflowDb.js";
8
+ import { canonicalSessionPath, captureWorkspaceIdentity, createProviderSessionRef, latestSessionBinding, upsertSessionBinding } from "./sessionIdentity.js";
9
+ import { resolveProviderSessionAvailability } from "./sessionAvailability.js";
10
+ import { captureCurrentWorkflowSessionIdentity, currentWorkflowIdentityKey } from "./branch/currentGuard.js";
8
11
  import { isTicketsInitialized, loadTicketsConfig, resolveTicketPaths } from "./tickets/config.js";
9
12
  import { loadTickets } from "./tickets/ticketLoader.js";
10
13
  export const BUILD_RUN_VERSION = 2;
@@ -37,6 +40,7 @@ export function createBuildRun(input) {
37
40
  baselineComplete: Boolean(snapshot.baselineHead && snapshot.startHead && snapshot.branch),
38
41
  },
39
42
  progress: { completedTickets: [], completedOperations: [], remainingTickets: [...input.tickets], nextAction: input.tickets[0] ? `Start ${input.tickets[0]}` : "Plan next work" },
43
+ runDecisions: input.runDecisions,
40
44
  receipts: {},
41
45
  lease: currentLease(now),
42
46
  createdAt: stamp,
@@ -65,15 +69,27 @@ export function resumeBuildRun(projectDir, runId, patch, now = new Date()) {
65
69
  finally {
66
70
  workflow.close();
67
71
  }
72
+ const clearBuilderSession = patch.builderSessionId === null && patch.builderSessionRef === null;
73
+ const builderSessionId = clearBuilderSession
74
+ ? undefined
75
+ : patch.builderSessionRef?.sessionId ?? patch.builderSessionId ?? existing.builder?.sessionId;
68
76
  return saveBuildRun(projectDir, {
69
77
  ...existing, status: "running", checkpoint: "recovery-resumed", completedAt: undefined,
70
- builder: patch.builder ? { settings: normalizeCapturedSettings(patch.builder), ...(patch.builderSessionId === null ? {} : { sessionId: patch.builderSessionId ?? existing.builder?.sessionId }) } : existing.builder,
78
+ builder: patch.builder ? { settings: normalizeCapturedSettings(patch.builder), ...(builderSessionId ? { sessionId: builderSessionId } : {}) } : existing.builder,
71
79
  qa: patch.qa ? { settings: normalizeCapturedSettings(patch.qa), sessionId: existing.qa?.sessionId } : existing.qa,
80
+ sessionBindings: patch.builderSessionRef ? upsertSessionBinding(existing.sessionBindings, patch.builderSessionRef) : existing.sessionBindings,
72
81
  lease: currentLease(now),
73
82
  }, now);
74
83
  }
75
84
  function normalizeCapturedSettings(settings) {
76
- return { ...settings, session_strategy: settings.session_strategy ?? (["builder", "qa", "ticket-maker"].includes(settings.role) ? "compact" : "fresh"), settings_revision: settings.settings_revision ?? 0 };
85
+ return {
86
+ ...settings,
87
+ session_strategy: settings.session_strategy ?? (["builder", "qa", "ticket-maker"].includes(settings.role) ? "compact" : "fresh"),
88
+ display_session_cost: settings.display_session_cost ?? false,
89
+ auto_compact_threshold_percent: settings.auto_compact_threshold_percent ?? 50,
90
+ compact_maximum: settings.compact_maximum ?? 10,
91
+ settings_revision: settings.settings_revision ?? 0,
92
+ };
77
93
  }
78
94
  export function saveBuildRun(projectDir, run, now = new Date()) {
79
95
  validateBuildRun(run);
@@ -104,9 +120,9 @@ export function saveBuildRun(projectDir, run, now = new Date()) {
104
120
  remainingWork: { tickets: remainingTickets(next) }, state: next, event: "build_snapshot",
105
121
  }, now);
106
122
  if (next.builder?.sessionId)
107
- workflow.recordSession(next.runId, "builder", "builder", next.builder.sessionId, "checkpoint", next.builder.settings, now);
123
+ workflow.recordSession(next.runId, "builder", "builder", latestSessionBinding(next.sessionBindings, "builder", next.builder.sessionId) ?? next.builder.sessionId, "checkpoint", next.builder.settings, now);
108
124
  if (next.qa?.sessionId)
109
- workflow.recordSession(next.runId, "qa", "qa", next.qa.sessionId, "checkpoint", next.qa.settings, now);
125
+ workflow.recordSession(next.runId, "qa", "qa", latestSessionBinding(next.sessionBindings, "qa", next.qa.sessionId) ?? next.qa.sessionId, "checkpoint", next.qa.settings, now);
110
126
  }
111
127
  finally {
112
128
  workflow.close();
@@ -116,11 +132,40 @@ export function saveBuildRun(projectDir, run, now = new Date()) {
116
132
  export function checkpointBuildRun(projectDir, run, checkpoint, patch = {}) {
117
133
  return saveBuildRun(projectDir, { ...run, ...patch, checkpoint, receipts: patch.receipts ?? run.receipts });
118
134
  }
119
- export function persistBuildSession(projectDir, run, role, sessionId) {
135
+ export function persistBuildSession(projectDir, run, role, session) {
120
136
  const current = run[role];
121
137
  if (!current)
122
138
  throw new Error(`${role} settings were not captured for run ${run.runId}`);
123
- return checkpointBuildRun(projectDir, { ...run, [role]: { ...current, sessionId } }, `${role}-session-ready`);
139
+ const sessionId = typeof session === "string" ? session : session.sessionId;
140
+ const sessionBindings = typeof session === "string" ? run.sessionBindings : upsertSessionBinding(run.sessionBindings, session);
141
+ return checkpointBuildRun(projectDir, { ...run, sessionBindings, [role]: { ...current, sessionId } }, `${role}-session-ready`);
142
+ }
143
+ /** Latest scoped binding, with the narrow legacy Builder inference allowed by the recovery protocol. */
144
+ export function buildRunSessionBinding(run, role, sessionId) {
145
+ const bound = latestSessionBinding(run.sessionBindings, role, sessionId);
146
+ if (bound)
147
+ return bound;
148
+ if (role === "qa")
149
+ return undefined;
150
+ const raw = sessionId ?? run.builder?.sessionId;
151
+ if (!raw || !run.builder || !existsSync(run.repository.worktree))
152
+ return undefined;
153
+ return createProviderSessionRef({
154
+ provider: run.builder.settings.make,
155
+ sessionId: raw,
156
+ role: "builder",
157
+ stream: "builder",
158
+ generation: 0,
159
+ cwd: run.repository.worktree,
160
+ configRoot: run.repository.root,
161
+ workspaceIdentity: run.branchMode === "current" && run.repository.git.branch
162
+ ? currentWorkflowIdentityKey({ worktree: canonicalSessionPath(run.repository.worktree), ref: `branch:${run.repository.git.branch}` })
163
+ : run.repository.git.worktreeIdentity ?? captureWorkspaceIdentity(run.repository.worktree),
164
+ ticketId: run.currentTicket,
165
+ deliveryUnitId: run.deliveryUnit,
166
+ source: "legacy-inferred",
167
+ createdAt: run.createdAt,
168
+ });
124
169
  }
125
170
  export function recordBuildReceipt(projectDir, run, operationId, detail) {
126
171
  if (run.receipts[operationId])
@@ -244,11 +289,11 @@ export function buildRecoveryPreview(run) {
244
289
  `baseline ${run.repository.baselineComplete ? `${run.repository.git.baseRef ?? "recorded"} @ ${run.repository.git.baselineHead}` : "incomplete legacy baseline"}`,
245
290
  `preserved paths ${run.repository.git.runOwnedPaths.join(", ") || run.repository.git.statusPaths.join(", ") || "none recorded"}`,
246
291
  `completed operations ${completed.length ? completed.join(", ") : "none"}`,
247
- `session ${run.builder?.sessionId ? "exact Builder session available" : "fresh Builder session required"}`,
248
- `QA session ${run.qa?.sessionId ? "exact QA session available" : "fresh QA session required"}`,
292
+ `session ${buildRunSessionBinding(run, "builder") ? "Builder session candidate requires validation" : "fresh Builder session required"}`,
293
+ `QA session ${buildRunSessionBinding(run, "qa") ? "QA session candidate requires validation" : "fresh QA session required"}`,
249
294
  ];
250
295
  }
251
- export function projectBuildRecovery(projectDir, run, now = new Date(), ticketOverride) {
296
+ export function projectBuildRecovery(projectDir, run, now = new Date(), ticketOverride, sessionAvailability) {
252
297
  let operationNames = [];
253
298
  let issues = [];
254
299
  let workflowState = {};
@@ -297,6 +342,9 @@ export function projectBuildRecovery(projectDir, run, now = new Date(), ticketOv
297
342
  ? `Resume ${ticketOverride} from ${run.checkpoint}`
298
343
  : run.progress.nextAction ?? (ticketId ? `Resume ${ticketId} from ${run.checkpoint}` : `Resume from ${run.checkpoint}`);
299
344
  const primary = ticketId ? `${ticketId}${ticketTitle ? `: ${ticketTitle}` : ""}` : "legacy run (ticket unavailable)";
345
+ const stateSessionId = typeof workflowState.sessionId === "string" ? workflowState.sessionId : undefined;
346
+ const sessionCandidateRef = buildRunSessionBinding(run, "builder", stateSessionId ?? run.builder?.sessionId);
347
+ const exactSessionRef = sessionAvailability?.status === "available" ? sessionAvailability.sessionRef ?? sessionCandidateRef : undefined;
300
348
  return {
301
349
  run, ticketId, ticketTitle,
302
350
  compactLabel: `${primary} — ${run.status}`,
@@ -310,9 +358,33 @@ export function projectBuildRecovery(projectDir, run, now = new Date(), ticketOv
310
358
  nextAction,
311
359
  worktree: recoveryWorktree,
312
360
  branch: stateBranch ?? branchNow ?? run.repository.git.branch ?? run.repository.branch,
313
- exactSessionId: typeof workflowState.sessionId === "string" ? workflowState.sessionId : run.builder?.sessionId,
361
+ ...(sessionCandidateRef ? { sessionCandidateRef } : {}),
362
+ ...(sessionAvailability ? { sessionAvailability } : {}),
363
+ ...(exactSessionRef ? { exactSessionRef, exactSessionId: exactSessionRef.sessionId } : {}),
314
364
  };
315
365
  }
366
+ export async function resolveBuildRecoveryProjection(projectDir, run, now = new Date(), ticketOverride, probe = {}) {
367
+ const frozen = projectBuildRecovery(projectDir, run, now, ticketOverride);
368
+ const candidate = frozen.sessionCandidateRef;
369
+ if (!candidate)
370
+ return frozen;
371
+ const availability = await resolveProviderSessionAvailability(candidate, {
372
+ ...probe,
373
+ cwd: frozen.worktree,
374
+ configRoot: run.repository.root,
375
+ workspaceIdentity: run.branchMode === "current" ? safeCurrentIdentity(frozen.worktree) : captureWorkspaceIdentity(frozen.worktree),
376
+ now,
377
+ });
378
+ return projectBuildRecovery(projectDir, run, now, ticketOverride, availability);
379
+ }
380
+ function safeCurrentIdentity(worktree) {
381
+ try {
382
+ return captureCurrentWorkflowSessionIdentity(worktree);
383
+ }
384
+ catch {
385
+ return undefined;
386
+ }
387
+ }
316
388
  export function formatBuildRecoveryProjection(projection) {
317
389
  const { run } = projection;
318
390
  const lines = [
@@ -327,7 +399,7 @@ export function formatBuildRecoveryProjection(projection) {
327
399
  `validation/QA ${projection.validation}`,
328
400
  `branch ${projection.branch ?? "current"}; worktree ${projection.worktree}`,
329
401
  `baseline ${run.repository.baselineComplete ? `${run.repository.git.baseRef ?? "recorded"} @ ${run.repository.git.baselineHead}` : "incomplete; automatic rollback is unavailable"}`,
330
- `session ${projection.exactSessionId ? "exact Builder session available" : "fresh Builder session required"}`,
402
+ `session ${projection.exactSessionId ? "exact Builder session available (validated)" : projection.sessionCandidateRef ? `exact Builder session unavailable (${projection.sessionAvailability?.reason ?? "validation pending"})` : "fresh Builder session required"}`,
331
403
  `runtime ${run.builder?.settings.make ?? "unavailable"}; model ${run.builder?.settings.model ?? "unavailable"}`,
332
404
  `updated ${run.updatedAt}`,
333
405
  `preserved expected in-progress paths ${projection.expectedChanges.join(", ") || "none currently dirty"}`,
@@ -402,13 +474,7 @@ function captureGitSnapshot(worktree, input) {
402
474
  };
403
475
  }
404
476
  function worktreeIdentity(worktree) {
405
- try {
406
- const stat = statSync(resolve(worktree));
407
- return createHash("sha256").update(`${stat.dev}:${stat.ino}`).digest("hex");
408
- }
409
- catch {
410
- return undefined;
411
- }
477
+ return captureWorkspaceIdentity(worktree);
412
478
  }
413
479
  function gitValue(cwd, args) {
414
480
  try {
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function buildHandoffsCommand(): Command;
@@ -0,0 +1,74 @@
1
+ import { Command } from "commander";
2
+ import { resolve } from "node:path";
3
+ import { recoverableBuildRuns } from "../buildRuns.js";
4
+ import { HandoffService, writeHandoffInspection } from "../handoffs.js";
5
+ import { WorkflowDb } from "../workflowDb.js";
6
+ export function buildHandoffsCommand() {
7
+ const command = new Command("handoffs").description("Inspect and explicitly manage validated cumulative handoffs.");
8
+ command.command("inspect")
9
+ .requiredOption("--run <id>", "build run ID")
10
+ .option("--generation <n>", "handoff generation")
11
+ .option("--format <format>", "markdown or json", "markdown")
12
+ .option("--output <path>", "write inspection to a file")
13
+ .option("--project <dir>", "project directory", ".")
14
+ .action((opts) => {
15
+ const format = String(opts.format);
16
+ if (!["markdown", "json"].includes(format))
17
+ throw new Error("--format must be markdown or json");
18
+ const generation = opts.generation === undefined ? undefined : positiveInteger(opts.generation, "--generation");
19
+ const service = new HandoffService(resolve(String(opts.project)));
20
+ const value = service.inspect(String(opts.run), generation);
21
+ const content = format === "json"
22
+ ? `${JSON.stringify({ version: 1, lineage: value.lineage, manifest: value.manifest }, null, 2)}\n`
23
+ : value.markdown;
24
+ if (opts.output) {
25
+ writeHandoffInspection(String(opts.output), content);
26
+ console.log(`rafi handoffs: wrote ${format} inspection to ${resolve(String(opts.output))}`);
27
+ }
28
+ else
29
+ process.stdout.write(content.endsWith("\n") ? content : `${content}\n`);
30
+ });
31
+ command.command("prune-cache")
32
+ .requiredOption("--run <id>", "build run ID")
33
+ .option("--keep-latest <n>", "number of newest cache generations to keep", "1")
34
+ .option("--project <dir>", "project directory", ".")
35
+ .action((opts) => {
36
+ const keep = nonNegativeInteger(opts.keepLatest, "--keep-latest");
37
+ const removed = new HandoffService(resolve(String(opts.project))).pruneCache(String(opts.run), keep);
38
+ console.log(`rafi handoffs: pruned ${removed.length} disposable cache generation(s); durable history was preserved`);
39
+ });
40
+ command.command("delete-history")
41
+ .requiredOption("--run <id>", "build run ID")
42
+ .requiredOption("--yes", "confirm irreversible durable history deletion")
43
+ .option("--project <dir>", "project directory", ".")
44
+ .action((opts) => {
45
+ const projectDir = resolve(String(opts.project));
46
+ const runId = String(opts.run);
47
+ const recoverable = recoverableBuildRuns(projectDir).find((run) => run.runId === runId);
48
+ if (recoverable)
49
+ throw new Error(`run ${runId} is active or recoverable; finish or explicitly clean up the run before deleting durable handoff history`);
50
+ const db = new WorkflowDb(projectDir);
51
+ try {
52
+ const deleted = db.deleteHandoffHistory(runId);
53
+ console.log(`rafi handoffs: deleted ${deleted} durable handoff generation(s) for run ${runId}`);
54
+ console.log(`rafi handoffs: disposable copies remain until pruned; run: rafi handoffs prune-cache --run ${shellQuote(runId)} --keep-latest 1`);
55
+ }
56
+ finally {
57
+ db.close();
58
+ }
59
+ });
60
+ return command;
61
+ }
62
+ function positiveInteger(value, label) {
63
+ const result = Number(value);
64
+ if (!Number.isSafeInteger(result) || result < 1)
65
+ throw new Error(`${label} must be a positive safe integer`);
66
+ return result;
67
+ }
68
+ function nonNegativeInteger(value, label) {
69
+ const result = Number(value);
70
+ if (!Number.isSafeInteger(result) || result < 0)
71
+ throw new Error(`${label} must be a non-negative safe integer`);
72
+ return result;
73
+ }
74
+ function shellQuote(value) { return `'${value.replace(/'/g, `'\\''`)}'`; }