@mingchuno/agent-workflows 0.2.0 → 0.4.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.
- package/README.md +31 -15
- package/dist/src/attribution.d.ts +9 -0
- package/dist/src/attribution.js +57 -0
- package/dist/src/cli-config.d.ts +2 -0
- package/dist/src/cli-config.js +35 -0
- package/dist/src/cli.js +50 -31
- package/dist/src/config.d.ts +2 -8
- package/dist/src/config.js +1 -1
- package/dist/src/domain.d.ts +7 -0
- package/dist/src/invocation.d.ts +4 -3
- package/dist/src/invocation.js +6 -3
- package/dist/src/operations.d.ts +1 -0
- package/dist/src/operations.js +25 -3
- package/dist/src/runner.d.ts +2 -0
- package/dist/src/runner.js +28 -3
- package/dist/src/tui/data.d.ts +2 -1
- package/dist/src/tui/data.js +7 -2
- package/dist/src/tui/index.d.ts +1 -0
- package/dist/src/tui/index.js +1 -0
- package/dist/src/tui/layout.d.ts +1 -1
- package/dist/src/tui/layout.js +2 -2
- package/dist/src/tui/monitor.d.ts +3 -1
- package/dist/src/tui/monitor.js +93 -31
- package/dist/src/tui/notifications.d.ts +23 -0
- package/dist/src/tui/notifications.js +104 -0
- package/dist/src/tui/views.d.ts +10 -2
- package/dist/src/tui/views.js +272 -42
- package/dist/src/workspace.js +21 -8
- package/docs/api.md +25 -10
- package/docs/configuration.md +78 -27
- package/docs/database.md +2 -18
- package/docs/operations.md +54 -22
- package/docs/providers.md +2 -2
- package/examples/config.ts +4 -4
- package/examples/run.ts +4 -1
- package/package.json +1 -1
- package/docs/architecture.md +0 -41
package/dist/src/tui/views.js
CHANGED
|
@@ -1,7 +1,167 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import stringWidth from "string-width";
|
|
3
4
|
import { recoveryUnavailable } from "../recovery.js";
|
|
4
5
|
import { cells, colorFor, duration, elapsedRun, executionDuration, wrapLines, } from "./format.js";
|
|
6
|
+
// The detail panel contributes four columns of border and padding.
|
|
7
|
+
const detailWideBreakpoint = 136;
|
|
8
|
+
const detailLabelWidth = 19;
|
|
9
|
+
const hangingLabelMarker = "\u{e000}";
|
|
10
|
+
const conciseErrorWidth = 120;
|
|
11
|
+
const detailColumnGap = " │ ";
|
|
12
|
+
function localTimestamp(value) {
|
|
13
|
+
if (!value)
|
|
14
|
+
return "not recorded";
|
|
15
|
+
const date = new Date(value);
|
|
16
|
+
if (!Number.isFinite(date.valueOf()))
|
|
17
|
+
return "unavailable";
|
|
18
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
19
|
+
dateStyle: "medium",
|
|
20
|
+
timeStyle: "medium",
|
|
21
|
+
}).format(date);
|
|
22
|
+
}
|
|
23
|
+
function profileValue(value, key) {
|
|
24
|
+
if (!value || typeof value !== "object")
|
|
25
|
+
return undefined;
|
|
26
|
+
const candidate = value[key];
|
|
27
|
+
return typeof candidate === "string" && candidate ? candidate : undefined;
|
|
28
|
+
}
|
|
29
|
+
function readableProfile(value, fallbackProvider) {
|
|
30
|
+
return [
|
|
31
|
+
profileValue(value, "provider") ??
|
|
32
|
+
fallbackProvider ??
|
|
33
|
+
"provider unavailable",
|
|
34
|
+
profileValue(value, "model") ?? "model unavailable",
|
|
35
|
+
profileValue(value, "reasoningEffort") ?? "reasoning unavailable",
|
|
36
|
+
].join(" / ");
|
|
37
|
+
}
|
|
38
|
+
function sameProfile(requested, effective, provider) {
|
|
39
|
+
return (readableProfile(requested, provider) ===
|
|
40
|
+
readableProfile(effective, provider));
|
|
41
|
+
}
|
|
42
|
+
function json(value) {
|
|
43
|
+
return JSON.stringify(value) ?? "unavailable";
|
|
44
|
+
}
|
|
45
|
+
function conciseError(run) {
|
|
46
|
+
const error = run.error ?? run.executions?.at(-1)?.error;
|
|
47
|
+
if (!error)
|
|
48
|
+
return run.outcome === "cancelled"
|
|
49
|
+
? "Run was cancelled."
|
|
50
|
+
: run.outcome === "blocked"
|
|
51
|
+
? "Run is blocked."
|
|
52
|
+
: "Run requires operator attention.";
|
|
53
|
+
const firstLine = error.split("\n", 1)[0].replace(/^Error:\s*/i, "");
|
|
54
|
+
const summary = cells(firstLine, conciseErrorWidth);
|
|
55
|
+
return cells(firstLine, conciseErrorWidth + 1) === summary
|
|
56
|
+
? summary
|
|
57
|
+
: `${cells(firstLine, conciseErrorWidth - 1)}…`;
|
|
58
|
+
}
|
|
59
|
+
function label(label, value) {
|
|
60
|
+
return `${hangingLabelMarker}${label.padEnd(detailLabelWidth)}${value}`;
|
|
61
|
+
}
|
|
62
|
+
export function wrapDetailLines(lines, width) {
|
|
63
|
+
return lines.flatMap((line) => {
|
|
64
|
+
if (!line.startsWith(hangingLabelMarker))
|
|
65
|
+
return wrapLines([line], width);
|
|
66
|
+
const content = line.slice(hangingLabelMarker.length);
|
|
67
|
+
const prefix = content.slice(0, detailLabelWidth);
|
|
68
|
+
const value = content.slice(detailLabelWidth);
|
|
69
|
+
const wrappedValue = wrapLines([value], Math.max(1, width - detailLabelWidth));
|
|
70
|
+
return wrappedValue.map((part, index) => index === 0
|
|
71
|
+
? `${prefix}${part}`
|
|
72
|
+
: `${" ".repeat(detailLabelWidth)}${part}`);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function validationLines(run, now) {
|
|
76
|
+
return [
|
|
77
|
+
"VALIDATION",
|
|
78
|
+
...(run.validation?.length
|
|
79
|
+
? run.validation.flatMap((check) => [
|
|
80
|
+
`${check.command} ${check.args.join(" ")} · exit ${check.exitCode} · ${duration(check.startedAt, check.finishedAt, now)}`,
|
|
81
|
+
])
|
|
82
|
+
: ["not recorded"]),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
function validationSummary(run) {
|
|
86
|
+
const checks = run.validation ?? [];
|
|
87
|
+
if (!checks.length)
|
|
88
|
+
return "not recorded";
|
|
89
|
+
const successful = checks.filter((check) => check.exitCode === 0).length;
|
|
90
|
+
const failed = checks.length - successful;
|
|
91
|
+
return [
|
|
92
|
+
`${checks.length} recorded`,
|
|
93
|
+
successful ? `${successful} exit 0` : "",
|
|
94
|
+
failed ? `${failed} nonzero` : "",
|
|
95
|
+
]
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.join(" · ");
|
|
98
|
+
}
|
|
99
|
+
function sessionLines(sessions, now) {
|
|
100
|
+
return [
|
|
101
|
+
"AGENT SESSIONS",
|
|
102
|
+
...(sessions.length
|
|
103
|
+
? sessions.flatMap((session) => {
|
|
104
|
+
const effective = readableProfile(session.effective, session.provider);
|
|
105
|
+
const profile = sameProfile(session.requested, session.effective, session.provider)
|
|
106
|
+
? `Effective ${effective}`
|
|
107
|
+
: `Requested ${readableProfile(session.requested, session.provider)} → Effective ${effective}`;
|
|
108
|
+
return [
|
|
109
|
+
`${session.step} · invocation ${session.attempt} · ${session.outcome} · ${duration(session.startedAt, session.finishedAt, now)}`,
|
|
110
|
+
profile,
|
|
111
|
+
];
|
|
112
|
+
})
|
|
113
|
+
: ["not recorded"]),
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
function executionLines(run, now) {
|
|
117
|
+
const executions = [...(run.executions ?? [])].reverse();
|
|
118
|
+
if (!executions.length)
|
|
119
|
+
return ["CURRENT EXECUTION", "not started"];
|
|
120
|
+
return executions.flatMap((execution, index) => {
|
|
121
|
+
const executionTime = execution.startedAt
|
|
122
|
+
? executionDuration(execution, now).replace("—", "unavailable")
|
|
123
|
+
: "not started";
|
|
124
|
+
const queueWait = duration(execution.createdAt, execution.startedAt ?? execution.finishedAt, now);
|
|
125
|
+
const recovery = execution.recoveryOf
|
|
126
|
+
? [
|
|
127
|
+
label("Recovery", "continued from prior execution"),
|
|
128
|
+
label("Recovery gap", duration(run.executions?.find((candidate) => candidate.id === execution.recoveryOf)?.finishedAt, execution.createdAt, now).replace("—", "unavailable")),
|
|
129
|
+
label("Reused steps", execution.reusedSteps?.length
|
|
130
|
+
? execution.reusedSteps.join(", ")
|
|
131
|
+
: "none recorded"),
|
|
132
|
+
]
|
|
133
|
+
: [];
|
|
134
|
+
const lines = index === 0
|
|
135
|
+
? [
|
|
136
|
+
"CURRENT EXECUTION",
|
|
137
|
+
label("Outcome", execution.outcome),
|
|
138
|
+
label("Phase", execution.phase),
|
|
139
|
+
label("Execution duration", executionTime),
|
|
140
|
+
label("Queue wait", queueWait),
|
|
141
|
+
label("Created", localTimestamp(execution.createdAt)),
|
|
142
|
+
label("Started", localTimestamp(execution.startedAt)),
|
|
143
|
+
label("Finished", localTimestamp(execution.finishedAt)),
|
|
144
|
+
...recovery,
|
|
145
|
+
]
|
|
146
|
+
: [
|
|
147
|
+
`PRIOR EXECUTION ${executions.length - index}`,
|
|
148
|
+
`${execution.outcome} · ${execution.phase} · duration ${executionTime} · queue ${queueWait}`,
|
|
149
|
+
`${localTimestamp(execution.startedAt)} → ${localTimestamp(execution.finishedAt)}`,
|
|
150
|
+
...recovery,
|
|
151
|
+
];
|
|
152
|
+
return index < executions.length - 1 ? [...lines, ""] : lines;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function padToWidth(value, width) {
|
|
156
|
+
return `${value}${" ".repeat(Math.max(0, width - stringWidth(value)))}`;
|
|
157
|
+
}
|
|
158
|
+
function composeDetailColumns(left, right, width) {
|
|
159
|
+
const leftWidth = Math.floor((width - detailColumnGap.length) * 0.58);
|
|
160
|
+
const rightWidth = width - leftWidth - detailColumnGap.length;
|
|
161
|
+
const leftLines = wrapDetailLines(left, leftWidth);
|
|
162
|
+
const rightLines = wrapDetailLines(right, rightWidth);
|
|
163
|
+
return Array.from({ length: Math.max(leftLines.length, rightLines.length) }, (_, index) => `${padToWidth(leftLines[index] ?? "", leftWidth)}${detailColumnGap}${rightLines[index] ?? ""}`);
|
|
164
|
+
}
|
|
5
165
|
function stepText(event) {
|
|
6
166
|
if (!event)
|
|
7
167
|
return "No steps recorded";
|
|
@@ -38,56 +198,126 @@ export function summaryLines(run, now, event) {
|
|
|
38
198
|
...(run.change ? [`Change request: ${run.change.url}`] : []),
|
|
39
199
|
];
|
|
40
200
|
}
|
|
41
|
-
export function detailLines(run, sessions, now, recoveryReason = recoveryUnavailable(run)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
201
|
+
export function detailLines(run, sessions, now, recoveryReason = recoveryUnavailable(run), width = 76, available = {
|
|
202
|
+
stop: false,
|
|
203
|
+
retry: false,
|
|
204
|
+
recover: false,
|
|
205
|
+
}) {
|
|
206
|
+
const validation = validationSummary(run);
|
|
207
|
+
const recovery = run.outcome === "failed"
|
|
208
|
+
? recoveryReason
|
|
209
|
+
? "restricted"
|
|
210
|
+
: "eligible"
|
|
211
|
+
: "not applicable";
|
|
212
|
+
const attention = ["failed", "blocked", "cancelled"].includes(run.outcome)
|
|
213
|
+
? [
|
|
214
|
+
"",
|
|
215
|
+
"ATTENTION",
|
|
216
|
+
label("Problem", conciseError(run)),
|
|
217
|
+
label("Recovery", recoveryReason ?? "Eligible; runner checks still apply"),
|
|
218
|
+
label("Action", [
|
|
219
|
+
available.recover ? "c recover" : "",
|
|
220
|
+
available.retry ? "r retry" : "",
|
|
221
|
+
available.stop ? "s stop" : "",
|
|
222
|
+
]
|
|
223
|
+
.filter(Boolean)
|
|
224
|
+
.join(" · ") || "none available"),
|
|
225
|
+
]
|
|
226
|
+
: [];
|
|
227
|
+
const executions = executionLines(run, now);
|
|
228
|
+
const evidence = [
|
|
229
|
+
...validationLines(run, now),
|
|
230
|
+
"",
|
|
231
|
+
...sessionLines(sessions, now),
|
|
232
|
+
];
|
|
233
|
+
const evidenceArea = width >= detailWideBreakpoint
|
|
234
|
+
? composeDetailColumns(executions, evidence, width)
|
|
235
|
+
: [...executions, "", ...evidence];
|
|
236
|
+
const executionsByRecency = [...(run.executions ?? [])].reverse();
|
|
237
|
+
const executionErrors = executionsByRecency.filter((execution) => execution.error);
|
|
238
|
+
const diagnostics = run.error || executionErrors.length
|
|
239
|
+
? [
|
|
240
|
+
"",
|
|
241
|
+
"DIAGNOSTICS",
|
|
242
|
+
...(run.error ? [label("Run error", run.error)] : []),
|
|
243
|
+
...executionErrors.map((execution, index) => label(index === 0 ? "Current error" : "Prior error", execution.error)),
|
|
244
|
+
]
|
|
245
|
+
: [];
|
|
246
|
+
const technical = [
|
|
247
|
+
"",
|
|
248
|
+
"TECHNICAL DETAILS",
|
|
249
|
+
label("Run ID", run.id),
|
|
250
|
+
label("Issue URL", run.issue.url || "unavailable"),
|
|
251
|
+
label("Checkout", run.checkout || "unavailable"),
|
|
252
|
+
label("Task key", run.taskKey),
|
|
253
|
+
label("Created ISO", run.createdAt),
|
|
254
|
+
label("Updated ISO", run.updatedAt),
|
|
255
|
+
...(run.retryOf ? [label("Retry of", run.retryOf)] : []),
|
|
256
|
+
...(run.base ? [label("Base revision", run.base)] : []),
|
|
257
|
+
...(run.head ? [label("Head revision", run.head)] : []),
|
|
258
|
+
...(run.change ? [label("Change URL", run.change.url)] : []),
|
|
259
|
+
...executionsByRecency.flatMap((execution, index) => [
|
|
50
260
|
"",
|
|
51
|
-
`
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
261
|
+
`Execution ${executionsByRecency.length - index}`,
|
|
262
|
+
label("Execution ID", execution.id),
|
|
263
|
+
label("Created ISO", execution.createdAt),
|
|
264
|
+
label("Started ISO", execution.startedAt ?? "not recorded"),
|
|
265
|
+
label("Finished ISO", execution.finishedAt ?? "not recorded"),
|
|
55
266
|
...(execution.recoveryOf
|
|
56
|
-
? [
|
|
57
|
-
`Recovered from: ${execution.recoveryOf}`,
|
|
58
|
-
`Reused: ${execution.reusedSteps?.join(", ") ?? "—"}`,
|
|
59
|
-
]
|
|
267
|
+
? [label("Recovery source", execution.recoveryOf)]
|
|
60
268
|
: []),
|
|
61
269
|
]),
|
|
62
|
-
|
|
63
|
-
"RECOVERY",
|
|
64
|
-
recoveryReason ?? "Eligible for admission; runner checks still required",
|
|
65
|
-
"",
|
|
66
|
-
"ERROR",
|
|
67
|
-
run.error ?? "None",
|
|
68
|
-
"",
|
|
69
|
-
"VALIDATION",
|
|
70
|
-
...(run.validation?.flatMap((check) => [
|
|
71
|
-
`${check.command} ${check.args.join(" ")} · exit ${check.exitCode} · ${duration(check.startedAt, check.finishedAt, now)}`,
|
|
72
|
-
`Log: ${check.log}`,
|
|
73
|
-
]) ?? ["No checks recorded"]),
|
|
74
|
-
"",
|
|
75
|
-
"AGENT SESSIONS",
|
|
76
|
-
...sessions.flatMap((session) => [
|
|
77
|
-
`${session.step} · invocation ${session.attempt} · ${session.outcome}`,
|
|
78
|
-
`Session: ${session.sessionId ?? session.sessionState}`,
|
|
79
|
-
`Duration: ${duration(session.startedAt, session.finishedAt, now)}`,
|
|
80
|
-
`Requested: ${JSON.stringify(session.requested)}`,
|
|
81
|
-
`Effective: ${JSON.stringify(session.effective)}`,
|
|
82
|
-
`Log: ${session.log}`,
|
|
270
|
+
...(run.validation?.flatMap((check, index) => [
|
|
83
271
|
"",
|
|
272
|
+
`Validation ${index + 1}`,
|
|
273
|
+
label("Log path", check.log),
|
|
274
|
+
label("Started ISO", check.startedAt),
|
|
275
|
+
label("Finished ISO", check.finishedAt),
|
|
276
|
+
]) ?? []),
|
|
277
|
+
...sessions.flatMap((session, index) => [
|
|
278
|
+
"",
|
|
279
|
+
`Agent session ${index + 1}`,
|
|
280
|
+
label("Invocation ID", session.id),
|
|
281
|
+
label("Session ID", session.sessionId ?? session.sessionState),
|
|
282
|
+
label("Provider", session.provider),
|
|
283
|
+
label("Log path", session.log),
|
|
284
|
+
label("Started ISO", session.startedAt),
|
|
285
|
+
label("Finished ISO", session.finishedAt ?? "not recorded"),
|
|
286
|
+
label("Requested profile", json(session.requested)),
|
|
287
|
+
label("Effective profile", json(session.effective)),
|
|
84
288
|
]),
|
|
85
289
|
];
|
|
290
|
+
return [
|
|
291
|
+
`#${run.issue.number} ${run.issue.title}`,
|
|
292
|
+
label("Outcome", run.outcome),
|
|
293
|
+
label("Phase", run.phase),
|
|
294
|
+
label("Attempt", String(run.attempt)),
|
|
295
|
+
label("Total elapsed", elapsedRun(run, now).replace("—", "unavailable")),
|
|
296
|
+
label("Branch", run.branch || "unavailable"),
|
|
297
|
+
label("Health", `Error ${run.error || run.executions?.some((execution) => execution.error) ? "recorded" : "none"} · Validation ${validation} · Recovery ${recovery}`),
|
|
298
|
+
...attention,
|
|
299
|
+
"",
|
|
300
|
+
...evidenceArea,
|
|
301
|
+
...diagnostics,
|
|
302
|
+
...technical,
|
|
303
|
+
];
|
|
86
304
|
}
|
|
87
|
-
export function Lines({ lines, width, height, offset = 0, }) {
|
|
88
|
-
const wrapped =
|
|
305
|
+
export function Lines({ lines, width, height, offset = 0, outcome, }) {
|
|
306
|
+
const wrapped = wrapDetailLines(lines, width);
|
|
89
307
|
const start = Math.min(offset, Math.max(0, wrapped.length - height));
|
|
90
|
-
|
|
308
|
+
const summaryOutcomeIndex = wrapped.findIndex((line) => line.startsWith("Outcome"));
|
|
309
|
+
return (_jsx(Box, { flexDirection: "column", width: width, height: height, overflow: "hidden", children: wrapped.slice(start, start + height).map((line, index) => {
|
|
310
|
+
const absoluteIndex = start + index;
|
|
311
|
+
const divider = line.indexOf(detailColumnGap);
|
|
312
|
+
const left = divider >= 0 ? line.slice(0, divider) : line;
|
|
313
|
+
const right = divider >= 0
|
|
314
|
+
? line.slice(divider + detailColumnGap.length)
|
|
315
|
+
: undefined;
|
|
316
|
+
const isHeading = (value) => /^[A-Z][A-Z0-9 ]+$/.test(value.trim());
|
|
317
|
+
return (_jsx(Box, { height: 1, flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", color: absoluteIndex === summaryOutcomeIndex
|
|
318
|
+
? colorFor(outcome ?? "")
|
|
319
|
+
: undefined, children: [_jsx(Text, { bold: isHeading(left), children: left || " " }), right !== undefined ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: detailColumnGap }), _jsx(Text, { bold: isHeading(right), children: right })] })) : null] }) }, `${absoluteIndex}`));
|
|
320
|
+
}) }));
|
|
91
321
|
}
|
|
92
322
|
export function RunList({ runs, selected, width, height, now, }) {
|
|
93
323
|
const count = Math.max(1, Math.floor(height / 3));
|
package/dist/src/workspace.js
CHANGED
|
@@ -3,6 +3,12 @@ import { access, lstat, mkdir, readFile, realpath } from "node:fs/promises";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { BlockedError, } from "./domain.js";
|
|
5
5
|
import { command } from "./runtime/process.js";
|
|
6
|
+
function fileStateDigest(content, mode) {
|
|
7
|
+
return createHash("sha256")
|
|
8
|
+
.update(content)
|
|
9
|
+
.update(String(mode))
|
|
10
|
+
.digest("hex");
|
|
11
|
+
}
|
|
6
12
|
export class ExistingCheckout {
|
|
7
13
|
processDirectories = new Map();
|
|
8
14
|
processDirectory(project) {
|
|
@@ -97,7 +103,7 @@ export class ExistingCheckout {
|
|
|
97
103
|
if (stat.isSymbolicLink())
|
|
98
104
|
throw new BlockedError(`Changed symlink requires manual handling: ${path}`);
|
|
99
105
|
const content = await readFile(absolute);
|
|
100
|
-
files[path] =
|
|
106
|
+
files[path] = fileStateDigest(content, stat.mode);
|
|
101
107
|
hash.update(path).update(content).update(String(stat.mode));
|
|
102
108
|
if (untracked.split("\0").includes(path))
|
|
103
109
|
fullDiff += `\n--- /dev/null\n+++ b/${path}\n${content.toString()}`;
|
|
@@ -125,12 +131,19 @@ export class ExistingCheckout {
|
|
|
125
131
|
}
|
|
126
132
|
async commit(project, expected, publication, runId, signal) {
|
|
127
133
|
signal?.throwIfAborted();
|
|
134
|
+
const runMarkers = publication.commitMessage
|
|
135
|
+
.split(/\r?\n/)
|
|
136
|
+
.map((line) => line.trim())
|
|
137
|
+
.filter((line) => line.startsWith("Agent-Workflows-Run:"));
|
|
138
|
+
if (runMarkers.length !== 1 ||
|
|
139
|
+
runMarkers[0] !== `Agent-Workflows-Run: ${runId}`)
|
|
140
|
+
throw new BlockedError("Commit message must contain exactly one matching workflow run marker");
|
|
128
141
|
const current = await this.inspect(project);
|
|
129
142
|
if (current.head !== expected.head) {
|
|
130
143
|
const message = await this.git(project, "log", "-1", "--format=%B");
|
|
131
144
|
const parent = await this.git(project, "rev-parse", "HEAD^");
|
|
132
145
|
if (parent === expected.head &&
|
|
133
|
-
message.
|
|
146
|
+
message.trimEnd() === publication.commitMessage.trimEnd() &&
|
|
134
147
|
current.paths.length === 0) {
|
|
135
148
|
const changed = (await this.git(project, "diff", "HEAD^", "HEAD", "--no-renames", "--name-only", "-z"))
|
|
136
149
|
.split("\0")
|
|
@@ -139,7 +152,11 @@ export class ExistingCheckout {
|
|
|
139
152
|
if (JSON.stringify(changed) !== JSON.stringify(expected.paths))
|
|
140
153
|
throw new BlockedError("Reconciled commit has an unexpected change set");
|
|
141
154
|
for (const [path, digest] of Object.entries(expected.files)) {
|
|
142
|
-
const
|
|
155
|
+
const absolute = join(project.checkout, path);
|
|
156
|
+
const actual = await Promise.all([
|
|
157
|
+
readFile(absolute),
|
|
158
|
+
lstat(absolute),
|
|
159
|
+
]).then(([content, stat]) => fileStateDigest(content, stat.mode), (error) => {
|
|
143
160
|
if (error.code === "ENOENT")
|
|
144
161
|
return null;
|
|
145
162
|
throw error;
|
|
@@ -156,15 +173,11 @@ export class ExistingCheckout {
|
|
|
156
173
|
throw new BlockedError("No changes to commit");
|
|
157
174
|
await this.runGit(project, ["add", "--", ...expected.paths], { signal });
|
|
158
175
|
await this.runGit(project, [
|
|
159
|
-
"-c",
|
|
160
|
-
`user.name=${project.gitIdentity.name}`,
|
|
161
|
-
"-c",
|
|
162
|
-
`user.email=${project.gitIdentity.email}`,
|
|
163
176
|
"-c",
|
|
164
177
|
"core.hooksPath=/dev/null",
|
|
165
178
|
"commit",
|
|
166
179
|
"-m",
|
|
167
|
-
|
|
180
|
+
publication.commitMessage,
|
|
168
181
|
], { signal });
|
|
169
182
|
return this.git(project, "rev-parse", "HEAD");
|
|
170
183
|
}
|
package/docs/api.md
CHANGED
|
@@ -4,7 +4,13 @@ Exports are in `src/index.ts`; the built package resolves to `dist/src/index.js`
|
|
|
4
4
|
|
|
5
5
|
## Runner and controls
|
|
6
6
|
|
|
7
|
-
`new Runner({config,databaseUrl,hosting,agents,workspace?,workflow?,workflowVersion?,promptBaseDirectory?})` injects hosting/agent adapters and optionally a workspace strategy or workflow. `hosting(project)` returns a host-qualified adapter. `agents` maps provider names to adapters. The default workspace uses the existing checkout. One DBOS runtime runs per Node process; one runner owns each configuration and checkout.
|
|
7
|
+
`new Runner({config,databaseUrl,hosting,agents,workspace?,workflow?,workflowVersion?,pathBaseDirectory?,promptBaseDirectory?})` injects hosting/agent adapters and optionally a workspace strategy or workflow. `hosting(project)` returns a host-qualified adapter. `agents` maps provider names to adapters. The default workspace uses the existing checkout. One DBOS runtime runs per Node process; one runner owns each configuration and checkout.
|
|
8
|
+
|
|
9
|
+
`pathBaseDirectory` must identify an existing directory. It resolves relative
|
|
10
|
+
state, checkout, and prompt-file paths once when the runner is constructed.
|
|
11
|
+
Omitting it preserves current-working-directory behavior for SDK callers.
|
|
12
|
+
`promptBaseDirectory` retains its narrower role and, when both are supplied,
|
|
13
|
+
overrides only relative prompt files.
|
|
8
14
|
|
|
9
15
|
`start()` validates registration, acquires ownership, launches DBOS, registers concurrency-one project queues, and starts polling. `poll(projectId?)` performs an immediate scan. `pause(projectId)` stops new starts while active work continues. `resume(projectId)` refuses blocked checkouts. `stop(runId)` waits for the active invocation/process to end, or cancels queued work. `retry(runId)` requires a terminal failed/blocked/cancelled run and a clean checkout, then returns a new linked run ID. `recover(runId)` returns a new execution ID for publication recovery of the same run. `shutdown()` stops intake, cancels and awaits active work, closes DBOS and releases ownership.
|
|
10
16
|
|
|
@@ -17,7 +23,9 @@ the next attempt; another request for the same task fails while that retry is
|
|
|
17
23
|
queued or running. Replaying the same command ID returns its existing retry,
|
|
18
24
|
without rechecking the checkout or emitting events. A command ID cannot identify
|
|
19
25
|
retries of different runs. Retry creation, project unblocking and their events
|
|
20
|
-
commit together; failure preserves the blocked state.
|
|
26
|
+
commit together; failure preserves the blocked state. The rationale for separate
|
|
27
|
+
run and execution identities is in
|
|
28
|
+
[ADR 0003](adr/0003-run-and-execution-identity.md).
|
|
21
29
|
|
|
22
30
|
## Durable operations
|
|
23
31
|
|
|
@@ -29,7 +37,7 @@ commit together; failure preserves the blocked state.
|
|
|
29
37
|
| `prepare()` | Require clean Git state; fetch base and create unique branch |
|
|
30
38
|
| `implement()` | Fresh implementation session; reject unexpected commits or branch changes |
|
|
31
39
|
| `validate()` | Run commands and verify unchanged diff; false means no change |
|
|
32
|
-
| `writePublication()` |
|
|
40
|
+
| `writePublication()` | Validate text; finalize persisted run/co-author trailers from retained changes |
|
|
33
41
|
| `commit()` / `push()` | Separate reconciled Git effects using the verified change set |
|
|
34
42
|
| `publish()` | Find existing request by branch before creating a draft |
|
|
35
43
|
| `review()` | Fresh read-only reviewer; exact published diff, head and validation evidence |
|
|
@@ -44,7 +52,7 @@ checks. Stage overrides replace only `defaultPrompt`. Custom stages have no
|
|
|
44
52
|
inferred built-in default; resolve file-based custom stages before invocation
|
|
45
53
|
with `resolveStagePrompt(stage, defaultPrompt, baseDirectory)` if they must be
|
|
46
54
|
frozen alongside startup configuration. The runner resolves built-in prompt files
|
|
47
|
-
at construction using `promptBaseDirectory`.
|
|
55
|
+
at construction using `promptBaseDirectory ?? pathBaseDirectory ?? process.cwd()`.
|
|
48
56
|
|
|
49
57
|
```ts
|
|
50
58
|
await operations.invoke("report", stage, {
|
|
@@ -135,7 +143,7 @@ Caveats:
|
|
|
135
143
|
|
|
136
144
|
## Extension contracts
|
|
137
145
|
|
|
138
|
-
`Workspace` separates `check`, `prepare`, `inspect`, `verify`, `commit`, `push` and `release`. `Snapshot` contains branch/head, changed paths, diff and a content fingerprint. Never implement release by discarding files. A future isolated workspace implementation can replace this interface without changing workflow composition.
|
|
146
|
+
`Workspace` separates `check`, `prepare`, `inspect`, `verify`, `commit`, `push` and `release`. `Snapshot` contains branch/head, changed paths, diff and a content fingerprint. `commit` receives the already-finalized message, including attribution and run marker, and must preserve native Git identity selection. Never implement release by discarding files. A future isolated workspace implementation can replace this interface without changing workflow composition.
|
|
139
147
|
|
|
140
148
|
`prepare`, `commit` and `push` receive an optional final `AbortSignal`. Custom workspaces must stop their subprocesses before settling a cancelled operation. The existing-checkout strategy journals Git processes under the Git directory; ownership acquisition rejects surviving process groups after a runner crash.
|
|
141
149
|
|
|
@@ -150,10 +158,16 @@ Caveats:
|
|
|
150
158
|
`Store.admitRetry` owns persisted retry admission. Runner supplies its checkout
|
|
151
159
|
and process safety check, which runs under the project lock for new admissions
|
|
152
160
|
only. This callback must not mutate Store records. Operator tools should use
|
|
153
|
-
`retry` commands or `Runner.retry`, preserving those safety checks.
|
|
161
|
+
`retry` commands or `Runner.retry`, preserving those safety checks. The locking
|
|
162
|
+
boundary is recorded in [ADR 0005](adr/0005-postgresql-persistence-boundary.md).
|
|
154
163
|
|
|
155
164
|
Invocation records include project/run IDs, stable DBOS step ID and name, invocation ID, attempt, timestamps, requested/effective profile, provider, effective task prompt/source/hash, output-contract and evidence identities, artifact path and session state (`pending`, `available`, `unavailable`). Repeated custom steps retain separate invocations. A retry has a separate run record linked to its predecessor.
|
|
156
165
|
|
|
166
|
+
Writable invocation before/after evidence is retained on the run. Publication
|
|
167
|
+
finalization persists contributing provider identities in first-contribution
|
|
168
|
+
order, independent of custom stage names. Recovery reuses that provenance and
|
|
169
|
+
the finalized publication message.
|
|
170
|
+
|
|
157
171
|
`request(kind,target)` queues the same `pause`, `resume`, `stop`, `retry`, or `recover` commands used by the CLI/TUI; `commands()` reports pending/success/failure. A runner must be active to execute them. `finishCommand` and record-writing methods support adapters and custom workflows; operator tools should prefer commands over direct mutation.
|
|
158
172
|
|
|
159
173
|
## Publication recovery
|
|
@@ -171,7 +185,8 @@ markers. `Store.recoveryPlan(runId)` reports persisted eligibility and its reaso
|
|
|
171
185
|
live safety checks happen at admission and execution. Runs without execution
|
|
172
186
|
metadata remain readable and retryable, but cannot be recovered.
|
|
173
187
|
|
|
174
|
-
Recovery
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
188
|
+
Recovery preserves the original workflow input and completed checkpoint prefix.
|
|
189
|
+
The accepted command ID identifies the new execution so uncertain dispatch can
|
|
190
|
+
be reconciled after a crash. Live checks still run in the first non-replayed
|
|
191
|
+
operation. See [ADR 0003](adr/0003-run-and-execution-identity.md) for the complete
|
|
192
|
+
identity and recovery decision.
|