ai-foreman 1.7.24 → 1.7.26

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 (42) hide show
  1. package/README.md +2 -1
  2. package/dist/adapters/claude.d.ts +15 -0
  3. package/dist/adapters/claude.js +149 -28
  4. package/dist/adapters/codex.d.ts +9 -0
  5. package/dist/adapters/codex.js +77 -8
  6. package/dist/adapters/fastMode.d.ts +4 -0
  7. package/dist/adapters/fastMode.js +9 -0
  8. package/dist/adapters/types.d.ts +2 -0
  9. package/dist/agentRun.js +1 -0
  10. package/dist/branch/runner.js +46 -44
  11. package/dist/buildAssignment.d.ts +21 -1
  12. package/dist/buildAssignment.js +215 -2
  13. package/dist/buildRuns.d.ts +6 -0
  14. package/dist/buildRuns.js +73 -7
  15. package/dist/cli/start.d.ts +8 -1
  16. package/dist/cli/start.js +132 -38
  17. package/dist/cli/tickets.d.ts +1 -0
  18. package/dist/cli/tickets.js +20 -2
  19. package/dist/continuity.js +11 -10
  20. package/dist/foreman.d.ts +8 -0
  21. package/dist/foreman.js +69 -10
  22. package/dist/handoffProtocol.d.ts +4 -0
  23. package/dist/handoffProtocol.js +19 -0
  24. package/dist/handoffs.d.ts +1 -1
  25. package/dist/handoffs.js +23 -4
  26. package/dist/permissions/policy.js +18 -11
  27. package/dist/permissions/shellForms.d.ts +3 -0
  28. package/dist/permissions/shellForms.js +203 -0
  29. package/dist/qaBuilderActivation.d.ts +19 -0
  30. package/dist/qaBuilderActivation.js +112 -0
  31. package/dist/qaFailureDelivery.js +1 -1
  32. package/dist/qaRecovery.js +2 -1
  33. package/dist/qaReview.d.ts +4 -0
  34. package/dist/qaReview.js +37 -7
  35. package/dist/sessionLifecycle.js +1 -1
  36. package/dist/supervisedStart.js +23 -3
  37. package/dist/supervisor.js +33 -0
  38. package/dist/tickets/groupReset.js +100 -96
  39. package/dist/tickets/reset.js +80 -69
  40. package/dist/workflowDb.d.ts +16 -1
  41. package/dist/workflowDb.js +276 -8
  42. package/package.json +2 -2
@@ -94,107 +94,111 @@ export function resolveTicketResetSelection(projectDir, requested, options = {},
94
94
  }
