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.
- package/README.md +2 -1
- package/dist/adapters/claude.d.ts +15 -0
- package/dist/adapters/claude.js +149 -28
- package/dist/adapters/codex.d.ts +9 -0
- package/dist/adapters/codex.js +77 -8
- package/dist/adapters/fastMode.d.ts +4 -0
- package/dist/adapters/fastMode.js +9 -0
- package/dist/adapters/types.d.ts +2 -0
- package/dist/agentRun.js +1 -0
- package/dist/branch/runner.js +46 -44
- package/dist/buildAssignment.d.ts +21 -1
- package/dist/buildAssignment.js +215 -2
- package/dist/buildRuns.d.ts +6 -0
- package/dist/buildRuns.js +73 -7
- package/dist/cli/start.d.ts +8 -1
- package/dist/cli/start.js +132 -38
- package/dist/cli/tickets.d.ts +1 -0
- package/dist/cli/tickets.js +20 -2
- package/dist/continuity.js +11 -10
- package/dist/foreman.d.ts +8 -0
- package/dist/foreman.js +69 -10
- package/dist/handoffProtocol.d.ts +4 -0
- package/dist/handoffProtocol.js +19 -0
- package/dist/handoffs.d.ts +1 -1
- package/dist/handoffs.js +23 -4
- package/dist/permissions/policy.js +18 -11
- package/dist/permissions/shellForms.d.ts +3 -0
- package/dist/permissions/shellForms.js +203 -0
- package/dist/qaBuilderActivation.d.ts +19 -0
- package/dist/qaBuilderActivation.js +112 -0
- package/dist/qaFailureDelivery.js +1 -1
- package/dist/qaRecovery.js +2 -1
- package/dist/qaReview.d.ts +4 -0
- package/dist/qaReview.js +37 -7
- package/dist/sessionLifecycle.js +1 -1
- package/dist/supervisedStart.js +23 -3
- package/dist/supervisor.js +33 -0
- package/dist/tickets/groupReset.js +100 -96
- package/dist/tickets/reset.js +80 -69
- package/dist/workflowDb.d.ts +16 -1
- package/dist/workflowDb.js +276 -8
- 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,
|
|
98
|
-
|
|
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
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
197
|
-
|
|
196
|
+
try {
|
|
197
|
+
workflowHandle?.close();
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
db.close();
|
|
201
|
+
}
|
|
198
202
|
}
|
|
199
203
|
}
|
|
200
204
|
function selectGroups(groups, requested) {
|
package/dist/tickets/reset.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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. */
|
package/dist/workflowDb.d.ts
CHANGED
|
@@ -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: {
|