@mingchuno/agent-workflows 0.2.0 → 0.3.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 +6 -1
- package/dist/src/attribution.d.ts +9 -0
- package/dist/src/attribution.js +57 -0
- package/dist/src/cli.js +47 -13
- 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 +15 -4
- package/docs/configuration.md +50 -9
- package/docs/operations.md +45 -5
- package/examples/config.ts +4 -4
- package/examples/run.ts +4 -1
- package/package.json +1 -1
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
|
|
|
@@ -29,7 +35,7 @@ commit together; failure preserves the blocked state.
|
|
|
29
35
|
| `prepare()` | Require clean Git state; fetch base and create unique branch |
|
|
30
36
|
| `implement()` | Fresh implementation session; reject unexpected commits or branch changes |
|
|
31
37
|
| `validate()` | Run commands and verify unchanged diff; false means no change |
|
|
32
|
-
| `writePublication()` |
|
|
38
|
+
| `writePublication()` | Validate text; finalize persisted run/co-author trailers from retained changes |
|
|
33
39
|
| `commit()` / `push()` | Separate reconciled Git effects using the verified change set |
|
|
34
40
|
| `publish()` | Find existing request by branch before creating a draft |
|
|
35
41
|
| `review()` | Fresh read-only reviewer; exact published diff, head and validation evidence |
|
|
@@ -44,7 +50,7 @@ checks. Stage overrides replace only `defaultPrompt`. Custom stages have no
|
|
|
44
50
|
inferred built-in default; resolve file-based custom stages before invocation
|
|
45
51
|
with `resolveStagePrompt(stage, defaultPrompt, baseDirectory)` if they must be
|
|
46
52
|
frozen alongside startup configuration. The runner resolves built-in prompt files
|
|
47
|
-
at construction using `promptBaseDirectory`.
|
|
53
|
+
at construction using `promptBaseDirectory ?? pathBaseDirectory ?? process.cwd()`.
|
|
48
54
|
|
|
49
55
|
```ts
|
|
50
56
|
await operations.invoke("report", stage, {
|
|
@@ -135,7 +141,7 @@ Caveats:
|
|
|
135
141
|
|
|
136
142
|
## Extension contracts
|
|
137
143
|
|
|
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.
|
|
144
|
+
`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
145
|
|
|
140
146
|
`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
147
|
|
|
@@ -154,6 +160,11 @@ only. This callback must not mutate Store records. Operator tools should use
|
|
|
154
160
|
|
|
155
161
|
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
162
|
|
|
163
|
+
Writable invocation before/after evidence is retained on the run. Publication
|
|
164
|
+
finalization persists contributing provider identities in first-contribution
|
|
165
|
+
order, independent of custom stage names. Recovery reuses that provenance and
|
|
166
|
+
the finalized publication message.
|
|
167
|
+
|
|
157
168
|
`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
169
|
|
|
159
170
|
## Publication recovery
|
package/docs/configuration.md
CHANGED
|
@@ -1,29 +1,46 @@
|
|
|
1
1
|
# Configuration
|
|
2
2
|
|
|
3
|
-
The CLI reads `agent-workflows.json`, or `--config PATH`.
|
|
3
|
+
The CLI reads `agent-workflows.json`, or `--config PATH`. Both paths resolve from
|
|
4
|
+
the launch directory. Unknown properties are rejected by Zod. Relative
|
|
5
|
+
configuration-owned paths resolve from the directory containing the resolved
|
|
6
|
+
configuration file, independently of where the CLI is launched. Use
|
|
7
|
+
`--config-base-directory DIRECTORY` to select another base; a relative option
|
|
8
|
+
value resolves from the launch directory. The base must be an existing directory
|
|
9
|
+
and is canonicalized before startup.
|
|
4
10
|
|
|
5
11
|
| Runner field | Default / meaning |
|
|
6
12
|
| ---------------- | --------------------------------------------------------------------------------------------------------- |
|
|
7
13
|
| `id` | Required stable letters/digits/underscore/hyphen identity; scopes records and queues |
|
|
8
14
|
| `databaseUrlEnv` | `AGENT_WORKFLOWS_DATABASE_URL`; environment variable containing a PostgreSQL connection URL with username |
|
|
9
|
-
| `stateDirectory` | `.agent-workflows`;
|
|
15
|
+
| `stateDirectory` | `.agent-workflows`; relative to the configuration base and outside every managed checkout |
|
|
10
16
|
| `projects` | Nonempty array; duplicate IDs or canonical checkout roots are rejected |
|
|
11
17
|
|
|
12
18
|
| Project field | Default / meaning |
|
|
13
19
|
| ---------------------- | --------------------------------------------------------------------------------------------------- |
|
|
14
|
-
| `id`, `checkout` | Required stable identity and existing Git repository root
|
|
20
|
+
| `id`, `checkout` | Required stable identity and existing Git repository root; relative to the configuration base |
|
|
15
21
|
| `hosting` | `provider` (`github`/`gitlab`), web `origin`, `repository`, and `tokenEnv`; no serialized tokens |
|
|
16
22
|
| `labels` | `['ready-for-agent']`; all labels must match |
|
|
17
23
|
| `baseBranch`, `remote` | `main`, `origin`; Git remote is independent of hosting API origin |
|
|
18
24
|
| `branchTemplate` | `agent/{issue}-{attempt}`; `{issue}` required; `{attempt}` and `{run}` supported |
|
|
19
25
|
| `pollIntervalMs` | 30000; minimum 100 |
|
|
20
|
-
| `
|
|
26
|
+
| `includeAgentCoAuthors` | `true`; append co-author trailers for providers whose writable invocations produced retained changes |
|
|
21
27
|
| `validation` | Array of `{command,args,timeoutMs}`; no shell expansion; timeout defaults to 300000 ms |
|
|
22
28
|
| `agent` | Required default profile |
|
|
23
29
|
| `stages` | `implementation`, `publication`, `review`; each has optional `profile`, `prompt`, `promptFile`, `timeoutMs` |
|
|
24
30
|
|
|
25
31
|
Issues are selected in ascending issue-number order within each intake scan. Deduplication persists across restarts. An explicit retry is a new numbered attempt linked through `retryOf`.
|
|
26
32
|
|
|
33
|
+
Absolute configuration paths remain absolute. Effective state and checkout
|
|
34
|
+
paths are normalized once during startup before safety and ownership checks, so
|
|
35
|
+
a later working-directory change cannot redirect a running process. Validation
|
|
36
|
+
commands still execute in the canonical checkout; command names and arguments
|
|
37
|
+
are passed unchanged and are not rebased to the configuration directory.
|
|
38
|
+
|
|
39
|
+
`init` writes relative checkout and external sibling-state paths from the
|
|
40
|
+
effective configuration base. It still refuses to overwrite an existing file.
|
|
41
|
+
When the configuration is in the checkout root, the checkout is `.` and state
|
|
42
|
+
is the relative sibling `<checkout-name>.agent-workflows`.
|
|
43
|
+
|
|
27
44
|
## CLI environment files
|
|
28
45
|
|
|
29
46
|
Select one file explicitly for any CLI command:
|
|
@@ -128,11 +145,12 @@ in inspection explicitly; do not present an incomplete review as a clean review.
|
|
|
128
145
|
These exact defaults are checked against the runtime source.
|
|
129
146
|
|
|
130
147
|
Use either nonblank literal `prompt` text or a `promptFile` path, never both.
|
|
131
|
-
Files must contain nonblank UTF-8 text. Relative paths
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
148
|
+
Files must contain nonblank UTF-8 text. Relative paths use the same configuration
|
|
149
|
+
base as state and checkout paths; absolute paths are allowed. Files load once
|
|
150
|
+
when the runner is constructed. Restart to apply edits. No templating,
|
|
151
|
+
interpolation, or includes are supported. SDK callers use `pathBaseDirectory` as
|
|
152
|
+
the general base. `promptBaseDirectory`, when supplied, overrides it for prompt
|
|
153
|
+
files only.
|
|
136
154
|
|
|
137
155
|
```json
|
|
138
156
|
"stages": {
|
|
@@ -146,6 +164,29 @@ supply `promptBaseDirectory` for relative paths.
|
|
|
146
164
|
without aliases. Configure skills in the selected agent runtime and request them
|
|
147
165
|
in task text. Old invocation records and skill snapshots remain readable.
|
|
148
166
|
|
|
167
|
+
## Git identity and agent attribution
|
|
168
|
+
|
|
169
|
+
Commits use Git's native author and committer selection. The application does
|
|
170
|
+
not configure, validate, snapshot, or override identity. Repository, worktree,
|
|
171
|
+
global, system, and identity environment settings therefore behave as they do
|
|
172
|
+
for `git commit`, including distinct author and committer identities. Missing or
|
|
173
|
+
invalid identity fails at the commit operation with Git's error. The removed
|
|
174
|
+
`gitIdentity` property is rejected as unknown input.
|
|
175
|
+
|
|
176
|
+
Agent assistance is represented separately. With `includeAgentCoAuthors: true`,
|
|
177
|
+
the finalized commit message includes each provider once, ordered by its first
|
|
178
|
+
successful writable invocation that produced a retained change:
|
|
179
|
+
|
|
180
|
+
- `Codex <noreply@openai.com>` (OpenAI's published implementation convention)
|
|
181
|
+
- `Copilot <223556219+Copilot@users.noreply.github.com>` (GitHub's current
|
|
182
|
+
first-party convention, not a stable product API guarantee)
|
|
183
|
+
|
|
184
|
+
Read-only publication/review invocations and writable invocations with no
|
|
185
|
+
accepted retained change are not attributed. Existing trailers are preserved,
|
|
186
|
+
matching agent trailers are deduplicated, and exactly one
|
|
187
|
+
`Agent-Workflows-Run` trailer remains. Set `includeAgentCoAuthors: false` per
|
|
188
|
+
project to disable injection; existing publication trailers are not removed.
|
|
189
|
+
|
|
149
190
|
Stage timeout defaults to 30 minutes, including profile validation and at most
|
|
150
191
|
one format-correction attempt. Correction uses a fresh inspection-only session
|
|
151
192
|
and only the remaining deadline. Provider failures, cancellation, timeout and
|