95
95
  export function applyResolvedTicketReset(projectDir, selection, actor = "user", now = new Date(), hooks = {}) {
96
96
  const opened = open(projectDir);
97
- const { config, paths, definitions, db } = opened;
98
- const workflow = new WorkflowDb(projectDir);
99
- const resetId = hooks.resetId ?? randomUUID();
100
- const current = new Map(definitions.map((definition) => [definition.id, definition]));
101
- const groupCatalog = db.listTicketGroups();
102
- const snapshots = new Map(groupCatalog.flatMap((group) => group.members.map((member) => [member.ticketId, member.snapshot])));
103
- const states = db.getAllStates();
104
- const selectedIds = new Set(selection.ticketIds);
105
- const currentRelatedRuns = recoverableBuildRuns(projectDir, now)
106
- .filter((run) => run.tickets.some((ticketId) => selectedIds.has(ticketId)))
107
- .map((run) => ({ runId: run.runId, status: run.status, tickets: [...run.tickets] }));
108
- const comparable = { ...selection, relatedRuns: currentRelatedRuns };
109
- delete comparable.inputFingerprint;
110
- const actualFingerprint = selectionFingerprint(comparable, current, states, snapshots, groupCatalog);
111
- if (actualFingerprint !== selection.inputFingerprint) {
112
- db.close();
113
- workflow.close();
114
- throw new Error("ticket/group/reset inputs changed after preview; no reset was applied—preview the exact selection again");
115
- }
116
- const restore = selection.definitionRestorations.filter((item) => item.restore);
117
- const restored = restore.map((item) => item.ticketId);
118
- const restoredSet = new Set(restored);
119
- const restoredDefinitions = restore.map((item) => snapshots.get(item.ticketId)?.definition);
120
- if (restoredDefinitions.some((item) => !item)) {
121
- db.close();
122
- workflow.close();
123
- throw new Error("a selected definition snapshot became unavailable");
124
- }
125
- const nextDefinitions = [...definitions, ...restoredDefinitions.filter((definition) => !current.has(definition.id))].sort((a, b) => a.order - b.order);
126
- const definitionIssues = validateTicketDefs(nextDefinitions);
127
- if (definitionIssues.length) {
128
- db.close();
129
- workflow.close();
130
- throw new Error(`restored ticket definitions are invalid: ${definitionIssues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
131
- }
132
- const resettable = selection.ticketIds.filter((ticketId) => current.has(ticketId) || restoredSet.has(ticketId));
133
- const ignoredMissing = selection.ticketIds.filter((ticketId) => !current.has(ticketId) && !restoredSet.has(ticketId));
134
- if (resettable.length === 0 && restored.length === 0) {
135
- db.close();
136
- workflow.close();
137
- return { resetId, tickets: [], restored: [], ignoredMissing };
138
- }
139
- const stageDir = join(projectDir, ".rafi", "reset-stage", resetId);
140
- const stagedDefinitions = join(stageDir, "tickets.yaml");
141
- const stagedProgress = join(stageDir, "progress.md");
142
- const definitionsBefore = existsSync(paths.tickets) ? readFileSync(paths.tickets) : undefined;
143
- const progressBefore = existsSync(paths.progressDoc) ? readFileSync(paths.progressDoc) : undefined;
144
- const oldStates = new Map([...new Set([...resettable, ...restored])].map((ticketId) => [ticketId, db.getState(ticketId)]));
145
- const oldRecent = db.getRecentCompleted().filter((row) => resettable.includes(row.ticket_id));
146
- const eventIds = [];
147
- const run = workflow.createRun({ kind: "recovery", checkpoint: "group-reset-intent", originalWork: selection, remainingWork: { tickets: resettable }, state: { resetId } }, now);
148
- const publication = workflow.beginPublication(run.runId, { operation: "group-ticket-reset", resetId, selection, stagedDefinitions, stagedProgress, definitionsTarget: paths.tickets, progressTarget: paths.progressDoc }, { definitions: digest(definitionsBefore), progress: digest(progressBefore) }, now);
97
+ const { config, paths, db } = opened;
98
+ let workflowHandle;
149
99
  try {
150
- const stamp = now.toISOString();
151
- db.transaction(() => {
152
- for (const definition of restoredDefinitions)
153
- if (!db.getState(definition.id))
154
- db.upsertState(definition.id, pristineTicketState(definition.id, stamp, actor), stamp);
155
- for (const row of selection.previewRows) {
156
- if (!resettable.includes(row.ticketId)) {
157
- eventIds.push(db.insertEvent({ timestamp: stamp, actor, ticket_id: row.ticketId, event_type: "ticket_reset_skipped_missing", old_status: row.status, new_status: row.status, summary: `Skipped missing definition ${row.ticketId}`, validation: null, evidence: null, payload_json: JSON.stringify({ reset_id: resetId, input_fingerprint: selection.inputFingerprint }) }));
158
- continue;
159
- }
160
- const old = db.getState(row.ticketId)?.status ?? row.status;
161
- db.upsertState(row.ticketId, pristineTicketState(row.ticketId, stamp, actor), stamp);
162
- db.deleteRecentCompleted(row.ticketId);
163
- eventIds.push(db.insertEvent({ timestamp: stamp, actor, ticket_id: row.ticketId, event_type: "ticket_reset", old_status: old, new_status: "planned", summary: `Reset ${row.ticketId} from approved immutable selection`, validation: null, evidence: null, payload_json: JSON.stringify({ reset_id: resetId, groups: selection.groups.map((group) => group.id), restored_definition: restoredSet.has(row.ticketId), input_fingerprint: selection.inputFingerprint }) }));
100
+ const workflow = workflowHandle = new WorkflowDb(projectDir);
101
+ const resetId = hooks.resetId ?? randomUUID();
102
+ return workflow.withTrackerMutationAuthority(() => {
103
+ const definitions = loadTickets(paths.tickets);
104
+ const current = new Map(definitions.map((definition) => [definition.id, definition]));
105
+ const groupCatalog = db.listTicketGroups();
106
+ const snapshots = new Map(groupCatalog.flatMap((group) => group.members.map((member) => [member.ticketId, member.snapshot])));
107
+ const states = db.getAllStates();
108
+ const selectedIds = new Set(selection.ticketIds);
109
+ const currentRelatedRuns = recoverableBuildRuns(projectDir, now)
110
+ .filter((run) => run.tickets.some((ticketId) => selectedIds.has(ticketId)))
111
+ .map((run) => ({ runId: run.runId, status: run.status, tickets: [...run.tickets] }));
112
+ const comparable = { ...selection, relatedRuns: currentRelatedRuns };
113
+ delete comparable.inputFingerprint;
114
+ const actualFingerprint = selectionFingerprint(comparable, current, states, snapshots, groupCatalog);
115
+ if (actualFingerprint !== selection.inputFingerprint) {
116
+ throw new Error("ticket/group/reset inputs changed after preview; no reset was applied—preview the exact selection again");
117
+ }
118
+ const restore = selection.definitionRestorations.filter((item) => item.restore);
119
+ const restored = restore.map((item) => item.ticketId);
120
+ const restoredSet = new Set(restored);
121
+ const restoredDefinitions = restore.map((item) => snapshots.get(item.ticketId)?.definition);
122
+ if (restoredDefinitions.some((item) => !item)) {
123
+ throw new Error("a selected definition snapshot became unavailable");
124
+ }
125
+ const nextDefinitions = [...definitions, ...restoredDefinitions.filter((definition) => !current.has(definition.id))].sort((a, b) => a.order - b.order);
126
+ const definitionIssues = validateTicketDefs(nextDefinitions);
127
+ if (definitionIssues.length) {
128
+ throw new Error(`restored ticket definitions are invalid: ${definitionIssues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
129
+ }
130
+ const resettable = selection.ticketIds.filter((ticketId) => current.has(ticketId) || restoredSet.has(ticketId));
131
+ const ignoredMissing = selection.ticketIds.filter((ticketId) => !current.has(ticketId) && !restoredSet.has(ticketId));
132
+ if (resettable.length === 0 && restored.length === 0) {
133
+ return { resetId, tickets: [], restored: [], ignoredMissing };
134
+ }
135
+ const stageDir = join(projectDir, ".rafi", "reset-stage", resetId);
136
+ const stagedDefinitions = join(stageDir, "tickets.yaml");
137
+ const stagedProgress = join(stageDir, "progress.md");
138
+ const definitionsBefore = existsSync(paths.tickets) ? readFileSync(paths.tickets) : undefined;
139
+ const progressBefore = existsSync(paths.progressDoc) ? readFileSync(paths.progressDoc) : undefined;
140
+ const oldStates = new Map([...new Set([...resettable, ...restored])].map((ticketId) => [ticketId, db.getState(ticketId)]));
141
+ const oldRecent = db.getRecentCompleted().filter((row) => resettable.includes(row.ticket_id));
142
+ const eventIds = [];
143
+ const run = workflow.createRun({ kind: "recovery", checkpoint: "group-reset-intent", originalWork: selection, remainingWork: { tickets: resettable }, state: { resetId } }, now);
144
+ let publication;
145
+ try {
146
+ publication = workflow.beginPublication(run.runId, { operation: "group-ticket-reset", resetId, selection, stagedDefinitions, stagedProgress, definitionsTarget: paths.tickets, progressTarget: paths.progressDoc }, { definitions: digest(definitionsBefore), progress: digest(progressBefore) }, now);
147
+ const stamp = now.toISOString();
148
+ db.transaction(() => {
149
+ for (const definition of restoredDefinitions)
150
+ if (!db.getState(definition.id))
151
+ db.upsertState(definition.id, pristineTicketState(definition.id, stamp, actor), stamp);
152
+ for (const row of selection.previewRows) {
153
+ if (!resettable.includes(row.ticketId)) {
154
+ eventIds.push(db.insertEvent({ timestamp: stamp, actor, ticket_id: row.ticketId, event_type: "ticket_reset_skipped_missing", old_status: row.status, new_status: row.status, summary: `Skipped missing definition ${row.ticketId}`, validation: null, evidence: null, payload_json: JSON.stringify({ reset_id: resetId, input_fingerprint: selection.inputFingerprint }) }));
155
+ continue;
156
+ }
157
+ const old = db.getState(row.ticketId)?.status ?? row.status;
158
+ db.upsertState(row.ticketId, pristineTicketState(row.ticketId, stamp, actor), stamp);
159
+ db.deleteRecentCompleted(row.ticketId);
160
+ eventIds.push(db.insertEvent({ timestamp: stamp, actor, ticket_id: row.ticketId, event_type: "ticket_reset", old_status: old, new_status: "planned", summary: `Reset ${row.ticketId} from approved immutable selection`, validation: null, evidence: null, payload_json: JSON.stringify({ reset_id: resetId, groups: selection.groups.map((group) => group.id), restored_definition: restoredSet.has(row.ticketId), input_fingerprint: selection.inputFingerprint }) }));
161
+ }
162
+ });
163
+ mkdirSync(stageDir, { recursive: true });
164
+ writeFileSync(stagedDefinitions, stringify({ tickets: nextDefinitions }, { lineWidth: 120 }), "utf8");
165
+ writeFileSync(stagedProgress, renderProgressDoc({ config, projectDir, ticketDefs: nextDefinitions, states: db.getAllStates(), db, now: stamp }), "utf8");
166
+ workflow.updatePublication(publication.transactionId, "staged", now);
167
+ workflow.updatePublication(publication.transactionId, "tracker_committed", now);
168
+ hooks.beforePublish?.();
169
+ atomicCopy(stagedDefinitions, paths.tickets);
170
+ writeProgressDoc(paths.progressDoc, readFileSync(stagedProgress, "utf8"), true);
171
+ workflow.updatePublication(publication.transactionId, "published", now);
172
+ workflow.updatePublication(publication.transactionId, "committed", now);
173
+ workflow.transition(run.runId, { status: "completed", checkpoint: "group-reset-committed", remainingWork: {}, state: { resetId, tickets: resettable, restored, ignoredMissing } }, now);
174
+ rmSync(stageDir, { recursive: true, force: true });
175
+ return { resetId, tickets: resettable, restored, ignoredMissing };
176
+ }
177
+ catch (error) {
178
+ db.transaction(() => {
179
+ db.deleteEvents(eventIds);
180
+ for (const [ticketId, state] of oldStates)
181
+ state ? db.upsertState(ticketId, state, state.updated_at) : db.deleteState(ticketId);
182
+ for (const row of oldRecent)
183
+ db.upsertRecentCompleted(row);
184
+ });
185
+ restoreFile(paths.tickets, definitionsBefore);
186
+ restoreFile(paths.progressDoc, progressBefore);
187
+ if (publication)
188
+ workflow.updatePublication(publication.transactionId, "rolled_back", now);
189
+ workflow.transition(run.runId, { status: "failed", checkpoint: "group-reset-rolled-back", state: { resetId, error: error instanceof Error ? error.message : String(error) } }, now);
190
+ rmSync(stageDir, { recursive: true, force: true });
191
+ throw error;
164
192
  }
165
193
  });
166
- mkdirSync(stageDir, { recursive: true });
167
- writeFileSync(stagedDefinitions, stringify({ tickets: nextDefinitions }, { lineWidth: 120 }), "utf8");
168
- writeFileSync(stagedProgress, renderProgressDoc({ config, projectDir, ticketDefs: nextDefinitions, states: db.getAllStates(), db, now: stamp }), "utf8");
169
- workflow.updatePublication(publication.transactionId, "staged", now);
170
- workflow.updatePublication(publication.transactionId, "tracker_committed", now);
171
- hooks.beforePublish?.();
172
- atomicCopy(stagedDefinitions, paths.tickets);
173
- writeProgressDoc(paths.progressDoc, readFileSync(stagedProgress, "utf8"), true);
174
- workflow.updatePublication(publication.transactionId, "published", now);
175
- workflow.updatePublication(publication.transactionId, "committed", now);
176
- workflow.transition(run.runId, { status: "completed", checkpoint: "group-reset-committed", remainingWork: {}, state: { resetId, tickets: resettable, restored, ignoredMissing } }, now);
177
- rmSync(stageDir, { recursive: true, force: true });
178
- return { resetId, tickets: resettable, restored, ignoredMissing };
179
- }
180
- catch (error) {
181
- db.transaction(() => {
182
- db.deleteEvents(eventIds);
183
- for (const [ticketId, state] of oldStates)
184
- state ? db.upsertState(ticketId, state, state.updated_at) : db.deleteState(ticketId);
185
- for (const row of oldRecent)
186
- db.upsertRecentCompleted(row);
187
- });
188
- restoreFile(paths.tickets, definitionsBefore);
189
- restoreFile(paths.progressDoc, progressBefore);
190
- workflow.updatePublication(publication.transactionId, "rolled_back", now);
191
- workflow.transition(run.runId, { status: "failed", checkpoint: "group-reset-rolled-back", state: { resetId, error: error instanceof Error ? error.message : String(error) } }, now);
192
- rmSync(stageDir, { recursive: true, force: true });
193
- throw error;
194
194
  }
195
195
  finally {
196
- db.close();
197
- workflow.close();
196
+ try {
197
+ workflowHandle?.close();
198
+ }
199
+ finally {
200
+ db.close();
201
+ }
198
202
  }
199
203
  }
200
204
  function selectGroups(groups, requested) {
@@ -61,82 +61,93 @@ export function resetTickets(projectDir, target, actor = "user", now = new Date(
61
61
  if (ids.length === 0)
62
62
  return { resetId: randomUUID(), tickets: [] };
63
63
  const { config, paths, tickets, db } = open(projectDir);
64
- const workflow = new WorkflowDb(projectDir);
65
- const resetId = hooks.resetId ?? randomUUID();
66
- if (ids.every((id) => db.getTicketEvents(id).some((event) => event.event_type === "ticket_reset" && event.payload_json?.includes(resetId)))) {
67
- workflow.close();
68
- db.close();
69
- return { resetId, tickets: ids };
70
- }
71
- const stagePath = join(projectDir, ".rafi", "reset-stage", resetId, "progress.md");
72
- const oldStates = new Map(ids.map((id) => [id, db.getState(id)]));
73
- const oldRecentCompleted = db.getRecentCompleted().filter((row) => ids.includes(row.ticket_id));
74
- const insertedEventIds = [];
75
- const progressBefore = existsSync(paths.progressDoc) ? readFileSync(paths.progressDoc) : undefined;
76
- const run = workflow.createRun({
77
- kind: "recovery",
78
- checkpoint: "ticket-reset-intent",
79
- originalWork: { resetId, scope: preview.scope, tickets: ids, states: Object.fromEntries(oldStates) },
80
- remainingWork: { tickets: ids },
81
- state: { operation: "ticket-reset", progressDigest: digest(progressBefore) },
82
- }, now);
83
- const publication = workflow.beginPublication(run.runId, { operation: "ticket-reset", resetId, tickets: ids, stagePath, targetPath: paths.progressDoc }, { progressDigest: digest(progressBefore) }, now);
64
+ let workflowHandle;
84
65
  try {
85
- const stamp = now.toISOString();
86
- db.transaction(() => {
87
- for (const row of preview.tickets) {
88
- db.upsertState(row.id, pristineTicketState(row.id, stamp, actor), stamp);
89
- db.deleteRecentCompleted(row.id);
90
- insertedEventIds.push(db.insertEvent({
91
- timestamp: stamp,
92
- actor,
93
- ticket_id: row.id,
94
- event_type: "ticket_reset",
95
- old_status: row.status,
96
- new_status: "planned",
97
- summary: `Reset ${row.id} to pristine active state`,
98
- validation: null,
99
- evidence: null,
100
- payload_json: JSON.stringify({ reset_id: resetId, scope: preview.scope, cleared: row.cleared }),
101
- }));
66
+ const workflow = workflowHandle = new WorkflowDb(projectDir);
67
+ const resetId = hooks.resetId ?? randomUUID();
68
+ if (ids.every((id) => db.getTicketEvents(id).some((event) => event.event_type === "ticket_reset" && event.payload_json?.includes(resetId)))) {
69
+ return { resetId, tickets: ids };
70
+ }
71
+ return workflow.withTrackerMutationAuthority(() => {
72
+ if (JSON.stringify(loadTickets(paths.tickets)) !== JSON.stringify(tickets))
73
+ throw new Error("Ticket definitions changed before reset authority was acquired; preview again");
74
+ const stagePath = join(projectDir, ".rafi", "reset-stage", resetId, "progress.md");
75
+ const oldStates = new Map(ids.map((id) => [id, db.getState(id)]));
76
+ const oldRecentCompleted = db.getRecentCompleted().filter((row) => ids.includes(row.ticket_id));
77
+ const insertedEventIds = [];
78
+ const progressBefore = existsSync(paths.progressDoc) ? readFileSync(paths.progressDoc) : undefined;
79
+ const run = workflow.createRun({
80
+ kind: "recovery",
81
+ checkpoint: "ticket-reset-intent",
82
+ originalWork: { resetId, scope: preview.scope, tickets: ids, states: Object.fromEntries(oldStates) },
83
+ remainingWork: { tickets: ids },
84
+ state: { operation: "ticket-reset", progressDigest: digest(progressBefore) },
85
+ }, now);
86
+ let publication;
87
+ try {
88
+ publication = workflow.beginPublication(run.runId, { operation: "ticket-reset", resetId, tickets: ids, stagePath, targetPath: paths.progressDoc }, { progressDigest: digest(progressBefore) }, now);
89
+ const stamp = now.toISOString();
90
+ db.transaction(() => {
91
+ for (const row of preview.tickets) {
92
+ db.upsertState(row.id, pristineTicketState(row.id, stamp, actor), stamp);
93
+ db.deleteRecentCompleted(row.id);
94
+ insertedEventIds.push(db.insertEvent({
95
+ timestamp: stamp,
96
+ actor,
97
+ ticket_id: row.id,
98
+ event_type: "ticket_reset",
99
+ old_status: oldStates.get(row.id)?.status ?? row.status,
100
+ new_status: "planned",
101
+ summary: `Reset ${row.id} to pristine active state`,
102
+ validation: null,
103
+ evidence: null,
104
+ payload_json: JSON.stringify({ reset_id: resetId, scope: preview.scope, cleared: row.cleared }),
105
+ }));
106
+ }
107
+ });
108
+ mkdirSync(dirname(stagePath), { recursive: true });
109
+ writeFileSync(stagePath, renderProgressDoc({ config, projectDir, ticketDefs: tickets, states: db.getAllStates(), db, now: stamp }), "utf8");
110
+ workflow.updatePublication(publication.transactionId, "staged", now);
111
+ workflow.updatePublication(publication.transactionId, "tracker_committed", now);
112
+ hooks.beforePublish?.();
113
+ writeProgressDoc(paths.progressDoc, readFileSync(stagePath, "utf8"), true);
114
+ workflow.updatePublication(publication.transactionId, "published", now);
115
+ workflow.updatePublication(publication.transactionId, "committed", now);
116
+ workflow.transition(run.runId, { status: "completed", checkpoint: "ticket-reset-committed", remainingWork: {}, state: { operation: "ticket-reset", resetId, tickets: ids } }, now);
117
+ rmSync(dirname(stagePath), { recursive: true, force: true });
118
+ return { resetId, tickets: ids };
102
119
  }
103
- });
104
- mkdirSync(dirname(stagePath), { recursive: true });
105
- writeFileSync(stagePath, renderProgressDoc({ config, projectDir, ticketDefs: tickets, states: db.getAllStates(), db, now: stamp }), "utf8");
106
- workflow.updatePublication(publication.transactionId, "staged", now);
107
- workflow.updatePublication(publication.transactionId, "tracker_committed", now);
108
- hooks.beforePublish?.();
109
- writeProgressDoc(paths.progressDoc, readFileSync(stagePath, "utf8"), true);
110
- workflow.updatePublication(publication.transactionId, "published", now);
111
- workflow.updatePublication(publication.transactionId, "committed", now);
112
- workflow.transition(run.runId, { status: "completed", checkpoint: "ticket-reset-committed", remainingWork: {}, state: { operation: "ticket-reset", resetId, tickets: ids } }, now);
113
- rmSync(dirname(stagePath), { recursive: true, force: true });
114
- return { resetId, tickets: ids };
115
- }
116
- catch (error) {
117
- db.transaction(() => {
118
- db.deleteEvents(insertedEventIds);
119
- for (const [id, state] of oldStates) {
120
- if (state)
121
- db.upsertState(id, state, state.updated_at);
120
+ catch (error) {
121
+ db.transaction(() => {
122
+ db.deleteEvents(insertedEventIds);
123
+ for (const [id, state] of oldStates) {
124
+ if (state)
125
+ db.upsertState(id, state, state.updated_at);
126
+ else
127
+ db.deleteState(id);
128
+ }
129
+ for (const row of oldRecentCompleted)
130
+ db.upsertRecentCompleted(row);
131
+ });
132
+ if (progressBefore)
133
+ writeProgressDoc(paths.progressDoc, progressBefore.toString("utf8"), true);
122
134
  else
123
- db.deleteState(id);
135
+ rmSync(paths.progressDoc, { force: true });
136
+ rmSync(dirname(stagePath), { recursive: true, force: true });
137
+ if (publication)
138
+ workflow.updatePublication(publication.transactionId, "rolled_back", now);
139
+ workflow.transition(run.runId, { status: "failed", checkpoint: "ticket-reset-rolled-back", state: { operation: "ticket-reset", resetId, error: error instanceof Error ? error.message : String(error) } }, now);
140
+ throw error;
124
141
  }
125
- for (const row of oldRecentCompleted)
126
- db.upsertRecentCompleted(row);
127
142
  });
128
- if (progressBefore)
129
- writeProgressDoc(paths.progressDoc, progressBefore.toString("utf8"), true);
130
- else
131
- rmSync(paths.progressDoc, { force: true });
132
- rmSync(dirname(stagePath), { recursive: true, force: true });
133
- workflow.updatePublication(publication.transactionId, "rolled_back", now);
134
- workflow.transition(run.runId, { status: "failed", checkpoint: "ticket-reset-rolled-back", state: { operation: "ticket-reset", resetId, error: error instanceof Error ? error.message : String(error) } }, now);
135
- throw error;
136
143
  }
137
144
  finally {
138
- workflow.close();
139
- db.close();
145
+ try {
146
+ workflowHandle?.close();
147
+ }
148
+ finally {
149
+ db.close();
150
+ }
140
151
  }
141
152
  }
142
153
  /** Finish or roll back a reset publication left incomplete by process interruption. */
@@ -201,7 +201,6 @@ export interface QaReportRecordV2 {
201
201
  createdAt: string;
202
202
  updatedAt: string;
203
203
  }
204
- /** Authoritative persistence for every resumable workflow in a project. */
205
204
  export declare class WorkflowDb {
206
205
  readonly projectDir: string;
207
206
  private readonly readinessAccess?;
@@ -212,6 +211,7 @@ export declare class WorkflowDb {
212
211
  private coordinatorTransition;
213
212
  private lineageTransition;
214
213
  private workAuthority;
214
+ private readonly createdRecoveryIntents;
215
215
  constructor(projectDir: string, path?: string, readinessAccess?: {
216
216
  probeId: string;
217
217
  } | {
@@ -254,6 +254,9 @@ export declare class WorkflowDb {
254
254
  /** Atomic local changes only; never await provider or filesystem work here. */
255
255
  atomic<T>(work: () => T): T;
256
256
  buildAdmission(): BuildAdmission | undefined;
257
+ /** Short synchronous tracker mutations share the build fence without retiring
258
+ * provider receipts or granting authority to resume implementation. */
259
+ withTrackerMutationAuthority<T>(work: () => T): T;
257
260
  private assertNoLegacyBuildOwner;
258
261
  private assertExecutionProtocol;
259
262
  acquireBuildAdmission(runId: string, phase: BuildAdmission["phase"]): BuildAdmission;
@@ -270,6 +273,10 @@ export declare class WorkflowDb {
270
273
  acknowledgeBuildLaunchChild(authority: BuildAdmission, token: string, pid: number): void;
271
274
  reconcileBuildLaunches(runId: string, original?: BuildAdmission): "retired" | "claimed" | "unknown";
272
275
  buildLaunch(token: string): BuildLaunch | undefined;
276
+ /** A supervisor's waiting exit may represent a successful partial QA boundary.
277
+ * Require this launch's clean worker exit and verified cleanup, never just QA. */
278
+ successfulPartialQaRecovery(runId: string, ticketId: string, launchToken: string): boolean;
279
+ waitForSuccessfulPartialQaRecovery(runId: string, ticketId: string, launchToken: string): Promise<boolean>;
273
280
  preparationSuccessor(runId: string): string;
274
281
  reservePreparationRetry(predecessor: string, args: string[], expectedStartArgs?: string): {
275
282
  runId: string;
@@ -310,6 +317,9 @@ export declare class WorkflowDb {
310
317
  state?: Record<string, unknown>;
311
318
  legacy?: boolean;
312
319
  }, now?: Date): WorkflowRunSnapshot;
320
+ /** Record failure of an intent this connection just created, before recovery
321
+ * authority was acquired. This cannot write build execution or publications. */
322
+ pauseOwnStartOverIntent(runId: string, error: unknown): void;
313
323
  getRun(runId: string): WorkflowRunSnapshot | undefined;
314
324
  /** Publication intents are immutable; mutable tracker snapshots are not approval evidence. */
315
325
  approvedPopulationDefinitions(): unknown[];
@@ -347,6 +357,7 @@ export declare class WorkflowDb {
347
357
  sessionUsageSamples(runId: string, role?: "builder" | "qa"): SessionUsageSample[];
348
358
  acknowledgeSettings(ack: LiveSettingsAcknowledgment): void;
349
359
  settingsAcknowledgments(revision: number): LiveSettingsAcknowledgment[];
360
+ settingsAdoptionState(runId: string, role: "builder" | "qa"): "active" | "deferred" | "unknown";
350
361
  appendContinuityEvent(input: {
351
362
  runId: string;
352
363
  role: "builder" | "qa" | "host";
@@ -494,6 +505,9 @@ export declare class WorkflowDb {
494
505
  recordQaHandoffReceipt(receipt: HandoffAcceptanceReceiptV2): void;
495
506
  qaHandoffReceipt(operationId: string): HandoffAcceptanceReceiptV2 | undefined;
496
507
  beginQaTurn(intent: QaTurnIntentV2): void;
508
+ /** Retire an interrupted, proven read-only review transport, never a QA pass
509
+ * or Builder remediation. A fresh immutable review must still run afterward. */
510
+ reconcileInterruptedQaDispatch(runId: string, ticketId: string): boolean;
497
511
  commitQaTurnIntent(intent: QaTurnIntentV2, expectedRevision: number): QaReducerStateV2;
498
512
  finishQaTurn(receipt: QaTurnReceiptV2): void;
499
513
  recordQaReport(input: Omit<QaReportRecordV2, "reportOccurrenceId" | "disposition" | "createdAt" | "updatedAt">, findingIds: string[], now?: Date): QaReportRecordV2;
@@ -521,6 +535,7 @@ export declare class WorkflowDb {
521
535
  authorizeContractQaWaiver(runId: string, workId: string, decisionId: string): string;
522
536
  commitQaWaiver(runId: string, ticketId: string, expectedRevision: number, reason: string, now?: Date, authorizationRef?: string): QaReducerStateV2;
523
537
  builderRemediationReceipt(operationId: string): BuilderRemediationReceiptV2 | BuilderRemediationReceiptV3 | undefined;
538
+ hasConfirmedQaRemediationForRecheck(runId: string, ticketId: string): boolean;
524
539
  recordBuilderRemediationReceipt(receipt: BuilderRemediationReceiptV2 | BuilderRemediationReceiptV3): void;
525
540
  commitBuilderRemediationSuccess(receipt: BuilderRemediationReceiptV2 | BuilderRemediationReceiptV3, expectedRevision: number, now?: Date): QaReducerStateV2;
526
541
  commitQaRemediationOutcome(input: {