@drisp/cli 0.5.28 → 0.6.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 +88 -47
- package/dist/{WorkflowInstallWizard-7OSOOVS2.js → WorkflowInstallWizard-JLB7P37Y.js} +4 -3
- package/dist/chunk-GMB4EJLK.js +56 -0
- package/dist/{chunk-2HR7FV3M.js → chunk-KPDCVR6Q.js} +1 -1
- package/dist/chunk-PMRYQUBV.js +34104 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/{chunk-2QV7YSX6.js → chunk-QVRTU2MM.js} +1313 -555
- package/dist/cli.js +9812 -10139
- package/dist/dashboard-daemon.js +6 -247
- package/dist/hook-forwarder.js +1 -0
- package/dist/runner.js +11 -0
- package/package.json +13 -6
- package/dist/athena-gateway.js +0 -4148
- package/dist/chunk-4HWBXCVY.js +0 -18954
- package/dist/chunk-MSPZOJMJ.js +0 -76
- package/dist/chunk-VAFS446Y.js +0 -1088
|
@@ -884,6 +884,19 @@ import fs5 from "fs";
|
|
|
884
884
|
import os3 from "os";
|
|
885
885
|
import path4 from "path";
|
|
886
886
|
var EMPTY_CONFIG = { plugins: [], additionalDirectories: [] };
|
|
887
|
+
var STALE_CONFIG_KEYS = ["channels"];
|
|
888
|
+
var warnedStaleKeys = /* @__PURE__ */ new Set();
|
|
889
|
+
function warnStaleKeys(configPath, raw) {
|
|
890
|
+
for (const key of STALE_CONFIG_KEYS) {
|
|
891
|
+
if (!(key in raw)) continue;
|
|
892
|
+
const dedupeKey = `${configPath}\0${key}`;
|
|
893
|
+
if (warnedStaleKeys.has(dedupeKey)) continue;
|
|
894
|
+
warnedStaleKeys.add(dedupeKey);
|
|
895
|
+
console.error(
|
|
896
|
+
`Warning: ignoring unsupported config key "${key}" in ${configPath} (channels were removed; delete the key to silence this)`
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
887
900
|
function projectConfigPath(projectDir) {
|
|
888
901
|
return path4.join(projectDir, ".athena", "config.json");
|
|
889
902
|
}
|
|
@@ -929,6 +942,12 @@ function readConfigFile(configPath, baseDir) {
|
|
|
929
942
|
return EMPTY_CONFIG;
|
|
930
943
|
}
|
|
931
944
|
const raw = JSON.parse(fs5.readFileSync(configPath, "utf-8"));
|
|
945
|
+
warnStaleKeys(configPath, raw);
|
|
946
|
+
if (raw.permissionGraceMs !== void 0 && (typeof raw.permissionGraceMs !== "number" || !Number.isFinite(raw.permissionGraceMs) || raw.permissionGraceMs < 0)) {
|
|
947
|
+
throw new Error(
|
|
948
|
+
`Invalid config: "${configPath}" field "permissionGraceMs" must be a non-negative number of milliseconds`
|
|
949
|
+
);
|
|
950
|
+
}
|
|
932
951
|
if ("workflowMarketplaceSource" in raw) {
|
|
933
952
|
throw new Error(
|
|
934
953
|
`Invalid config: "${configPath}" uses deprecated "workflowMarketplaceSource"; use "workflowMarketplaceSources"`
|
|
@@ -946,11 +965,6 @@ function readConfigFile(configPath, baseDir) {
|
|
|
946
965
|
`Invalid config: "${configPath}" field "harness" must be one of claude-code, openai-codex, opencode`
|
|
947
966
|
);
|
|
948
967
|
}
|
|
949
|
-
if (raw.channels !== void 0 && (!Array.isArray(raw.channels) || !raw.channels.every((c) => typeof c === "string"))) {
|
|
950
|
-
throw new Error(
|
|
951
|
-
`Invalid config: "${configPath}" field "channels" must be an array of strings`
|
|
952
|
-
);
|
|
953
|
-
}
|
|
954
968
|
const plugins = (raw.plugins ?? []).map((p) => {
|
|
955
969
|
if (isMarketplaceRef(p)) return p;
|
|
956
970
|
return path4.isAbsolute(p) ? p : path4.resolve(baseDir, p);
|
|
@@ -971,7 +985,7 @@ function readConfigFile(configPath, baseDir) {
|
|
|
971
985
|
telemetry: raw.telemetry,
|
|
972
986
|
telemetryDiagnostics: raw.telemetryDiagnostics,
|
|
973
987
|
deviceId: raw.deviceId,
|
|
974
|
-
|
|
988
|
+
permissionGraceMs: raw.permissionGraceMs,
|
|
975
989
|
// Personal capabilities are stored opaque. In particular skill `path`
|
|
976
990
|
// is NOT relative-resolved against baseDir (R1) — Issue 3 resolves it
|
|
977
991
|
// to an absolute path at install time.
|
|
@@ -1007,7 +1021,8 @@ function writeConfigFile(configDir, configPath, updates, deleteKeys) {
|
|
|
1007
1021
|
var GLOBAL_CONFIG_LEGACY_KEYS = [
|
|
1008
1022
|
"workflow",
|
|
1009
1023
|
"mcpServerOptions",
|
|
1010
|
-
"workflowMarketplaceSource"
|
|
1024
|
+
"workflowMarketplaceSource",
|
|
1025
|
+
...STALE_CONFIG_KEYS
|
|
1011
1026
|
];
|
|
1012
1027
|
function writeGlobalConfig(updates) {
|
|
1013
1028
|
const homeDir = os3.homedir();
|
|
@@ -1037,6 +1052,7 @@ var DEFAULT_MAX_TURN_TOKEN_COUNT = 13e4;
|
|
|
1037
1052
|
var DEFAULT_NUDGE_CAP = 3;
|
|
1038
1053
|
var DEFAULT_RETRY_CAP = 3;
|
|
1039
1054
|
var DEFAULT_RETRY_BACKOFF_MS = 1e4;
|
|
1055
|
+
var DEFAULT_PERMISSION_GRACE_MS = 6e4;
|
|
1040
1056
|
function pluginSpecRef(spec) {
|
|
1041
1057
|
return typeof spec === "string" ? spec : spec.ref;
|
|
1042
1058
|
}
|
|
@@ -1138,7 +1154,7 @@ import fs7 from "fs";
|
|
|
1138
1154
|
import os4 from "os";
|
|
1139
1155
|
import path6 from "path";
|
|
1140
1156
|
|
|
1141
|
-
// src/core/workflows/
|
|
1157
|
+
// src/core/workflows/journalReader.ts
|
|
1142
1158
|
import fs6 from "fs";
|
|
1143
1159
|
import path5 from "path";
|
|
1144
1160
|
|
|
@@ -1152,22 +1168,49 @@ function substituteVariables(text, ctx) {
|
|
|
1152
1168
|
result = result.replaceAll("{sessionId}", ctx.sessionId);
|
|
1153
1169
|
result = result.replaceAll("<session_id>", ctx.sessionId);
|
|
1154
1170
|
}
|
|
1155
|
-
if (ctx.
|
|
1156
|
-
result = result.replaceAll("{
|
|
1171
|
+
if (ctx.journalPath !== void 0) {
|
|
1172
|
+
result = result.replaceAll("{journalPath}", ctx.journalPath);
|
|
1173
|
+
result = result.replaceAll("{trackerPath}", ctx.journalPath);
|
|
1157
1174
|
}
|
|
1158
1175
|
return result;
|
|
1159
1176
|
}
|
|
1160
1177
|
|
|
1161
|
-
// src/core/workflows/
|
|
1178
|
+
// src/core/workflows/journalReader.ts
|
|
1179
|
+
var DEPRECATION_REMOVAL_RELEASE = "0.7.0";
|
|
1162
1180
|
var DEFAULT_COMPLETION_MARKER = "<!-- WORKFLOW_COMPLETE -->";
|
|
1163
|
-
var
|
|
1164
|
-
var
|
|
1165
|
-
var
|
|
1166
|
-
var
|
|
1167
|
-
var
|
|
1168
|
-
|
|
1181
|
+
var DEFAULT_NEEDS_HUMAN_MARKER = "<!-- NEEDS_HUMAN";
|
|
1182
|
+
var LEGACY_BLOCKED_MARKER = "<!-- WORKFLOW_BLOCKED";
|
|
1183
|
+
var DEFAULT_JOURNAL_PATH = ".athena/{sessionId}/journal.md";
|
|
1184
|
+
var LEGACY_JOURNAL_FILENAME = "tracker.md";
|
|
1185
|
+
var JOURNAL_SKELETON_MARKER = "<!-- JOURNAL_SKELETON -->";
|
|
1186
|
+
var LEGACY_TRACKER_SKELETON_MARKER = "<!-- TRACKER_SKELETON -->";
|
|
1187
|
+
var DEFAULT_JOURNAL_TOKEN_BOUND = 8e3;
|
|
1188
|
+
var DEFAULT_CONTINUE_PROMPT = "Continue the task. Read the journal at {journalPath} for current progress. If the work is complete or you need a human, the terminal marker must be the final non-empty line of the journal; do not write any prose after it.";
|
|
1189
|
+
function loopNeedsHumanMarker(markers) {
|
|
1190
|
+
return markers.needsHumanMarker ?? markers.blockedMarker ?? DEFAULT_NEEDS_HUMAN_MARKER;
|
|
1191
|
+
}
|
|
1192
|
+
function loopJournalPath(loop) {
|
|
1193
|
+
return loop.journalPath ?? loop.trackerPath ?? DEFAULT_JOURNAL_PATH;
|
|
1194
|
+
}
|
|
1195
|
+
function needsHumanMarkersFor(markers) {
|
|
1196
|
+
return [
|
|
1197
|
+
.../* @__PURE__ */ new Set([
|
|
1198
|
+
loopNeedsHumanMarker(markers),
|
|
1199
|
+
DEFAULT_NEEDS_HUMAN_MARKER,
|
|
1200
|
+
LEGACY_BLOCKED_MARKER
|
|
1201
|
+
])
|
|
1202
|
+
];
|
|
1203
|
+
}
|
|
1204
|
+
function hasSkeletonMarker(content) {
|
|
1205
|
+
return content.includes(JOURNAL_SKELETON_MARKER) || content.includes(LEGACY_TRACKER_SKELETON_MARKER);
|
|
1206
|
+
}
|
|
1207
|
+
function buildMarkerDeprecation(deprecatedMarker) {
|
|
1208
|
+
const name = deprecatedMarker.replace(/^<!--\s*/, "").replace(/:?$/, "");
|
|
1209
|
+
return `${name} is deprecated and is removed in ${DEPRECATION_REMOVAL_RELEASE}; declare NEEDS_HUMAN: <reason> as the journal's final non-empty line instead.`;
|
|
1210
|
+
}
|
|
1211
|
+
function readJournal(journalPath) {
|
|
1169
1212
|
try {
|
|
1170
|
-
return fs6.readFileSync(
|
|
1213
|
+
return fs6.readFileSync(journalPath, "utf-8");
|
|
1171
1214
|
} catch {
|
|
1172
1215
|
return "";
|
|
1173
1216
|
}
|
|
@@ -1175,78 +1218,99 @@ function readTracker(trackerPath) {
|
|
|
1175
1218
|
function getNonEmptyLines(content) {
|
|
1176
1219
|
return content.trimEnd().split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1177
1220
|
}
|
|
1178
|
-
function
|
|
1179
|
-
return line === `${
|
|
1221
|
+
function isNeedsHumanLine(line, marker) {
|
|
1222
|
+
return line === `${marker} -->` || line.startsWith(`${marker}:`);
|
|
1223
|
+
}
|
|
1224
|
+
function matchNeedsHumanMarker(line, markers) {
|
|
1225
|
+
return markers.find((marker) => isNeedsHumanLine(line, marker));
|
|
1180
1226
|
}
|
|
1181
|
-
function isTerminalMarkerLine(line, completionMarker,
|
|
1182
|
-
return line === completionMarker ||
|
|
1227
|
+
function isTerminalMarkerLine(line, completionMarker, needsHumanMarkers) {
|
|
1228
|
+
return line === completionMarker || matchNeedsHumanMarker(line, needsHumanMarkers) !== void 0;
|
|
1183
1229
|
}
|
|
1184
|
-
function getMisplacedTerminalMarker(lines, completionMarker,
|
|
1230
|
+
function getMisplacedTerminalMarker(lines, completionMarker, needsHumanMarkers) {
|
|
1185
1231
|
if (lines.length < 2) return void 0;
|
|
1186
1232
|
const terminalLine = lines.at(-1);
|
|
1187
|
-
if (terminalLine && isTerminalMarkerLine(terminalLine, completionMarker,
|
|
1233
|
+
if (terminalLine && isTerminalMarkerLine(terminalLine, completionMarker, needsHumanMarkers)) {
|
|
1188
1234
|
return void 0;
|
|
1189
1235
|
}
|
|
1190
|
-
return lines.slice(0, -1).find(
|
|
1236
|
+
return lines.slice(0, -1).find(
|
|
1237
|
+
(line) => isTerminalMarkerLine(line, completionMarker, needsHumanMarkers)
|
|
1238
|
+
);
|
|
1191
1239
|
}
|
|
1192
|
-
function
|
|
1193
|
-
if (!line.startsWith(
|
|
1194
|
-
const afterMarker = line.slice(
|
|
1240
|
+
function extractNeedsHumanReason(line, marker) {
|
|
1241
|
+
if (!line.startsWith(marker)) return void 0;
|
|
1242
|
+
const afterMarker = line.slice(marker.length);
|
|
1195
1243
|
const match = afterMarker.match(/^:\s*(.+?)(?:\s*-->|$)/);
|
|
1196
1244
|
return match?.[1]?.trim();
|
|
1197
1245
|
}
|
|
1198
|
-
function
|
|
1246
|
+
function parseJournalState(content, markers = {}) {
|
|
1199
1247
|
const completionMarker = markers.completionMarker ?? DEFAULT_COMPLETION_MARKER;
|
|
1200
|
-
const
|
|
1248
|
+
const needsHumanMarkers = needsHumanMarkersFor(markers);
|
|
1201
1249
|
const lines = getNonEmptyLines(content);
|
|
1202
1250
|
const terminalLine = lines.at(-1);
|
|
1203
1251
|
const completed = terminalLine === completionMarker;
|
|
1204
|
-
const
|
|
1205
|
-
const
|
|
1252
|
+
const matchedMarker = terminalLine !== void 0 ? matchNeedsHumanMarker(terminalLine, needsHumanMarkers) : void 0;
|
|
1253
|
+
const needsHuman = matchedMarker !== void 0;
|
|
1254
|
+
const needsHumanReason = matchedMarker && terminalLine ? extractNeedsHumanReason(terminalLine, matchedMarker) : void 0;
|
|
1206
1255
|
return {
|
|
1207
1256
|
completed,
|
|
1208
|
-
|
|
1209
|
-
|
|
1257
|
+
needsHuman,
|
|
1258
|
+
needsHumanReason,
|
|
1210
1259
|
misplacedTerminalMarker: getMisplacedTerminalMarker(
|
|
1211
1260
|
lines,
|
|
1212
1261
|
completionMarker,
|
|
1213
|
-
|
|
1262
|
+
needsHumanMarkers
|
|
1214
1263
|
),
|
|
1215
|
-
skeletonNotReplaced: content
|
|
1264
|
+
skeletonNotReplaced: hasSkeletonMarker(content),
|
|
1265
|
+
...matchedMarker === LEGACY_BLOCKED_MARKER ? { deprecatedMarker: LEGACY_BLOCKED_MARKER } : {}
|
|
1216
1266
|
};
|
|
1217
1267
|
}
|
|
1218
1268
|
function demoteTerminalMarkers(content, markers = {}) {
|
|
1219
1269
|
const completionMarker = markers.completionMarker ?? DEFAULT_COMPLETION_MARKER;
|
|
1220
|
-
const
|
|
1270
|
+
const needsHumanMarkers = needsHumanMarkersFor(markers);
|
|
1221
1271
|
return content.split("\n").map((line) => {
|
|
1222
1272
|
const trimmed = line.trim();
|
|
1223
1273
|
if (trimmed === completionMarker) return "> _Prior Run ended: complete._";
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1274
|
+
const matched = matchNeedsHumanMarker(trimmed, needsHumanMarkers);
|
|
1275
|
+
if (matched) {
|
|
1276
|
+
const reason = extractNeedsHumanReason(trimmed, matched);
|
|
1277
|
+
return reason ? `> _Prior Run ended: needed a human \u2014 ${reason}._` : "> _Prior Run ended: needed a human._";
|
|
1227
1278
|
}
|
|
1228
1279
|
return line;
|
|
1229
1280
|
}).join("\n");
|
|
1230
1281
|
}
|
|
1282
|
+
function insertAboveTerminalMarker(content, entry, markers = {}) {
|
|
1283
|
+
const completionMarker = markers.completionMarker ?? DEFAULT_COMPLETION_MARKER;
|
|
1284
|
+
const needsHumanMarkers = needsHumanMarkersFor(markers);
|
|
1285
|
+
const trimmed = content.trimEnd();
|
|
1286
|
+
const lastBreak = trimmed.lastIndexOf("\n");
|
|
1287
|
+
const lastLine = trimmed.slice(lastBreak + 1).trim();
|
|
1288
|
+
if (!isTerminalMarkerLine(lastLine, completionMarker, needsHumanMarkers)) {
|
|
1289
|
+
return content + entry;
|
|
1290
|
+
}
|
|
1291
|
+
const above = lastBreak === -1 ? "" : trimmed.slice(0, lastBreak);
|
|
1292
|
+
const markerLine = trimmed.slice(lastBreak + 1);
|
|
1293
|
+
return `${above.trimEnd()}${entry.replace(/\n*$/, "\n")}
|
|
1294
|
+
${markerLine}
|
|
1295
|
+
`;
|
|
1296
|
+
}
|
|
1231
1297
|
function buildContinuePrompt(loop) {
|
|
1232
1298
|
const template = loop.continuePrompt ?? DEFAULT_CONTINUE_PROMPT;
|
|
1233
|
-
return substituteVariables(template, {
|
|
1234
|
-
trackerPath: loop.trackerPath ?? DEFAULT_TRACKER_PATH
|
|
1235
|
-
});
|
|
1299
|
+
return substituteVariables(template, { journalPath: loopJournalPath(loop) });
|
|
1236
1300
|
}
|
|
1237
1301
|
function buildNudgePrompt(loop, opts) {
|
|
1238
1302
|
const completionMarker = loop.completionMarker ?? DEFAULT_COMPLETION_MARKER;
|
|
1239
|
-
const
|
|
1240
|
-
const
|
|
1241
|
-
const bootstrapPreamble = opts?.skeletonNotReplaced ? `You stopped without writing the
|
|
1242
|
-
return bootstrapPreamble + `If everything is done and verified, write ${completionMarker} as the final non-empty line of the
|
|
1303
|
+
const needsHumanMarker = loopNeedsHumanMarker(loop);
|
|
1304
|
+
const journalPath = loopJournalPath(loop);
|
|
1305
|
+
const bootstrapPreamble = opts?.skeletonNotReplaced ? `You stopped without writing the journal at ${journalPath} \u2014 it still contains the runner's skeleton. If you were asking the human a question, do not ask it in chat: write it to the journal and declare ${needsHumanMarker}: <your question> --> as the final non-empty line. Otherwise replace the skeleton with the real plan and continue. ` : `You stopped without declaring how this workflow ended. If work remains, continue it now. `;
|
|
1306
|
+
return bootstrapPreamble + `If everything is done and verified, write ${completionMarker} as the final non-empty line of the journal at ${journalPath}. If you cannot proceed without a human, write ${needsHumanMarker}: <reason> --> there instead. Do not stop again without either finishing the work or declaring one of these markers.`;
|
|
1243
1307
|
}
|
|
1244
1308
|
function estimateTokenCount(content) {
|
|
1245
1309
|
return Math.ceil(content.length / 4);
|
|
1246
1310
|
}
|
|
1247
|
-
function
|
|
1248
|
-
const where =
|
|
1249
|
-
return ` Separately: ${where} has crossed the ~8,000-token shedding backstop (ADR 0015 \xA73). Before continuing, cut the completed phases of the still-open unit out of the
|
|
1311
|
+
function buildJournalSizeNudgeSuffix(journalPath) {
|
|
1312
|
+
const where = journalPath ?? "the journal";
|
|
1313
|
+
return ` Separately: ${where} has crossed the ~8,000-token shedding backstop (ADR 0015 \xA73). Before continuing, cut the completed phases of the still-open unit out of the journal and paste them verbatim into that unit's record under units/<slug>.md, leaving a pointer row behind \u2014 this is a nudge, not a requirement, so continue the work either way.`;
|
|
1250
1314
|
}
|
|
1251
1315
|
var UNITS_HEADING_RE = /^##\s+Units\s*$/;
|
|
1252
1316
|
function splitTableRow(line) {
|
|
@@ -1284,6 +1348,55 @@ function parseUnitTable(content) {
|
|
|
1284
1348
|
}
|
|
1285
1349
|
return rows;
|
|
1286
1350
|
}
|
|
1351
|
+
function normaliseRecordPath(recordPath) {
|
|
1352
|
+
return recordPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1353
|
+
}
|
|
1354
|
+
var LEVEL_TWO_HEADING_RE = /^##\s+(.+?)\s*$/;
|
|
1355
|
+
function levelTwoHeadings(content) {
|
|
1356
|
+
const headings = /* @__PURE__ */ new Map();
|
|
1357
|
+
for (const line of content.split("\n")) {
|
|
1358
|
+
const match = LEVEL_TWO_HEADING_RE.exec(line.trim());
|
|
1359
|
+
if (match) {
|
|
1360
|
+
const heading = `## ${match[1]}`;
|
|
1361
|
+
const key = heading.toLowerCase();
|
|
1362
|
+
if (!headings.has(key)) headings.set(key, heading);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return headings;
|
|
1366
|
+
}
|
|
1367
|
+
function checkShedIntegrity(journalContent, records) {
|
|
1368
|
+
const rows = parseUnitTable(journalContent);
|
|
1369
|
+
if (!rows) return null;
|
|
1370
|
+
const pointed = new Set(rows.map((row) => normaliseRecordPath(row.recordPath)));
|
|
1371
|
+
const journalHeadings = levelTwoHeadings(journalContent);
|
|
1372
|
+
const orphanRecords = [];
|
|
1373
|
+
const sharedHeadings = [];
|
|
1374
|
+
for (const record of records) {
|
|
1375
|
+
const recordPath = normaliseRecordPath(record.recordPath);
|
|
1376
|
+
if (!pointed.has(recordPath)) orphanRecords.push(recordPath);
|
|
1377
|
+
for (const [key, heading] of levelTwoHeadings(record.content)) {
|
|
1378
|
+
if (journalHeadings.has(key)) {
|
|
1379
|
+
sharedHeadings.push({
|
|
1380
|
+
heading: journalHeadings.get(key) ?? heading,
|
|
1381
|
+
recordPath
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
if (orphanRecords.length === 0 && sharedHeadings.length === 0) return null;
|
|
1387
|
+
return { orphanRecords, sharedHeadings };
|
|
1388
|
+
}
|
|
1389
|
+
function buildShedIntegrityNudgeSuffix(gaps) {
|
|
1390
|
+
const findings = [
|
|
1391
|
+
...gaps.orphanRecords.map(
|
|
1392
|
+
(recordPath) => `unit record ${recordPath} has no row in the journal's ## Units table`
|
|
1393
|
+
),
|
|
1394
|
+
...gaps.sharedHeadings.map(
|
|
1395
|
+
(shared) => `heading "${shared.heading}" appears in both the journal and ${shared.recordPath}`
|
|
1396
|
+
)
|
|
1397
|
+
];
|
|
1398
|
+
return ` Separately: the Dossier shows a half-executed shed \u2014 ${findings.join("; ")}. Finish the shed: cut the section out of the journal, paste it verbatim into the record, and leave a pointer row in the ## Units table \u2014 this is a nudge, not a requirement, so continue the work either way.`;
|
|
1399
|
+
}
|
|
1287
1400
|
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/;
|
|
1288
1401
|
function parseUnitRecordFrontmatter(content) {
|
|
1289
1402
|
const match = FRONTMATTER_RE.exec(content);
|
|
@@ -1294,12 +1407,12 @@ function parseUnitRecordFrontmatter(content) {
|
|
|
1294
1407
|
if (raw !== "open" && raw !== "closed") return null;
|
|
1295
1408
|
return { status: raw };
|
|
1296
1409
|
}
|
|
1297
|
-
function
|
|
1298
|
-
const content =
|
|
1410
|
+
function projectJournalTasks(journalAbsPath) {
|
|
1411
|
+
const content = readJournal(journalAbsPath);
|
|
1299
1412
|
if (!content) return null;
|
|
1300
1413
|
const rows = parseUnitTable(content);
|
|
1301
1414
|
if (!rows) return null;
|
|
1302
|
-
const baseDir = path5.dirname(
|
|
1415
|
+
const baseDir = path5.dirname(journalAbsPath);
|
|
1303
1416
|
const tasks = [];
|
|
1304
1417
|
for (const row of rows) {
|
|
1305
1418
|
const recordAbsPath = path5.resolve(baseDir, row.recordPath);
|
|
@@ -1325,22 +1438,22 @@ function projectTrackerTasks(trackerAbsPath) {
|
|
|
1325
1438
|
}
|
|
1326
1439
|
|
|
1327
1440
|
// src/core/workflows/builtins/index.ts
|
|
1328
|
-
var
|
|
1329
|
-
var
|
|
1330
|
-
var SYSTEM_PROMPT = `You are working on a long-horizon task managed by Athena. A
|
|
1441
|
+
var DEFAULT_NEEDS_HUMAN_CLOSED_MARKER = `${DEFAULT_NEEDS_HUMAN_MARKER} -->`;
|
|
1442
|
+
var DEFAULT_NEEDS_HUMAN_REASON_MARKER = `${DEFAULT_NEEDS_HUMAN_MARKER}: reason -->`;
|
|
1443
|
+
var SYSTEM_PROMPT = `You are working on a long-horizon task managed by Athena. A journal file is used to persist progress across sessions.
|
|
1331
1444
|
|
|
1332
|
-
##
|
|
1445
|
+
## Journal File
|
|
1333
1446
|
|
|
1334
|
-
At the start of each session, read the
|
|
1447
|
+
At the start of each session, read the journal file if it exists. It contains the task plan, completed steps, and current status from prior sessions.
|
|
1335
1448
|
|
|
1336
|
-
If no
|
|
1449
|
+
If no journal file exists, create one by:
|
|
1337
1450
|
1. Analyzing the user's request to understand the full scope
|
|
1338
1451
|
2. Breaking the task into concrete, actionable steps
|
|
1339
|
-
3. Writing the plan to the
|
|
1452
|
+
3. Writing the plan to the journal file
|
|
1340
1453
|
|
|
1341
|
-
###
|
|
1454
|
+
### Journal Format
|
|
1342
1455
|
|
|
1343
|
-
Use this markdown format for the
|
|
1456
|
+
Use this markdown format for the journal:
|
|
1344
1457
|
|
|
1345
1458
|
\`\`\`
|
|
1346
1459
|
# Task: <one-line summary>
|
|
@@ -1358,9 +1471,9 @@ Use this markdown format for the tracker:
|
|
|
1358
1471
|
<any important context, decisions, or blockers discovered along the way>
|
|
1359
1472
|
\`\`\`
|
|
1360
1473
|
|
|
1361
|
-
### Updating the
|
|
1474
|
+
### Updating the Journal
|
|
1362
1475
|
|
|
1363
|
-
After completing meaningful work, update the
|
|
1476
|
+
After completing meaningful work, update the journal:
|
|
1364
1477
|
- Check off completed steps
|
|
1365
1478
|
- Update the current status section
|
|
1366
1479
|
- Add any important notes or decisions
|
|
@@ -1368,18 +1481,18 @@ After completing meaningful work, update the tracker:
|
|
|
1368
1481
|
### Completion
|
|
1369
1482
|
|
|
1370
1483
|
When all steps are complete:
|
|
1371
|
-
1. Update the
|
|
1484
|
+
1. Update the journal with all steps checked off
|
|
1372
1485
|
2. Put any final summary or outcome notes above the terminal marker
|
|
1373
|
-
3. Add \`${DEFAULT_COMPLETION_MARKER}\` as the final non-empty line of the
|
|
1374
|
-
4. Do not write any
|
|
1486
|
+
3. Add \`${DEFAULT_COMPLETION_MARKER}\` as the final non-empty line of the journal file
|
|
1487
|
+
4. Do not write any journal content after the terminal marker
|
|
1375
1488
|
|
|
1376
|
-
###
|
|
1489
|
+
### Needs a human
|
|
1377
1490
|
|
|
1378
|
-
If you
|
|
1379
|
-
1. Document what
|
|
1491
|
+
If you cannot proceed without a person \u2014 a question only they can answer, or an external blocker only they can clear:
|
|
1492
|
+
1. Document what you need from them in the Notes section
|
|
1380
1493
|
2. Explain what needs to happen to unblock the task whenever possible above the terminal marker
|
|
1381
|
-
3. Add \`${
|
|
1382
|
-
4. Do not write any
|
|
1494
|
+
3. Add \`${DEFAULT_NEEDS_HUMAN_CLOSED_MARKER}\` or \`${DEFAULT_NEEDS_HUMAN_REASON_MARKER}\` as the final non-empty line of the journal file
|
|
1495
|
+
4. Do not write any journal content after the terminal marker
|
|
1383
1496
|
`;
|
|
1384
1497
|
function ensureSystemPromptFile() {
|
|
1385
1498
|
const dir = path6.join(
|
|
@@ -1407,7 +1520,7 @@ function resolveBuiltinWorkflow(name) {
|
|
|
1407
1520
|
loop: {
|
|
1408
1521
|
enabled: true,
|
|
1409
1522
|
completionMarker: DEFAULT_COMPLETION_MARKER,
|
|
1410
|
-
|
|
1523
|
+
needsHumanMarker: DEFAULT_NEEDS_HUMAN_MARKER,
|
|
1411
1524
|
maxIterations: 20
|
|
1412
1525
|
},
|
|
1413
1526
|
plugins: [],
|
|
@@ -1501,9 +1614,12 @@ function writeWorkflowSourceMetadata(workflowDir, metadata) {
|
|
|
1501
1614
|
}
|
|
1502
1615
|
|
|
1503
1616
|
// src/core/workflows/registry.ts
|
|
1504
|
-
function
|
|
1617
|
+
function workflowRegistryDir() {
|
|
1505
1618
|
return path8.join(os5.homedir(), ".config", "athena", "workflows");
|
|
1506
1619
|
}
|
|
1620
|
+
function registryDir() {
|
|
1621
|
+
return workflowRegistryDir();
|
|
1622
|
+
}
|
|
1507
1623
|
function ensurePathWithinRoot(rootDir, targetPath, label) {
|
|
1508
1624
|
const relative = path8.relative(rootDir, targetPath);
|
|
1509
1625
|
if (relative === "" || !relative.startsWith("..") && !path8.isAbsolute(relative)) {
|
|
@@ -1558,6 +1674,13 @@ function resolveWorkflow(name) {
|
|
|
1558
1674
|
`Invalid workflow.json: "examplePrompts" must be an array of strings`
|
|
1559
1675
|
);
|
|
1560
1676
|
}
|
|
1677
|
+
if (raw["askRules"] !== void 0 && (!Array.isArray(raw["askRules"]) || !raw["askRules"].every(
|
|
1678
|
+
(e) => typeof e === "string" && e.trim().length > 0
|
|
1679
|
+
))) {
|
|
1680
|
+
throw new Error(
|
|
1681
|
+
`Invalid workflow.json: "askRules" must be an array of non-empty tool-name patterns`
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1561
1684
|
if (typeof raw["workflowFile"] !== "string" || raw["workflowFile"].length === 0) {
|
|
1562
1685
|
throw new Error(
|
|
1563
1686
|
'Invalid workflow.json: "workflowFile" is required and must point to the workflow instructions file'
|
|
@@ -1790,10 +1913,11 @@ function compileWorkflowPlan(input) {
|
|
|
1790
1913
|
|
|
1791
1914
|
// src/core/workflows/sessionPlan.ts
|
|
1792
1915
|
import fs10 from "fs";
|
|
1916
|
+
import crypto from "crypto";
|
|
1793
1917
|
import path9 from "path";
|
|
1794
1918
|
|
|
1795
1919
|
// src/core/workflows/stateMachine.md
|
|
1796
|
-
var stateMachine_default = "# Turn Protocol\n\nYou run inside a managed workflow loop. **Run until the work is done.** The Dossier \u2014 `.athena/<session_id>/` \u2014 is your durable memory; `tracker.md` is your index into it: read it, work, write it as you go. Your conversation context usually survives between stops \u2014 the runner resumes your session rather than restarting it \u2014 but never rely on that: the runner may kill a long Turn, your session may be replaced at a context bound, the process may die mid-task. Anything not in the Dossier can be lost.\n\nTwo kinds of Turn exist, and you should know which you are in:\n\n- **A fresh Turn** \u2014 the first Turn of the run, or the Turn right after a Handover (the runner's context reset). You start with no memory of prior work; the tracker (and, after a Handover, the Handoff file) is all you have.\n- **A resumed Turn** \u2014 the runner continued your existing session with a new instruction (a corrective nudge, a retry after a transient failure, or a human's reply). Your context is intact; act on the new instruction and keep going.\n\n## First action, in a fresh Turn\n\n1. Read the tracker at the configured path (default: `.athena/<session_id>/tracker.md`). The runner provides the session ID \u2014 do not invent one.\n2. If the tracker contains `<!-- TRACKER_SKELETON -->` \u2192 this is Turn 1, run [**Orient**](#orient-turn-1).\n3. Otherwise \u2192 this is a continuation, run [**Execute**](#execute-continuation) from where the tracker says, not from the start of the flow.\n4. If the runner's prompt names a Handoff file, read it too \u2014 it's mandatory alongside the tracker: it carries the in-flight context the tracker never checkpointed. Before any domain work, fold whatever in it is still durable into the tracker (or the open unit's record, if it's been shed) \u2014 the Handoff is a one-time relay, not a permanent Dossier file, so anything worth keeping has to land in `tracker.md`/`units/<slug>.md` now or it is lost once the Handoff falls off the chain. Then read whatever the tracker's fifth question names (see [Tracker contract](#tracker-contract)) \u2014 that, the tracker, and any named Handoff file are your complete required reading. Do not redo completed work or re-litigate decisions any of them record.\n\nReading first prevents two failure modes that waste whole Turns: redoing work already done, or contradicting decisions a prior Turn made.\n\nIn a resumed Turn, your context is already loaded \u2014 skim the tracker only if you have any doubt it still matches reality, then continue.\n\n## Tracker contract\n\nThe tracker must always answer five questions:\n\n1. What are we trying to accomplish?\n2. What has been done?\n3. What's left?\n4. What should the next Turn do first?\n5. What else, beyond this file, must the next Turn read to have full context?\n\nA fresh Turn has no other context. If something isn't here or in a file question 5 names, it doesn't exist. Section headings may vary by workflow, but these five answers must be explicit and easy to find. Question 5 is the tracker's pointer into the rest of the Dossier (see [The Dossier](#the-dossier)): on a single-file Run its honest answer is \"nothing else\"; once anything has been shed, it names exactly which files, so a fresh Turn's opening read stays small even when the Dossier has grown.\n\n### Terminal markers\n\nDefault markers (workflows may override \u2014 use the markers configured for the active workflow):\n\n- `<!-- WORKFLOW_COMPLETE -->` \u2014 all work done and verified\n- `<!-- WORKFLOW_BLOCKED -->` or `<!-- WORKFLOW_BLOCKED: reason -->` \u2014 you need a human: a question only they can answer, or an external blocker only they can clear\n\nRules:\n\n- Only the last non-empty line of the tracker is authoritative. Marker-like text in notes, examples, or quoted instructions earlier in the file is ignored.\n- When you write a terminal marker, it must be the final non-empty line of the tracker. Put every summary, status note, and next-step sentence before the marker. Never append prose after it.\n- The runner trusts markers unconditionally. A premature `WORKFLOW_COMPLETE` ends the run with no automatic recovery \u2014 write it only when its criteria are fully met.\n- Include a concrete reason after `WORKFLOW_BLOCKED:` whenever possible \u2014 it is what the human sees when deciding how to answer you. Put the full question there.\n\n## The Dossier\n\n`.athena/<session_id>/` is the Dossier: the tracker plus everywhere it can shed cold detail once shedding earns its keep.\n\n```\ntracker.md state: index, open loops, next action, terminal marker\nunits/<slug>.md journal: contract, problem, design + rationale, build state, gate evidence\norientation.md cross-unit knowledge, revised in place\nhandoff/NNN.md episodic distillation, written at a Handover (chain, newest is mandatory reading)\n```\n\n**On most Runs the Dossier never grows past `tracker.md` itself** \u2014 one file, no ceremony, no extra directories, nothing below this line to act on. Nothing here changes how a short, single-unit Run works.\n\nA **unit** is a bounded piece of the plan that reaches its own closed state while the Run keeps going \u2014 one issue in a multi-issue epic, one phase of a larger goal. A Run with only one unit never closes a unit while another stays open, so it can never trip the first shed trigger below.\n\n### When to shed\n\nTwo triggers, both structural. A single-unit Run can only ever reach the second, and only once that unit has outgrown the bound:\n\n- **A unit closes while another is still open.** Cut that unit's whole detail out of the tracker now, before starting the next one.\n- **The tracker crosses ~8,000 tokens** (roughly 32,000 characters \u2014 a token is about 4 characters at this codebase's measured rate). This is a backstop for one long single unit, not a target to design toward \u2014 shed the completed phases of the still-open unit.\n\n### How to shed\n\nShedding is three positive acts on one whole named `##` section \u2014 never a summary, never a partial move:\n\n1. **Cut** the section, verbatim, out of `tracker.md`.\n2. **Paste** it, verbatim, into `units/<slug>.md` (create the file on that unit's first shed).\n3. **Pointer** \u2014 leave one index row in the tracker's unit table with a relative path to where the detail went.\n\nContent is demoted, never deleted. If you find yourself rewording or condensing while moving text, stop \u2014 that is not shedding, that is exactly where fidelity leaks.\n\n### The unit table\n\nThe tracker's unit table is the index the runner reads to keep the CLI's own task list in sync with your plan \u2014 it is parsed by tooling, so its shape is fixed, not free-form prose. Keep it under a `## Units` heading, as a GFM table with exactly two columns:\n\n```\n## Units\n\n| Unit | Record |\n| ---------------------- | --------------------------- |\n| Add the size nudge | units/size-nudge.md |\n| Wire up task projection | units/task-projection.md |\n```\n\n- **Unit** \u2014 a short human-readable label for the unit (what step 3 of [How to shed](#how-to-shed) points at).\n- **Record** \u2014 the path to that unit's `units/<slug>.md`, relative to the tracker's own directory.\n\nOne row per unit that has been shed; units still fully inline in the tracker (never shed) don't need a row. A row whose Record file is missing, unreadable, or malformed is simply skipped by the runner \u2014 never guessed at, never a failure (see the next section). Malformed rows in an otherwise-fine table don't invalidate the rest of the table.\n\nEach `units/<slug>.md` record opens with a small YAML frontmatter block the runner reads to know the unit's state:\n\n```\n---\nstatus: open\ngates: []\n---\n\n<the shed section content, verbatim>\n```\n\n- `status` \u2014 `open` or `closed`, exactly (case-insensitive). Any other value, or a missing `status` key, means the runner treats the whole record as unparseable for projection purposes \u2014 it still exists as your durable journal, it just doesn't surface in the task list.\n- `gates` \u2014 reserved for future gate-evidence tracking; not read by anything yet. Leave it present but empty, or omit it \u2014 either is fine today.\n\nThis frontmatter is the only structured part of a unit record; everything after the closing `---` is free-form prose exactly like the rest of the Dossier.\n\n### Parsing is best-effort, never a gate\n\nThe unit table and unit-record frontmatter exist so the runner can mirror your plan into the harness's own task list (see [Task UI projection](#task-ui-projection)) and, separately, so it can nudge you when the tracker grows past its size backstop. Both are conveniences layered on top of the Dossier, not requirements on it:\n\n- A missing table, an extra column, a typo'd status, a unit record that fails to parse \u2014 none of it fails your Turn, and none of it is worth stopping to fix for the runner's sake. The runner silently skips whatever it can't parse and, at most, folds a size nudge into your next prompt.\n- Never restructure the tracker or a unit record just to make automated parsing happy at the expense of the content itself. If the table and the prose disagree, the prose (what you actually did) is the truth.\n\n### orientation.md\n\nKnowledge that spans more than one unit lives here, revised in place instead of appended to \u2014 the one Dossier surface you edit rather than move things into. Two sections with different rules:\n\n- **Established** \u2014 decisions. Never decay; only explicitly superseded (\"we chose X over Y because Z \u2014 superseded 2026-09-03: \u2026\").\n- **Observed** \u2014 facts carrying a `file.ts:123`-style anchor. Revalidate the anchor before relying on it; code moves.\n\nA correction is an edit to the existing entry, not a new capitalized warning stacked on top of stale text.\n\n## Run until done; declare when blocked\n\nThe loop's contract with you:\n\n- **Do not stop early.** There is no checkpoint budget and no reason to end a Turn \"to be safe\" \u2014 context refresh is the runner's job, not yours. When your context approaches its bound the runner performs a **Handover**: your conversation is distilled into a Handoff file and a fresh session picks up seamlessly from it plus the tracker. You will not see this happen; just keep the tracker current so nothing is lost.\n- **Stopping without a marker is a mistake**, not a signal. The runner reads it as a premature stop and resumes you with a corrective prompt; repeated markerless stops without tracker progress escalate to a human. Never stop as a way of asking \"should I continue?\" \u2014 the answer is always to continue or to declare.\n- **Need a human? Declare it.** Write `WORKFLOW_BLOCKED: <your question or blocker>` as the tracker's final non-empty line and end. The run suspends until a human replies; their reply resumes your session with the answer. This is the only correct way to wait for a person \u2014 an interactive question asked into an unattended run cannot be answered.\n- Transient infrastructure failures are not yours to manage: the runner retries them by resuming your session. Just make sure the tracker reflects reality before risky operations.\n\n## Phases\n\n### Orient (Turn 1)\n\n1. **Replace the skeleton immediately**, before any domain work. Even a three-line tracker (goal + \"orienting\") protects you if the Turn dies during setup.\n2. Identify and load the applicable workflow skills before doing domain work. If a workflow, plugin, or local skill table names a relevant skill, read it fully and follow it. Do not assume you already know the workflow's conventions, tool sequence, quality gates, or implementation details.\n3. Use a dedicated git worktree for repository-changing work. If you are not already inside a task-specific worktree, create or enter one before editing files, record its branch/path in the tracker, and continue there. Skip this only when the workflow explicitly forbids it or the task is read-only.\n4. Run the workflow's orientation steps exactly as written. These vary by domain \u2014 a test-writing workflow explores the product in a browser; a migration workflow audits the schema. The workflow defines what orientation means. Do not skip, reorder, reinterpret, or replace workflow steps with a generic approach unless the workflow explicitly allows it or the tracker records a concrete blocker that makes the written step impossible.\n5. Refine the tracker into a granular plan. Each task a concrete, verifiable unit of work, including verification steps (running checks, reviewing output) \u2014 not just implementation. Vague tasks (\"write tests\") cannot be meaningfully resumed by a future Turn that has no idea what they mean here.\n6. Record concrete observations \u2014 what you actually saw, not what you assumed. Wrong assumptions burn entire future Turns on rework.\n7. **Single-Turn requests still go through this phase.** If the entire request is satisfied quickly, write a minimal tracker (what was asked, what was done, the outcome) and append `<!-- WORKFLOW_COMPLETE -->`. Leaving the skeleton in place gets you nudged \u2014 the runner cannot trust work it can't read from the tracker \u2014 and repeated stops with an untouched skeleton escalate to a human.\n\n### Execute (continuation)\n\n- Work from where the tracker says, in the workflow's prescribed sequence, and keep going until the work is done or a declared blocker stops you.\n- Be strict with workflow steps. Before starting each unit, identify the next required workflow step from the workflow document and tracker, follow it as written, and record completion or blockers against that step. Do not substitute your own process, collapse separate gates into one, or advance past an unchecked step.\n- Be strict with skills. Before each new activity, check the workflow, plugin metadata, local skill table, and tracker for relevant skills. Load the appropriate skill first, read it completely, and follow its instructions. If no skill applies, record that explicitly in the tracker before proceeding. Skills carry the implementation detail (scaffolding steps, locator rules, anti-patterns, code templates) that this protocol intentionally doesn't repeat.\n- Keep repository work inside the recorded git worktree. If a continuation starts outside the recorded worktree, enter it before editing. If no worktree is recorded and edits are still required, create or enter one before proceeding.\n- Delegate heavy exploration or generation to subagents via the Task tool. Pass file paths, conventions, and concrete output expectations; tell them which skill to load. Respect the workflow's **delegation constraints** \u2014 some operations must run in the main agent because their output is proof, or because the main agent needs to interpret results in context.\n- Run quality gates in order. Do not skip \u2014 they exist because skipping cascades into rework. On a failing verdict, address the issues and re-run before proceeding. Respect the workflow's **retry limits**: repeated failure usually signals a deeper issue another retry won't fix.\n\n### End\n\nYou end the run only by declaring:\n\n1. Tracker reflects all progress, discoveries, and blockers.\n2. Tracker says clearly what a fresh Turn would need to do first (a Handover can happen at any time).\n3. If all work is verified: append the completion marker as the final non-empty line.\n4. If a human is needed to proceed: append the blocked marker as the final non-empty line, with the question or blocker spelled out as the reason.\n\n## When to write the tracker\n\nWrite on **concrete triggers**, not on a vague sense of \"meaningful progress.\" The right cadence sits between every-tool-call (noisy log, wastes tokens) and end-of-run (everything lost if you die mid-task). This matters more, not less, now that Turns run long: the tracker (plus the Handoff file at a Handover) is what carries a killed or reset session.\n\n- **Discrete unit done** \u2014 file written, fix applied, test run, gate passed. Reflect the new reality before starting the next unit. If another unit is still open, this is also a shed trigger (see [The Dossier](#the-dossier)): cut this unit's detail into `units/<slug>.md` before you start the next one.\n- **Insight learned** \u2014 API quirk, config field that turned out to matter, dead end ruled out, decision between two approaches. Insights are tracker-worthy even when no code changed; rediscovering them costs a future Turn a full re-exploration. The tracker is a knowledge ledger, not just a task log. Insight that spans more than one unit belongs in `orientation.md`, not the tracker.\n- **About to do something risky or long-running** \u2014 subagent dispatch, long build, flaky external call, large refactor. Write _first_, then act. If the operation kills your Turn, only what's on disk survives.\n- **Plan changed** \u2014 task resequenced, new task surfaced, planned task no longer needed. Stale plans poison continuation Turns.\n- **The tracker crosses the ~8,000-token backstop** \u2014 shed the completed phases of the still-open unit into its `units/<slug>.md` record now, mid-unit; don't wait for it to close.\n- **You haven't written in a while** \u2014 if you can't remember the last update, you've gone too long. A short defensive update (\"doing X, last completed Y, next is Z\") beats nothing.\n\nEach update covers: what changed (work or knowledge), what's now next, and any caveat a future Turn needs. Don't transcribe tool calls \u2014 the tracker is a contract with your future self, not a replay log.\n\nThe cost of one extra tracker update is a few tokens. The cost of dying without one is rework. Bias toward writing.\n\n## Task UI projection\n\nThe tracker is the durable source of truth. Your harness's task tools are a session-scoped UI projection of the same plan, shown to the user in their CLI widget. They do not survive process exit.\n\n{{TASK_TOOL_INSTRUCTIONS}}\n\n- **Turn 1, after orientation:** project the tracker's task plan into the task tools.\n- **In a fresh continuation (e.g. after a Handover):** recreate the projection from the tracker; do not assume task IDs from prior sessions still exist.\n- **During work:** update both \u2014 the task tools for immediate UI feedback, the tracker for persistence \u2014 in the same working phase.\n\nSeparately, the runner independently re-derives a task list from the tracker's [unit table](#the-unit-table) and each unit record's frontmatter after every Turn, and mirrors it into the CLI's own task display. This is a backstop, not a substitute for the above \u2014 it only ever reaches `open`/`closed`, so keep calling the task tools yourself for anything finer-grained. It never blocks or fails a Turn: an unparseable table or record just means that Turn's mirror is skipped, exactly as described in [Parsing is best-effort, never a gate](#parsing-is-best-effort-never-a-gate).\n\n## Quick reference\n\n- [ ] Fresh Turn: read the tracker (and any named Handoff file) before doing anything else\n- [ ] Replace the skeleton immediately, even for single-Turn requests\n- [ ] Run until the work is done \u2014 do not stop at checkpoints, and never stop as a way of asking permission to continue\n- [ ] Need a human? Declare it: `WORKFLOW_BLOCKED: <question>` as the final non-empty line, then end\n- [ ] Update the tracker on concrete triggers \u2014 unit done, insight learned, risky op pending, plan changed\n- [ ] Shed a unit's detail into `units/<slug>.md` the moment it closes while another stays open, or the tracker crosses ~8,000 tokens \u2014 cut, paste, pointer, never summarize\n- [ ] Keep the unit table and each shed record's `status: open|closed` frontmatter current \u2014 the runner mirrors them into the task list and skips silently on any parse miss\n- [ ] After a Handover: fold the Handoff's durable content into the tracker or open unit record before any domain work\n- [ ] Project the tracker plan into task tools at session start; keep both in sync as work lands\n- [ ] Follow the workflow steps as written; do not skip, reorder, or substitute your own process\n- [ ] Load the appropriate skill before each activity; do not rely on assumed knowledge\n- [ ] Use and record a dedicated git worktree for repository-changing work\n- [ ] Run quality gates in order; respect delegation constraints and retry limits\n- [ ] Write the completion marker only when all work is verified, and make it the final non-empty line\n";
|
|
1920
|
+
var stateMachine_default = "# Turn Protocol\n\nYou run inside a managed workflow loop. **Run until the work is done.** The Dossier \u2014 `.athena/<session_id>/` \u2014 is your durable memory; `journal.md` is your index into it: read it, work, write it as you go. Your conversation context usually survives between stops \u2014 the runner resumes your session rather than restarting it \u2014 but never rely on that: the runner may kill a long Turn, your session may be replaced at a context bound, the process may die mid-task. Anything not in the Dossier can be lost.\n\nTwo kinds of Turn exist, and you should know which you are in:\n\n- **A fresh Turn** \u2014 the first Turn of the run, or the Turn right after a Handover (the runner's context reset). You start with no memory of prior work; the Journal checkpoint is your saved state.\n- **A resumed Turn** \u2014 the runner continued your existing session with a new instruction (a corrective nudge, a retry after a transient failure, a human's reply, or a human steer). Your context is intact; act on the new instruction and keep going.\n\nEither kind of Turn may open with a **human steer**: a block delimited by `=== HUMAN STEER \u2026 ===` and `=== END HUMAN STEER ===`, carrying instructions a human sent into the run while it was in progress (several are numbered in arrival order). Read it before you plan. Where it conflicts with the journal's planned next action, the steer wins; note what it changed in the journal and continue. The runner has already recorded the steer itself in the journal, with its origin and the Turn it reached.\n\n## First action, in a fresh Turn\n\n1. If the seed prompt contains a validated **Restart contract**, execute its next action using the objective, constraints, changes, open questions and references it carries. Read additional Journal and Unit Record sections selectively when needed. Do not read the whole Journal as an opening ritual.\n2. Otherwise read the journal at the configured path (default: `.athena/<session_id>/journal.md`). The runner provides the session ID \u2014 do not invent one.\n3. If it contains `<!-- JOURNAL_SKELETON -->`, run [**Orient**](#orient-turn-1). Otherwise run [**Execute**](#execute-continuation) from its saved next action.\n\nUpdate durable state only when it changes; never append checkpoint-processed notes. The Runner may suspend if the restart contract cannot fit its context allowance.\n\nIn a resumed Turn, your context is already loaded \u2014 skim the journal only if you have any doubt it still matches reality, then continue.\n\n## Journal contract\n\nThe journal must always answer five questions:\n\n1. What are we trying to accomplish?\n2. What has been done?\n3. What's left?\n4. What should the next Turn do first?\n5. What else, beyond this file, must the next Turn read to have full context?\n\nA fresh Turn has no other context. If something isn't here or in a file question 5 names, it doesn't exist. Section headings may vary by workflow, but these five answers must be explicit and easy to find. Question 5 is the journal's pointer into the rest of the Dossier (see [The Dossier](#the-dossier)): on a single-file Run its honest answer is \"nothing else\"; once anything has been shed, it names exactly which files, so a fresh Turn's opening read stays small even when the Dossier has grown.\n\n### Terminal markers\n\nDefault markers (workflows may override \u2014 use the markers configured for the active workflow):\n\n- `<!-- WORKFLOW_COMPLETE -->` \u2014 all work done and verified\n- `<!-- NEEDS_HUMAN -->` or `<!-- NEEDS_HUMAN: reason -->` \u2014 you need a human: a question only they can answer, or an external blocker only they can clear\n\nRules:\n\n- Only the last non-empty line of the journal is authoritative. Marker-like text in notes, examples, or quoted instructions earlier in the file is ignored.\n- When you write a terminal marker, it must be the final non-empty line of the journal. Put every summary, status note, and next-step sentence before the marker. Never append prose after it.\n- The runner trusts markers unconditionally. A premature `WORKFLOW_COMPLETE` ends the run with no automatic recovery \u2014 write it only when its criteria are fully met.\n- Include a concrete reason after `NEEDS_HUMAN:` whenever possible \u2014 it is what the human sees when deciding how to answer you. Put the full question there.\n\n### Step block\n\nWhen the workflow has named steps \u2014 phases, gates, stages, whatever the workflow document calls them \u2014 keep one Turn Protocol block in the journal naming the step you are on, revised in place as you move:\n\n```\n<!-- TURN_PROTOCOL\nstep: Build\nstep_index: 3\nstep_total: 5\n-->\n```\n\n`step` is the step's name as the workflow writes it. `step_index` and `step_total` are optional, 1-based, and `step_index` never exceeds `step_total`. The runner reads the block after every Turn and shows the step in the feed as the run's progress; it never changes what you do. Leave the block out when the workflow has no named steps. A block the runner cannot read is ignored with a warning \u2014 it never fails a Turn.\n\n## The Dossier\n\n`.athena/<session_id>/` is the Dossier: the journal plus everywhere it can shed cold detail once shedding earns its keep.\n\n```\njournal.md state: index, open loops, next action, terminal marker\nunits/<slug>.md record: contract, problem, design + rationale, build state, gate evidence\norientation.md cross-unit knowledge, revised in place\n```\n\n**On most Runs the Dossier never grows past `journal.md` itself** \u2014 one file, no ceremony, no extra directories, nothing below this line to act on. Nothing here changes how a short, single-unit Run works.\n\nA **unit** is a bounded piece of the plan that reaches its own closed state while the Run keeps going \u2014 one issue in a multi-issue epic, one phase of a larger goal. A Run with only one unit never closes a unit while another stays open, so it can never trip the first shed trigger below.\n\n### When to shed\n\nTwo triggers, both structural. A single-unit Run can only ever reach the second, and only once that unit has outgrown the bound:\n\n- **A unit closes while another is still open.** Cut that unit's whole detail out of the journal now, before starting the next one.\n- **The journal crosses ~8,000 tokens** (roughly 32,000 characters \u2014 a token is about 4 characters at this codebase's measured rate). This is a backstop for one long single unit, not a target to design toward \u2014 shed the completed phases of the still-open unit.\n\n### How to shed\n\nShedding is three positive acts on one whole named `##` section \u2014 never a summary, never a partial move:\n\n1. **Cut** the section, verbatim, out of `journal.md`.\n2. **Paste** it, verbatim, into `units/<slug>.md` (create the file on that unit's first shed).\n3. **Pointer** \u2014 leave one index row in the journal's unit table with a relative path to where the detail went.\n\nContent is demoted, never deleted. If you find yourself rewording or condensing while moving text, stop \u2014 that is not shedding, that is exactly where fidelity leaks.\n\n### The unit table\n\nThe journal's unit table is the index the runner reads to keep the CLI's own task list in sync with your plan \u2014 it is parsed by tooling, so its shape is fixed, not free-form prose. Keep it under a `## Units` heading, as a GFM table with exactly two columns:\n\n```\n## Units\n\n| Unit | Record |\n| ---------------------- | --------------------------- |\n| Add the size nudge | units/size-nudge.md |\n| Wire up task projection | units/task-projection.md |\n```\n\n- **Unit** \u2014 a short human-readable label for the unit (what step 3 of [How to shed](#how-to-shed) points at).\n- **Record** \u2014 the path to that unit's `units/<slug>.md`, relative to the journal's own directory.\n\nOne row per unit that has been shed; units still fully inline in the journal (never shed) don't need a row. A row whose Record file is missing, unreadable, or malformed is simply skipped by the runner \u2014 never guessed at, never a failure (see the next section). Malformed rows in an otherwise-fine table don't invalidate the rest of the table.\n\nEach `units/<slug>.md` record opens with a small YAML frontmatter block the runner reads to know the unit's state:\n\n```\n---\nstatus: open\ngates: []\n---\n\n<the shed section content, verbatim>\n```\n\n- `status` \u2014 `open` or `closed`, exactly (case-insensitive). Any other value, or a missing `status` key, means the runner treats the whole record as unparseable for projection purposes \u2014 it still exists as your durable journal, it just doesn't surface in the task list.\n- `gates` \u2014 reserved for future gate-evidence tracking; not read by anything yet. Leave it present but empty, or omit it \u2014 either is fine today.\n\nThis frontmatter is the only structured part of a unit record; everything after the closing `---` is free-form prose exactly like the rest of the Dossier.\n\n### Parsing is best-effort, never a gate\n\nThe unit table and unit-record frontmatter exist so the runner can mirror your plan into the harness's own task list (see [Task UI projection](#task-ui-projection)) and, separately, so it can nudge you when the journal grows past its size backstop. Both are conveniences layered on top of the Dossier, not requirements on it:\n\n- A missing table, an extra column, a typo'd status, a unit record that fails to parse \u2014 none of it fails your Turn, and none of it is worth stopping to fix for the runner's sake. The runner silently skips whatever it can't parse and, at most, folds a nudge into your next prompt: a size nudge when the journal is over its backstop, or a shed-integrity nudge when a shed was left half done \u2014 a `units/*.md` record with no row in the `## Units` table, or a `##` heading present in both the journal and a record. Finish the shed (cut, paste, pointer) and carry on.\n- Never restructure the journal or a unit record just to make automated parsing happy at the expense of the content itself. If the table and the prose disagree, the prose (what you actually did) is the truth.\n\n### orientation.md\n\nKnowledge that spans more than one unit lives here, revised in place instead of appended to \u2014 the one Dossier surface you edit rather than move things into. Two sections with different rules:\n\n- **Established** \u2014 decisions. Never decay; only explicitly superseded (\"we chose X over Y because Z \u2014 superseded 2026-09-03: \u2026\").\n- **Observed** \u2014 facts carrying a `file.ts:123`-style anchor. Revalidate the anchor before relying on it; code moves.\n\nA correction is an edit to the existing entry, not a new capitalized warning stacked on top of stale text.\n\n## Run until done; declare when you need a human\n\nThe loop's contract with you:\n\n- **Do not stop early.** There is no checkpoint budget and no reason to end a Turn \"to be safe\" \u2014 context refresh is the runner's job, not yours. When your context approaches its bound the runner performs a **Handover**: a fresh session resumes from the bounded `## Restart` section in your Journal; missing or stale state pauses the Run. You will not see this happen; just keep the journal current so nothing is lost.\n- **Stopping without a marker is a mistake**, not a signal. The runner reads it as a premature stop and resumes you with a corrective prompt; repeated markerless stops without journal progress escalate to a human. Never stop as a way of asking \"should I continue?\" \u2014 the answer is always to continue or to declare.\n- **Need a human? Declare it.** Write `NEEDS_HUMAN: <your question or blocker>` as the journal's final non-empty line and end. The run suspends until a human replies; their reply resumes your session with the answer. This is the only correct way to wait for a person \u2014 an interactive question asked into an unattended run cannot be answered.\n- Transient infrastructure failures are not yours to manage: the runner retries them by resuming your session. Just make sure the journal reflects reality before risky operations.\n\n## Phases\n\n### Orient (Turn 1)\n\n1. **Replace the skeleton immediately**, before any domain work. Even a three-line journal (goal + \"orienting\") protects you if the Turn dies during setup.\n2. Identify and load the applicable workflow skills before doing domain work. If a workflow, plugin, or local skill table names a relevant skill, read it fully and follow it. Do not assume you already know the workflow's conventions, tool sequence, quality gates, or implementation details.\n3. Use a dedicated git worktree for repository-changing work. If you are not already inside a task-specific worktree, create or enter one before editing files, record its branch/path in the journal, and continue there. Skip this only when the workflow explicitly forbids it or the task is read-only.\n4. Run the workflow's orientation steps exactly as written. These vary by domain \u2014 a test-writing workflow explores the product in a browser; a migration workflow audits the schema. The workflow defines what orientation means. Do not skip, reorder, reinterpret, or replace workflow steps with a generic approach unless the workflow explicitly allows it or the journal records a concrete blocker that makes the written step impossible.\n5. Refine the journal into a granular plan. Each task a concrete, verifiable unit of work, including verification steps (running checks, reviewing output) \u2014 not just implementation. Vague tasks (\"write tests\") cannot be meaningfully resumed by a future Turn that has no idea what they mean here.\n6. Record concrete observations \u2014 what you actually saw, not what you assumed. Wrong assumptions burn entire future Turns on rework.\n7. **Single-Turn requests still go through this phase.** If the entire request is satisfied quickly, write a minimal journal (what was asked, what was done, the outcome) and append `<!-- WORKFLOW_COMPLETE -->`. Leaving the skeleton in place gets you nudged \u2014 the runner cannot trust work it can't read from the journal \u2014 and repeated stops with an untouched skeleton escalate to a human.\n\n### Execute (continuation)\n\n- Work from where the journal says, in the workflow's prescribed sequence, and keep going until the work is done or a declared blocker stops you.\n- Be strict with workflow steps. Before starting each unit, identify the next required workflow step from the workflow document and journal, follow it as written, and record completion or blockers against that step. Do not substitute your own process, collapse separate gates into one, or advance past an unchecked step.\n- Be strict with skills. Before each new activity, check the workflow, plugin metadata, local skill table, and journal for relevant skills. Load the appropriate skill first, read it completely, and follow its instructions. If no skill applies, record that explicitly in the journal before proceeding. Skills carry the implementation detail (scaffolding steps, locator rules, anti-patterns, code templates) that this protocol intentionally doesn't repeat.\n- Keep repository work inside the recorded git worktree. If a continuation starts outside the recorded worktree, enter it before editing. If no worktree is recorded and edits are still required, create or enter one before proceeding.\n- Delegate heavy exploration or generation to subagents via the Task tool. Pass file paths, conventions, and concrete output expectations; tell them which skill to load. Respect the workflow's **delegation constraints** \u2014 some operations must run in the main agent because their output is proof, or because the main agent needs to interpret results in context.\n- Run quality gates in order. Do not skip \u2014 they exist because skipping cascades into rework. On a failing verdict, address the issues and re-run before proceeding. Respect the workflow's **retry limits**: repeated failure usually signals a deeper issue another retry won't fix.\n\n### End\n\nYou end the run only by declaring:\n\n1. Journal reflects all progress, discoveries, and blockers.\n2. Journal says clearly what a fresh Turn would need to do first (a Handover can happen at any time).\n3. If all work is verified: append the completion marker as the final non-empty line.\n4. If a human is needed to proceed: append the needs-human marker as the final non-empty line, with the question or blocker spelled out as the reason.\n\n## When to write the journal\n\nWrite on **concrete triggers**, not on a vague sense of \"meaningful progress.\" The right cadence sits between every-tool-call (noisy log, wastes tokens) and end-of-run (everything lost if you die mid-task). This matters more, not less, now that Turns run long: the Journal checkpoint is what carries a killed or reset session.\n\n- **Discrete unit done** \u2014 file written, fix applied, test run, gate passed. Reflect the new reality before starting the next unit. If another unit is still open, this is also a shed trigger (see [The Dossier](#the-dossier)): cut this unit's detail into `units/<slug>.md` before you start the next one.\n- **Insight learned** \u2014 API quirk, config field that turned out to matter, dead end ruled out, decision between two approaches. Insights are journal-worthy even when no code changed; rediscovering them costs a future Turn a full re-exploration. The journal is a knowledge ledger, not just a task log. Insight that spans more than one unit belongs in `orientation.md`, not the journal.\n- **About to do something risky or long-running** \u2014 subagent dispatch, long build, flaky external call, large refactor. Write _first_, then act. If the operation kills your Turn, only what's on disk survives.\n- **Plan changed** \u2014 task resequenced, new task surfaced, planned task no longer needed. Stale plans poison continuation Turns.\n- **The journal crosses the ~8,000-token backstop** \u2014 shed the completed phases of the still-open unit into its `units/<slug>.md` record now, mid-unit; don't wait for it to close.\n- **You haven't written in a while** \u2014 if you can't remember the last update, you've gone too long. A short defensive update (\"doing X, last completed Y, next is Z\") beats nothing.\n\nEach update covers: what changed (work or knowledge), what's now next, and any caveat a future Turn needs. Don't transcribe tool calls \u2014 the journal is a contract with your future self, not a replay log.\n\nThe cost of one extra journal update is a few tokens. The cost of dying without one is rework. Bias toward writing.\n\n## Task UI projection\n\nThe journal is the durable source of truth. Your harness's task tools are a session-scoped UI projection of the same plan, shown to the user in their CLI widget. They do not survive process exit.\n\n{{TASK_TOOL_INSTRUCTIONS}}\n\n- **Turn 1, after orientation:** project the journal's task plan into the task tools.\n- **In a fresh continuation (e.g. after a Handover):** recreate the projection from the journal; do not assume task IDs from prior sessions still exist.\n- **During work:** update both \u2014 the task tools for immediate UI feedback, the journal for persistence \u2014 in the same working phase.\n\nSeparately, the runner independently re-derives a task list from the journal's [unit table](#the-unit-table) and each unit record's frontmatter after every Turn, and mirrors it into the CLI's own task display. This is a backstop, not a substitute for the above \u2014 it only ever reaches `open`/`closed`, so keep calling the task tools yourself for anything finer-grained. It never blocks or fails a Turn: an unparseable table or record just means that Turn's mirror is skipped, exactly as described in [Parsing is best-effort, never a gate](#parsing-is-best-effort-never-a-gate).\n\n## Quick reference\n\nOn a Handover, use the validated **Restart contract** in the seed prompt: objective, next action, essential constraints, changes, open questions and evidence references. Read additional Journal and Unit Record sections selectively when the next action needs them. Update durable state only when it changes; never append checkpoint-processed notes. The Runner may suspend if the restart contract cannot fit its context allowance.\n\n- [ ] Replace the skeleton immediately, even for single-Turn requests\n- [ ] Run until the work is done \u2014 do not stop at checkpoints, and never stop as a way of asking permission to continue\n- [ ] Need a human? Declare it: `NEEDS_HUMAN: <question>` as the final non-empty line, then end\n- [ ] Update the journal on concrete triggers \u2014 unit done, insight learned, risky op pending, plan changed\n- [ ] Shed a unit's detail into `units/<slug>.md` the moment it closes while another stays open, or the journal crosses ~8,000 tokens \u2014 cut, paste, pointer, never summarize\n- [ ] Keep the unit table and each shed record's `status: open|closed` frontmatter current \u2014 the runner mirrors them into the task list and skips silently on any parse miss\n On a Handover, use the validated **Restart contract** in the seed prompt: objective, next action, essential constraints, changes, open questions and evidence references. Read additional Journal and Unit Record sections selectively when the next action needs them. Update durable state only when it changes; never append checkpoint-processed notes. The Runner may suspend if the restart contract cannot fit its context allowance.\n- [ ] Project the journal plan into task tools at session start; keep both in sync as work lands\n- [ ] Follow the workflow steps as written; do not skip, reorder, or substitute your own process\n- [ ] Workflow has named steps? Keep the `TURN_PROTOCOL` step block in the journal naming the one you are on\n- [ ] Load the appropriate skill before each activity; do not rely on assumed knowledge\n- [ ] Use and record a dedicated git worktree for repository-changing work\n- [ ] Run quality gates in order; respect delegation constraints and retry limits\n- [ ] Write the completion marker only when all work is verified, and make it the final non-empty line\n\n## Restart checkpoint\n\nMaintain one `## Restart` section in the Journal during normal work, using the\nRun id and token bound supplied by the Runner. Include `Run: <runId>` and these\nnonempty fields: Objective, Next action, Constraints, Changes, Open questions,\nReferences. Use `none` when appropriate. Preserve essential constraints; point to\nevidence instead of copying cold history. Update after orientation and concrete\nmilestones, before large reads or risky operations. Write atomically. At the\ncontext boundary the Runner restarts directly from this section, or pauses if it\nis missing, invalid or already consumed. Continue normal work; do not request a\nseparate summarization Turn.\n";
|
|
1797
1921
|
|
|
1798
1922
|
// src/core/workflows/stateMachine.ts
|
|
1799
1923
|
function buildTaskToolInstructions(harness) {
|
|
@@ -1801,23 +1925,23 @@ function buildTaskToolInstructions(harness) {
|
|
|
1801
1925
|
case "openai-codex":
|
|
1802
1926
|
return `Use the \`update_plan\` tool to create and maintain the task list shown to the user for the full harness session.
|
|
1803
1927
|
|
|
1804
|
-
- As soon as orientation yields a credible plan, create a detailed task list from the
|
|
1805
|
-
- At the start of every fresh harness session, recreate the full task list from the
|
|
1928
|
+
- As soon as orientation yields a credible plan, create a detailed task list from the journal; each item should be a concrete, verifiable unit of work, not a vague phase
|
|
1929
|
+
- At the start of every fresh harness session, recreate the full task list from the journal before resuming execution
|
|
1806
1930
|
- Do not carry forward prior session task IDs or assume prior session plan items still exist in the tool
|
|
1807
|
-
- Keep the task list accurate throughout the session: when scope, ordering, status, or completion changes, update \`update_plan\` immediately in the same working phase as the
|
|
1808
|
-
- Keep task state consistent and non-stale: reconcile the task list against the
|
|
1809
|
-
- Maintain exactly one \`in_progress\` task unless the
|
|
1931
|
+
- Keep the task list accurate throughout the session: when scope, ordering, status, or completion changes, update \`update_plan\` immediately in the same working phase as the journal
|
|
1932
|
+
- Keep task state consistent and non-stale: reconcile the task list against the journal before moving on, and before ending the session
|
|
1933
|
+
- Maintain exactly one \`in_progress\` task unless the journal explicitly records parallel active work`;
|
|
1810
1934
|
case "claude-code":
|
|
1811
1935
|
case "opencode":
|
|
1812
1936
|
default:
|
|
1813
1937
|
return `Use \`TaskCreate\` and \`TaskUpdate\` to create and maintain the task list shown to the user for the full harness session.
|
|
1814
1938
|
|
|
1815
|
-
- As soon as orientation yields a credible plan, create a detailed task list from the
|
|
1816
|
-
- At the start of every fresh harness session, recreate the full task list from the
|
|
1939
|
+
- As soon as orientation yields a credible plan, create a detailed task list from the journal; each item should be a concrete, verifiable unit of work, not a vague phase
|
|
1940
|
+
- At the start of every fresh harness session, recreate the full task list from the journal instead of trying to resume prior session task IDs
|
|
1817
1941
|
- Do not refer to task IDs created in earlier sessions; they are session-scoped UI artifacts, not durable workflow state
|
|
1818
|
-
- Keep the task list accurate throughout the session: when scope, ordering, status, or completion changes, update the task list in the same working phase as the
|
|
1819
|
-
- Keep task state consistent and non-stale: reconcile the task list against the
|
|
1820
|
-
- Maintain exactly one active task unless the
|
|
1942
|
+
- Keep the task list accurate throughout the session: when scope, ordering, status, or completion changes, update the task list in the same working phase as the journal
|
|
1943
|
+
- Keep task state consistent and non-stale: reconcile the task list against the journal before moving on, and before ending the session
|
|
1944
|
+
- Maintain exactly one active task unless the journal explicitly records parallel active work`;
|
|
1821
1945
|
}
|
|
1822
1946
|
}
|
|
1823
1947
|
function buildStateMachineContent(harness = "claude-code") {
|
|
@@ -1829,7 +1953,7 @@ function buildStateMachineContent(harness = "claude-code") {
|
|
|
1829
1953
|
var STATE_MACHINE_CONTENT = buildStateMachineContent();
|
|
1830
1954
|
|
|
1831
1955
|
// src/core/workflows/sessionPlan.ts
|
|
1832
|
-
function readWorkflowOverride(projectDir, workflow, sessionId,
|
|
1956
|
+
function readWorkflowOverride(projectDir, workflow, sessionId, journalPath, harness = "claude-code") {
|
|
1833
1957
|
if (!workflow?.workflowFile) {
|
|
1834
1958
|
return { workflowOverride: void 0, warnings: [] };
|
|
1835
1959
|
}
|
|
@@ -1848,11 +1972,24 @@ function readWorkflowOverride(projectDir, workflow, sessionId, trackerPath, harn
|
|
|
1848
1972
|
let composed = workflow.loop?.enabled ? buildStateMachineContent(harness) + "\n\n" + workflowContent : workflowContent;
|
|
1849
1973
|
composed = substituteVariables(composed, {
|
|
1850
1974
|
sessionId,
|
|
1851
|
-
|
|
1975
|
+
journalPath: journalPath ?? void 0
|
|
1852
1976
|
});
|
|
1853
|
-
const
|
|
1854
|
-
|
|
1855
|
-
|
|
1977
|
+
const assetDir = path9.join(projectDir, ".athena", "execution-assets");
|
|
1978
|
+
fs10.mkdirSync(assetDir, { recursive: true, mode: 448 });
|
|
1979
|
+
const digest = crypto.createHash("sha256").update(composed).digest("hex");
|
|
1980
|
+
const composedPath = path9.join(assetDir, `${digest}.md`);
|
|
1981
|
+
if (!fs10.existsSync(composedPath)) {
|
|
1982
|
+
const temporary = path9.join(
|
|
1983
|
+
assetDir,
|
|
1984
|
+
`${digest}.${crypto.randomUUID()}.tmp`
|
|
1985
|
+
);
|
|
1986
|
+
try {
|
|
1987
|
+
fs10.writeFileSync(temporary, composed, { encoding: "utf-8", mode: 384 });
|
|
1988
|
+
fs10.renameSync(temporary, composedPath);
|
|
1989
|
+
} finally {
|
|
1990
|
+
if (fs10.existsSync(temporary)) fs10.unlinkSync(temporary);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1856
1993
|
return {
|
|
1857
1994
|
workflowOverride: {
|
|
1858
1995
|
appendSystemPromptFile: composedPath,
|
|
@@ -1869,17 +2006,33 @@ function mergeOverrides(base, workflowOverride) {
|
|
|
1869
2006
|
...workflowOverride
|
|
1870
2007
|
};
|
|
1871
2008
|
}
|
|
1872
|
-
function
|
|
2009
|
+
function resolveJournalPath(input) {
|
|
1873
2010
|
const loop = input.workflow?.loop;
|
|
1874
2011
|
if (!loop?.enabled) {
|
|
1875
2012
|
return null;
|
|
1876
2013
|
}
|
|
1877
|
-
const rawPath = loop
|
|
2014
|
+
const rawPath = loopJournalPath(loop);
|
|
1878
2015
|
if (!input.sessionId && rawPath.includes("{sessionId}")) {
|
|
1879
2016
|
return null;
|
|
1880
2017
|
}
|
|
1881
2018
|
const promptPath = input.sessionId ? rawPath.replaceAll("{sessionId}", input.sessionId) : rawPath;
|
|
1882
2019
|
const absolutePath = path9.isAbsolute(promptPath) ? promptPath : path9.resolve(input.projectDir, promptPath);
|
|
2020
|
+
const usesDefaultPath = loop.journalPath === void 0 && loop.trackerPath === void 0;
|
|
2021
|
+
if (usesDefaultPath && !fs10.existsSync(absolutePath)) {
|
|
2022
|
+
const legacyAbsolutePath = path9.join(
|
|
2023
|
+
path9.dirname(absolutePath),
|
|
2024
|
+
LEGACY_JOURNAL_FILENAME
|
|
2025
|
+
);
|
|
2026
|
+
if (fs10.existsSync(legacyAbsolutePath)) {
|
|
2027
|
+
return {
|
|
2028
|
+
absolutePath: legacyAbsolutePath,
|
|
2029
|
+
promptPath: path9.posix.join(
|
|
2030
|
+
path9.posix.dirname(promptPath),
|
|
2031
|
+
LEGACY_JOURNAL_FILENAME
|
|
2032
|
+
)
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
1883
2036
|
return {
|
|
1884
2037
|
absolutePath,
|
|
1885
2038
|
promptPath
|
|
@@ -1887,17 +2040,17 @@ function resolveTrackerPath(input) {
|
|
|
1887
2040
|
}
|
|
1888
2041
|
function createWorkflowRunState(input) {
|
|
1889
2042
|
const { projectDir, sessionId, workflow, harness } = input;
|
|
1890
|
-
const
|
|
2043
|
+
const journalResolved = resolveJournalPath({ projectDir, sessionId, workflow });
|
|
1891
2044
|
const { workflowOverride, warnings } = readWorkflowOverride(
|
|
1892
2045
|
projectDir,
|
|
1893
2046
|
workflow,
|
|
1894
2047
|
sessionId,
|
|
1895
|
-
|
|
2048
|
+
journalResolved?.promptPath,
|
|
1896
2049
|
harness
|
|
1897
2050
|
);
|
|
1898
2051
|
return {
|
|
1899
2052
|
workflow,
|
|
1900
|
-
|
|
2053
|
+
journalPathForPrompt: journalResolved?.promptPath,
|
|
1901
2054
|
workflowOverride,
|
|
1902
2055
|
warnings
|
|
1903
2056
|
};
|
|
@@ -1908,7 +2061,7 @@ function prepareWorkflowTurn(state, input) {
|
|
|
1908
2061
|
const isContinuation = workflow?.loop?.enabled === true && iteration > 1;
|
|
1909
2062
|
const prompt = isContinuation ? buildContinuePrompt({
|
|
1910
2063
|
...workflow.loop,
|
|
1911
|
-
|
|
2064
|
+
journalPath: state.journalPathForPrompt ?? workflow.loop?.journalPath
|
|
1912
2065
|
}) : workflow ? applyPromptTemplate(workflow.promptTemplate, input.prompt) : input.prompt;
|
|
1913
2066
|
return {
|
|
1914
2067
|
prompt,
|
|
@@ -1920,54 +2073,310 @@ function prepareWorkflowTurn(state, input) {
|
|
|
1920
2073
|
};
|
|
1921
2074
|
}
|
|
1922
2075
|
|
|
1923
|
-
// src/core/workflows/useWorkflowSessionController.ts
|
|
1924
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
1925
|
-
|
|
1926
2076
|
// src/core/workflows/workflowRunner.ts
|
|
1927
|
-
import
|
|
2077
|
+
import crypto3 from "crypto";
|
|
1928
2078
|
import fs12 from "fs";
|
|
1929
2079
|
import path10 from "path";
|
|
1930
2080
|
|
|
1931
2081
|
// src/core/workflows/terminalOutcome.ts
|
|
1932
2082
|
import fs11 from "fs";
|
|
1933
|
-
var
|
|
1934
|
-
var MISPLACED_TERMINAL_MARKER_MESSAGE = "terminal workflow marker is not the final non-empty line of the
|
|
2083
|
+
var MISSING_JOURNAL_MESSAGE = "the journal file went missing during the run \u2014 the workflow can no longer verify progress";
|
|
2084
|
+
var MISPLACED_TERMINAL_MARKER_MESSAGE = "terminal workflow marker is not the final non-empty line of the journal; move all summary text above the marker";
|
|
1935
2085
|
function resolveTurnOutcome(input) {
|
|
1936
|
-
const {
|
|
1937
|
-
if (!fs11.existsSync(
|
|
2086
|
+
const { journalPath, loop } = input;
|
|
2087
|
+
if (!fs11.existsSync(journalPath)) {
|
|
1938
2088
|
return {
|
|
1939
2089
|
kind: "stop",
|
|
1940
2090
|
status: "failed",
|
|
1941
|
-
stopReason:
|
|
2091
|
+
stopReason: MISSING_JOURNAL_MESSAGE
|
|
1942
2092
|
};
|
|
1943
2093
|
}
|
|
1944
|
-
const
|
|
1945
|
-
if (
|
|
2094
|
+
const journal = parseJournalState(readJournal(journalPath), loop);
|
|
2095
|
+
if (journal.misplacedTerminalMarker) {
|
|
1946
2096
|
return {
|
|
1947
2097
|
kind: "stop",
|
|
1948
2098
|
status: "failed",
|
|
1949
2099
|
stopReason: MISPLACED_TERMINAL_MARKER_MESSAGE
|
|
1950
2100
|
};
|
|
1951
2101
|
}
|
|
1952
|
-
if (
|
|
2102
|
+
if (journal.completed) {
|
|
1953
2103
|
return { kind: "stop", status: "completed" };
|
|
1954
2104
|
}
|
|
1955
|
-
if (
|
|
2105
|
+
if (journal.needsHuman) {
|
|
1956
2106
|
return {
|
|
1957
2107
|
kind: "suspend",
|
|
1958
2108
|
status: "awaiting_attention",
|
|
1959
|
-
stopReason:
|
|
2109
|
+
stopReason: journal.needsHumanReason ? `agent declared NEEDS_HUMAN: ${journal.needsHumanReason}` : "agent declared NEEDS_HUMAN",
|
|
2110
|
+
...journal.deprecatedMarker ? { deprecation: buildMarkerDeprecation(journal.deprecatedMarker) } : {}
|
|
1960
2111
|
};
|
|
1961
2112
|
}
|
|
1962
|
-
|
|
2113
|
+
return { kind: "continue" };
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// src/core/workflows/continuationPolicy.ts
|
|
2117
|
+
var DEFAULT_RESTART_TOKENS = 2e3;
|
|
2118
|
+
var DEFAULT_WORKING_TOKENS = 8e3;
|
|
2119
|
+
function resourceInterruption(stop) {
|
|
2120
|
+
const names = {
|
|
2121
|
+
tokens: "token budget",
|
|
2122
|
+
iterations: "iteration ceiling",
|
|
2123
|
+
context: "restart context allowance",
|
|
2124
|
+
restart: "restart checkpoint"
|
|
2125
|
+
};
|
|
2126
|
+
return {
|
|
2127
|
+
kind: "cap_exhausted",
|
|
2128
|
+
cap: stop.cause === "iterations" ? "iterations" : stop.cause === "tokens" ? "tokens" : "handover",
|
|
2129
|
+
limit: stop.limit,
|
|
2130
|
+
message: `${names[stop.cause]} reached: ${stop.limit}; used ${stop.used}${stop.detail ? ` \u2014 ${stop.detail}` : ""}`,
|
|
2131
|
+
resource: stop
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
function admitContinuation(input) {
|
|
2135
|
+
const { loop } = input;
|
|
2136
|
+
if (!loop?.enabled) return null;
|
|
2137
|
+
const base = { fresh: false, checkpointPath: input.checkpointPath };
|
|
2138
|
+
if (loop.maxRunTokens !== void 0 && (input.tokens ?? 0) >= loop.maxRunTokens)
|
|
1963
2139
|
return {
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2140
|
+
...base,
|
|
2141
|
+
cause: "tokens",
|
|
2142
|
+
limit: loop.maxRunTokens,
|
|
2143
|
+
used: input.tokens ?? 0
|
|
1967
2144
|
};
|
|
2145
|
+
if (input.iteration > loop.maxIterations)
|
|
2146
|
+
return {
|
|
2147
|
+
...base,
|
|
2148
|
+
cause: "iterations",
|
|
2149
|
+
limit: loop.maxIterations,
|
|
2150
|
+
used: input.iteration - 1
|
|
2151
|
+
};
|
|
2152
|
+
if (input.context) {
|
|
2153
|
+
const { opening, required, ceiling, source } = input.context;
|
|
2154
|
+
const used = opening + required + DEFAULT_WORKING_TOKENS;
|
|
2155
|
+
if (used > ceiling)
|
|
2156
|
+
return {
|
|
2157
|
+
...base,
|
|
2158
|
+
cause: "context",
|
|
2159
|
+
fresh: true,
|
|
2160
|
+
limit: ceiling,
|
|
2161
|
+
used,
|
|
2162
|
+
detail: `opening ${opening} + checkpoint ${required} + ${DEFAULT_WORKING_TOKENS} working tokens; ${source}; reduce startup context or increase its allowance`
|
|
2163
|
+
};
|
|
1968
2164
|
}
|
|
1969
|
-
return
|
|
2165
|
+
return null;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
// src/core/workflows/restartContract.ts
|
|
2169
|
+
var fields = [
|
|
2170
|
+
"Objective",
|
|
2171
|
+
"Next action",
|
|
2172
|
+
"Constraints",
|
|
2173
|
+
"Changes",
|
|
2174
|
+
"Open questions",
|
|
2175
|
+
"References"
|
|
2176
|
+
];
|
|
2177
|
+
function readRestartContract(content, limit = DEFAULT_RESTART_TOKENS, expectedRunId) {
|
|
2178
|
+
if ((content.match(/^## Restart[ \t]*$/gm) ?? []).length !== 1) return null;
|
|
2179
|
+
const match = /^## Restart\s*\n([\s\S]*?)(?=^## |$(?![\s\S]))/m.exec(content);
|
|
2180
|
+
if (!match) return null;
|
|
2181
|
+
const text = match[1].trim();
|
|
2182
|
+
if (!fields.every((field) => new RegExp(`^${field}:[ ]*\\S`, "m").test(text)))
|
|
2183
|
+
return null;
|
|
2184
|
+
if (expectedRunId && !text.split("\n").some((line) => line.trim() === `Run: ${expectedRunId}`))
|
|
2185
|
+
return null;
|
|
2186
|
+
const tokens = estimateTokenCount(text);
|
|
2187
|
+
return tokens <= limit ? { text, tokens } : null;
|
|
2188
|
+
}
|
|
2189
|
+
function restartInstructions(journalPath, runId, limit = DEFAULT_RESTART_TOKENS) {
|
|
2190
|
+
return `Maintain one ## Restart section in the Journal at ${journalPath} during this Workflow Run. Include Run: ${runId}, followed by these nonempty fields: ${fields.join(", ")}. Use none where appropriate. Update it after orientation and each meaningful work checkpoint, before large reads or risky operations; keep it within ${limit} estimated tokens. Preserve essential constraints and reference supporting evidence. Write changes atomically so interruption cannot leave a partial checkpoint. The Runner may replace your Agent Session at its context bound using only this section. Do not stop to request a Handover; continue working and declare the normal terminal marker when done.`;
|
|
1970
2191
|
}
|
|
2192
|
+
function seedFromRestart(contract, checkpointPath, journalPath) {
|
|
2193
|
+
return `Continue this Workflow Run from its validated restart contract:
|
|
2194
|
+
|
|
2195
|
+
${contract.text}
|
|
2196
|
+
|
|
2197
|
+
The checkpoint is in ${checkpointPath}${journalPath ? `; the Journal is at ${journalPath}` : ""}. Read additional sections selectively when the next action requires them. Update durable state only when it changes. Do not append checkpoint-processed notes or repeat completed work. Essential constraints above remain in force.`;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
// src/core/workflows/turnProtocolBlock.ts
|
|
2201
|
+
var BLOCK_OPEN = /^\s*<!--\s*TURN_PROTOCOL\s*$/;
|
|
2202
|
+
var BLOCK_CLOSE = /^\s*-->\s*$/;
|
|
2203
|
+
var TRAILING_CLOSE = /\s*-->\s*$/;
|
|
2204
|
+
var KEY_VALUE = /^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$/;
|
|
2205
|
+
function parseTurnProtocolBlock(content) {
|
|
2206
|
+
const lines = content.split("\n");
|
|
2207
|
+
let last = null;
|
|
2208
|
+
let open = null;
|
|
2209
|
+
for (const raw of lines) {
|
|
2210
|
+
const line = raw.replace(/\r$/, "");
|
|
2211
|
+
if (open === null) {
|
|
2212
|
+
if (BLOCK_OPEN.test(line)) open = [];
|
|
2213
|
+
continue;
|
|
2214
|
+
}
|
|
2215
|
+
if (BLOCK_CLOSE.test(line)) {
|
|
2216
|
+
last = open;
|
|
2217
|
+
open = null;
|
|
2218
|
+
continue;
|
|
2219
|
+
}
|
|
2220
|
+
if (TRAILING_CLOSE.test(line)) {
|
|
2221
|
+
open.push(line.replace(TRAILING_CLOSE, ""));
|
|
2222
|
+
last = open;
|
|
2223
|
+
open = null;
|
|
2224
|
+
continue;
|
|
2225
|
+
}
|
|
2226
|
+
open.push(line);
|
|
2227
|
+
}
|
|
2228
|
+
if (open !== null) {
|
|
2229
|
+
return {
|
|
2230
|
+
kind: "malformed",
|
|
2231
|
+
reason: "the TURN_PROTOCOL block is never closed with -->"
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
if (last === null) return { kind: "missing" };
|
|
2235
|
+
return parseBlockLines(last);
|
|
2236
|
+
}
|
|
2237
|
+
function parseBlockLines(lines) {
|
|
2238
|
+
const fields2 = /* @__PURE__ */ new Map();
|
|
2239
|
+
for (const line of lines) {
|
|
2240
|
+
const match = KEY_VALUE.exec(line.trim());
|
|
2241
|
+
if (!match) continue;
|
|
2242
|
+
fields2.set(match[1], match[2].trim());
|
|
2243
|
+
}
|
|
2244
|
+
const name = fields2.get("step");
|
|
2245
|
+
if (name === void 0) {
|
|
2246
|
+
return { kind: "malformed", reason: "the block has no `step:` line" };
|
|
2247
|
+
}
|
|
2248
|
+
if (name.length === 0) {
|
|
2249
|
+
return { kind: "malformed", reason: "the `step:` line names no step" };
|
|
2250
|
+
}
|
|
2251
|
+
const index = parsePositiveInt(fields2.get("step_index"));
|
|
2252
|
+
if (index.kind === "invalid") {
|
|
2253
|
+
return {
|
|
2254
|
+
kind: "malformed",
|
|
2255
|
+
reason: `\`step_index\` must be a positive integer, got "${index.raw}"`
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
const total = parsePositiveInt(fields2.get("step_total"));
|
|
2259
|
+
if (total.kind === "invalid") {
|
|
2260
|
+
return {
|
|
2261
|
+
kind: "malformed",
|
|
2262
|
+
reason: `\`step_total\` must be a positive integer, got "${total.raw}"`
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
if (index.kind === "value" && total.kind === "value" && index.value > total.value) {
|
|
2266
|
+
return {
|
|
2267
|
+
kind: "malformed",
|
|
2268
|
+
reason: `\`step_index\` (${index.value}) is past \`step_total\` (${total.value})`
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
return {
|
|
2272
|
+
kind: "ok",
|
|
2273
|
+
step: {
|
|
2274
|
+
name,
|
|
2275
|
+
...index.kind === "value" ? { index: index.value } : {},
|
|
2276
|
+
...total.kind === "value" ? { total: total.value } : {}
|
|
2277
|
+
}
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
function parsePositiveInt(raw) {
|
|
2281
|
+
if (raw === void 0 || raw.length === 0) return { kind: "absent" };
|
|
2282
|
+
if (!/^\d+$/.test(raw)) return { kind: "invalid", raw };
|
|
2283
|
+
const value = Number(raw);
|
|
2284
|
+
if (!Number.isSafeInteger(value) || value < 1) return { kind: "invalid", raw };
|
|
2285
|
+
return { kind: "value", value };
|
|
2286
|
+
}
|
|
2287
|
+
function sameStep(a, b) {
|
|
2288
|
+
return a.name === b.name && a.index === b.index;
|
|
2289
|
+
}
|
|
2290
|
+
function createPhaseTracker() {
|
|
2291
|
+
let lastStep = null;
|
|
2292
|
+
let lastMalformedReason = null;
|
|
2293
|
+
return {
|
|
2294
|
+
observe(journalContent) {
|
|
2295
|
+
const parsed = parseTurnProtocolBlock(journalContent);
|
|
2296
|
+
if (parsed.kind === "missing") {
|
|
2297
|
+
lastMalformedReason = null;
|
|
2298
|
+
return { kind: "no_block" };
|
|
2299
|
+
}
|
|
2300
|
+
if (parsed.kind === "malformed") {
|
|
2301
|
+
const warning = parsed.reason === lastMalformedReason ? null : `ignoring the journal's TURN_PROTOCOL block: ${parsed.reason}`;
|
|
2302
|
+
lastMalformedReason = parsed.reason;
|
|
2303
|
+
return { kind: "malformed", reason: parsed.reason, warning };
|
|
2304
|
+
}
|
|
2305
|
+
lastMalformedReason = null;
|
|
2306
|
+
if (lastStep && sameStep(lastStep, parsed.step)) {
|
|
2307
|
+
lastStep = parsed.step;
|
|
2308
|
+
return { kind: "same_step" };
|
|
2309
|
+
}
|
|
2310
|
+
lastStep = parsed.step;
|
|
2311
|
+
return { kind: "new_step", step: parsed.step };
|
|
2312
|
+
}
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// src/core/workflows/steer.ts
|
|
2317
|
+
var STEER_BLOCK_OPEN = "=== HUMAN STEER";
|
|
2318
|
+
var STEER_BLOCK_END = "=== END HUMAN STEER ===";
|
|
2319
|
+
function steerHeading(steer, index, total) {
|
|
2320
|
+
const position = total > 1 ? `${index + 1} of ${total}, ` : "";
|
|
2321
|
+
return `${STEER_BLOCK_OPEN} (${position}via ${steer.origin}, received ${new Date(
|
|
2322
|
+
steer.receivedAt
|
|
2323
|
+
).toISOString()}) ===`;
|
|
2324
|
+
}
|
|
2325
|
+
function buildSteerBlock(steers) {
|
|
2326
|
+
const entries = steers.map(
|
|
2327
|
+
(steer, index) => `${steerHeading(steer, index, steers.length)}
|
|
2328
|
+
${steer.text.trim()}`
|
|
2329
|
+
);
|
|
2330
|
+
const noun = steers.length > 1 ? "steers" : "steer";
|
|
2331
|
+
return `${entries.join("\n")}
|
|
2332
|
+
${STEER_BLOCK_END}
|
|
2333
|
+
|
|
2334
|
+
A human steered this run. Read the ${noun} above before you plan: where it conflicts with the journal's planned next action, the steer wins. Apply it, note it in the journal, and continue the workflow.`;
|
|
2335
|
+
}
|
|
2336
|
+
function prependSteerBlock(prompt, steers) {
|
|
2337
|
+
if (steers.length === 0) return prompt;
|
|
2338
|
+
return `${buildSteerBlock(steers)}
|
|
2339
|
+
|
|
2340
|
+
---
|
|
2341
|
+
|
|
2342
|
+
${prompt}`;
|
|
2343
|
+
}
|
|
2344
|
+
function formatSteerJournalEntry(steer, iteration) {
|
|
2345
|
+
const quoted = steer.text.trim().split("\n").map((line) => `> ${line}`).join("\n");
|
|
2346
|
+
return `
|
|
2347
|
+
|
|
2348
|
+
---
|
|
2349
|
+
|
|
2350
|
+
## Human steer (via ${steer.origin})
|
|
2351
|
+
|
|
2352
|
+
_Received ${new Date(steer.receivedAt).toISOString()}; delivered into Turn ${iteration}._
|
|
2353
|
+
|
|
2354
|
+
${quoted}
|
|
2355
|
+
`;
|
|
2356
|
+
}
|
|
2357
|
+
function createSteerQueue() {
|
|
2358
|
+
const buffered = [];
|
|
2359
|
+
let listener = null;
|
|
2360
|
+
return {
|
|
2361
|
+
push(steer) {
|
|
2362
|
+
if (listener) {
|
|
2363
|
+
listener(steer);
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
buffered.push(steer);
|
|
2367
|
+
},
|
|
2368
|
+
subscribe(next) {
|
|
2369
|
+
listener = next;
|
|
2370
|
+
for (const steer of buffered.splice(0)) next(steer);
|
|
2371
|
+
return () => {
|
|
2372
|
+
if (listener === next) listener = null;
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
// src/core/workflows/runMachine.ts
|
|
2379
|
+
import crypto2 from "crypto";
|
|
1971
2380
|
|
|
1972
2381
|
// src/core/runtime/failureTaxonomy.ts
|
|
1973
2382
|
var RULES = [
|
|
@@ -2018,19 +2427,56 @@ function classifyTurnFailure(input) {
|
|
|
2018
2427
|
}
|
|
2019
2428
|
|
|
2020
2429
|
// src/core/workflows/runMachine.ts
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2430
|
+
function formatGrace(ms) {
|
|
2431
|
+
return ms >= 1e3 ? `${Math.round(ms / 1e3)}s` : `${ms}ms`;
|
|
2432
|
+
}
|
|
2433
|
+
function describeDeferral(permission) {
|
|
2434
|
+
return permission.graceMs > 0 ? ` unanswered within the grace window (${formatGrace(permission.graceMs)}); deferred` : " deferred immediately (no hub attached to answer)";
|
|
2024
2435
|
}
|
|
2025
|
-
function
|
|
2026
|
-
|
|
2436
|
+
function protocolInterruptionFor(interruption) {
|
|
2437
|
+
if (interruption.kind === "question" || !interruption.permission) return null;
|
|
2438
|
+
return {
|
|
2439
|
+
kind: "question",
|
|
2440
|
+
message: describeInterruption(interruption),
|
|
2441
|
+
requestId: interruption.permission.requestId,
|
|
2442
|
+
question: `${interruption.toolName}: ${interruption.permission.inputSummary}`
|
|
2443
|
+
};
|
|
2027
2444
|
}
|
|
2028
|
-
function
|
|
2445
|
+
function describeInterruption(interruption) {
|
|
2446
|
+
switch (interruption.kind) {
|
|
2447
|
+
case "ask_rule":
|
|
2448
|
+
if (interruption.permission) {
|
|
2449
|
+
return `ask rule "${interruption.rule}" fired on ${interruption.toolName}${describeDeferral(interruption.permission)}: ${interruption.permission.inputSummary} \u2014 wake with --answer=allow|deny`;
|
|
2450
|
+
}
|
|
2451
|
+
return `ask rule "${interruption.rule}" fired on ${interruption.toolName} \u2014 needs a human`;
|
|
2452
|
+
case "question":
|
|
2453
|
+
return interruption.question ? `agent asked a question with no human attached to answer: ${interruption.question}` : "agent asked a question with no human attached to answer";
|
|
2454
|
+
case "unclaimed_permission":
|
|
2455
|
+
if (interruption.permission) {
|
|
2456
|
+
return `permission request (${interruption.toolName})${describeDeferral(interruption.permission)}: ${interruption.permission.inputSummary} \u2014 wake with --answer=allow|deny, or rerun with --isolation autonomous`;
|
|
2457
|
+
}
|
|
2458
|
+
return `agent requested sandbox approval (${interruption.toolName}) with no human attached to answer \u2014 rerun with --isolation autonomous, or wake with guidance`;
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
function hashJournalContent(content) {
|
|
2462
|
+
return crypto2.createHash("sha256").update(content).digest("hex");
|
|
2463
|
+
}
|
|
2464
|
+
function withCumulativeTokens(memory, cumulativeTokens) {
|
|
2465
|
+
return cumulativeTokens === null ? memory : { ...memory, cumulativeTokens };
|
|
2466
|
+
}
|
|
2467
|
+
function buildWakePrompt(reply, journalPath, parkedInterruption) {
|
|
2029
2468
|
return `This workflow run was suspended awaiting a human; it is now resumed. The human replied:
|
|
2030
2469
|
|
|
2031
2470
|
${reply}
|
|
2032
2471
|
|
|
2033
|
-
` + (
|
|
2472
|
+
` + buildReplayGuidance(parkedInterruption) + (journalPath ? `Consult the relevant Journal sections at ${journalPath} as needed, apply the reply, and continue the workflow. ` : `Apply the reply and continue the workflow. `) + `Keep the journal current as you work \u2014 if it still contains the runner's skeleton, replace it while orienting \u2014 and end by declaring a terminal marker as usual.`;
|
|
2473
|
+
}
|
|
2474
|
+
function buildReplayGuidance(parked) {
|
|
2475
|
+
if (!parked || parked.kind !== "question" || !parked.requestId) return "";
|
|
2476
|
+
const call = parked.question ?? "the same call";
|
|
2477
|
+
return `Before your previous Turn ended, your request \`${call}\` (request ${parked.requestId}) was deferred because nobody answered it in time. Re-issue that exact call now, with the same input: if an answer was stored while this run was parked it is applied automatically, otherwise the request is held again for a human. Do not work around the deferred call or substitute a different one.
|
|
2478
|
+
|
|
2479
|
+
`;
|
|
2034
2480
|
}
|
|
2035
2481
|
function buildFailureDetail(event) {
|
|
2036
2482
|
const parts = [];
|
|
@@ -2046,29 +2492,34 @@ function buildFailureDetail(event) {
|
|
|
2046
2492
|
}
|
|
2047
2493
|
return parts.join(": ") || "Turn failed";
|
|
2048
2494
|
}
|
|
2049
|
-
function
|
|
2050
|
-
|
|
2051
|
-
return {
|
|
2052
|
-
phase: {
|
|
2053
|
-
kind: "awaiting_attention",
|
|
2054
|
-
stopReason: opts.awaitingAttentionStopReason ?? ""
|
|
2055
|
-
},
|
|
2056
|
-
memory: opts.resumedMemory
|
|
2057
|
-
};
|
|
2058
|
-
}
|
|
2495
|
+
function initialRun(cfg, opts) {
|
|
2496
|
+
const initialSteers = opts.initialSteers ?? [];
|
|
2059
2497
|
if (opts.resumedMemory) {
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2498
|
+
const memory = initialSteers.length === 0 ? opts.resumedMemory : {
|
|
2499
|
+
...opts.resumedMemory,
|
|
2500
|
+
pendingSteers: [
|
|
2501
|
+
...opts.resumedMemory.pendingSteers,
|
|
2502
|
+
...initialSteers
|
|
2503
|
+
]
|
|
2504
|
+
};
|
|
2505
|
+
if (opts.waking) {
|
|
2506
|
+
const phase3 = {
|
|
2507
|
+
kind: "awaiting_attention",
|
|
2508
|
+
stopReason: opts.awaitingAttentionStopReason ?? "",
|
|
2509
|
+
...opts.parkedInterruption ? { interruption: opts.parkedInterruption } : {}
|
|
2510
|
+
};
|
|
2511
|
+
return { phase: phase3, memory, actions: kickoffActionsFor(phase3) };
|
|
2512
|
+
}
|
|
2513
|
+
const phase2 = {
|
|
2514
|
+
kind: "backing_off",
|
|
2515
|
+
ms: 0,
|
|
2516
|
+
resume: {
|
|
2517
|
+
kind: "turn",
|
|
2518
|
+
prompt: opts.resumedMemory.lastStopPrompt,
|
|
2519
|
+
continuation: opts.resumedMemory.lastStopContinuation
|
|
2520
|
+
}
|
|
2071
2521
|
};
|
|
2522
|
+
return { phase: phase2, memory, actions: kickoffActionsFor(phase2) };
|
|
2072
2523
|
}
|
|
2073
2524
|
const continuation = opts.initialContinuation ?? { mode: "fresh" };
|
|
2074
2525
|
const iteration = 1;
|
|
@@ -2077,55 +2528,193 @@ function createInitialRun(cfg, opts) {
|
|
|
2077
2528
|
iteration,
|
|
2078
2529
|
configOverride: void 0
|
|
2079
2530
|
});
|
|
2080
|
-
const prompt = opts.waking ? buildWakePrompt(
|
|
2531
|
+
const prompt = opts.waking ? buildWakePrompt(
|
|
2532
|
+
cfg.initialPrompt,
|
|
2533
|
+
cfg.journalPromptPath,
|
|
2534
|
+
opts.parkedInterruption
|
|
2535
|
+
) : prepared.prompt;
|
|
2536
|
+
const phase = {
|
|
2537
|
+
kind: "turn_in_flight",
|
|
2538
|
+
prompt,
|
|
2539
|
+
continuation,
|
|
2540
|
+
configOverride: prepared.configOverride
|
|
2541
|
+
};
|
|
2081
2542
|
return {
|
|
2082
|
-
phase
|
|
2083
|
-
kind: "turn_in_flight",
|
|
2084
|
-
prompt,
|
|
2085
|
-
continuation,
|
|
2086
|
-
configOverride: prepared.configOverride
|
|
2087
|
-
},
|
|
2543
|
+
phase,
|
|
2088
2544
|
memory: {
|
|
2089
2545
|
iteration,
|
|
2090
2546
|
nudgeStreak: 0,
|
|
2091
2547
|
retryStreak: 0,
|
|
2092
|
-
|
|
2548
|
+
lastJournalHash: null,
|
|
2093
2549
|
lastStopPrompt: prompt,
|
|
2094
2550
|
lastStopContinuation: continuation,
|
|
2095
|
-
|
|
2096
|
-
|
|
2551
|
+
pendingSteers: initialSteers,
|
|
2552
|
+
parkedAfterHandover: false,
|
|
2553
|
+
lastBoundedTurn: null,
|
|
2554
|
+
cumulativeTokens: null
|
|
2555
|
+
},
|
|
2556
|
+
actions: kickoffActionsFor(phase)
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
function kickoffActionsFor(phase) {
|
|
2560
|
+
switch (phase.kind) {
|
|
2561
|
+
case "turn_in_flight":
|
|
2562
|
+
return [
|
|
2563
|
+
{
|
|
2564
|
+
type: "start_turn",
|
|
2565
|
+
prompt: phase.prompt,
|
|
2566
|
+
continuation: phase.continuation,
|
|
2567
|
+
configOverride: phase.configOverride
|
|
2568
|
+
}
|
|
2569
|
+
];
|
|
2570
|
+
case "backing_off":
|
|
2571
|
+
return [{ type: "wait", ms: phase.ms }];
|
|
2572
|
+
case "awaiting_attention":
|
|
2573
|
+
case "completed":
|
|
2574
|
+
case "failed":
|
|
2575
|
+
case "cancelled":
|
|
2576
|
+
return [];
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
function deliverPendingSteers(result) {
|
|
2580
|
+
const steers = result.memory.pendingSteers;
|
|
2581
|
+
if (steers.length === 0 || result.phase.kind !== "turn_in_flight") {
|
|
2582
|
+
return result;
|
|
2583
|
+
}
|
|
2584
|
+
const startIndex = result.actions.findIndex((a) => a.type === "start_turn");
|
|
2585
|
+
if (startIndex === -1) return result;
|
|
2586
|
+
const start = result.actions[startIndex];
|
|
2587
|
+
const prompt = prependSteerBlock(start.prompt, steers);
|
|
2588
|
+
const actions = [
|
|
2589
|
+
...result.actions.slice(0, startIndex),
|
|
2590
|
+
{
|
|
2591
|
+
type: "steers_delivered",
|
|
2592
|
+
steers,
|
|
2593
|
+
iteration: result.memory.iteration
|
|
2594
|
+
},
|
|
2595
|
+
{ ...start, prompt },
|
|
2596
|
+
...result.actions.slice(startIndex + 1)
|
|
2597
|
+
];
|
|
2598
|
+
return {
|
|
2599
|
+
phase: { ...result.phase, prompt },
|
|
2600
|
+
memory: { ...result.memory, lastStopPrompt: prompt, pendingSteers: [] },
|
|
2601
|
+
actions
|
|
2097
2602
|
};
|
|
2098
2603
|
}
|
|
2099
|
-
function
|
|
2604
|
+
function handleSteer(phase, memory, steer) {
|
|
2605
|
+
return {
|
|
2606
|
+
phase,
|
|
2607
|
+
memory: { ...memory, pendingSteers: [...memory.pendingSteers, steer] },
|
|
2608
|
+
actions: [{ type: "persist" }]
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
function handleTurnInFlight(phase, incoming, event, cfg) {
|
|
2100
2612
|
if (event.cancelled) {
|
|
2101
|
-
return {
|
|
2613
|
+
return {
|
|
2614
|
+
phase: { kind: "cancelled" },
|
|
2615
|
+
memory: incoming,
|
|
2616
|
+
actions: [{ type: "persist" }]
|
|
2617
|
+
};
|
|
2102
2618
|
}
|
|
2619
|
+
const memory = {
|
|
2620
|
+
...withCumulativeTokens(incoming, event.cumulativeTokens),
|
|
2621
|
+
...event.checkpoint ? { checkpoint: event.checkpoint } : {}
|
|
2622
|
+
};
|
|
2103
2623
|
if (event.handoverRequestHandle !== null) {
|
|
2624
|
+
const checkpoint = event.checkpoint;
|
|
2625
|
+
const hash = checkpoint ? hashJournalContent(checkpoint.contract.text) : null;
|
|
2626
|
+
const boundedMemory = {
|
|
2627
|
+
...memory,
|
|
2628
|
+
...checkpoint ? { checkpoint } : {},
|
|
2629
|
+
parkedAfterHandover: true,
|
|
2630
|
+
contextKey: cfg.contextKey,
|
|
2631
|
+
lastBoundedTurn: {
|
|
2632
|
+
openingContextTokens: event.openingContextTokens,
|
|
2633
|
+
openingRestartTokens: incoming.activeRestartTokens ?? 0,
|
|
2634
|
+
lastContextTokens: event.lastContextTokens,
|
|
2635
|
+
toolCalls: event.toolCalls
|
|
2636
|
+
},
|
|
2637
|
+
lastJournalHash: hashJournalContent(event.journalContent)
|
|
2638
|
+
};
|
|
2639
|
+
if (!checkpoint || hash === memory.lastRestartHash) {
|
|
2640
|
+
return parkResource(
|
|
2641
|
+
boundedMemory,
|
|
2642
|
+
{
|
|
2643
|
+
cause: "restart",
|
|
2644
|
+
limit: cfg.loop?.maxRestartTokens ?? 2e3,
|
|
2645
|
+
used: checkpoint?.contract.tokens ?? 0,
|
|
2646
|
+
fresh: true,
|
|
2647
|
+
detail: checkpoint ? "Restart checkpoint has already been consumed; update it before continuing" : "Missing, invalid, oversized or wrong-run Restart checkpoint",
|
|
2648
|
+
checkpointPath: boundedMemory.checkpoint?.path
|
|
2649
|
+
},
|
|
2650
|
+
[{ type: "notify_iteration_complete" }]
|
|
2651
|
+
);
|
|
2652
|
+
}
|
|
2653
|
+
const prompt = seedFromRestart(checkpoint.contract, checkpoint.path);
|
|
2654
|
+
const continuation2 = { mode: "fresh" };
|
|
2655
|
+
const iteration = memory.iteration + 1;
|
|
2656
|
+
const prepared2 = prepareWorkflowTurn(cfg.workflowState, {
|
|
2657
|
+
prompt: cfg.initialPrompt,
|
|
2658
|
+
iteration,
|
|
2659
|
+
configOverride: void 0
|
|
2660
|
+
});
|
|
2104
2661
|
return {
|
|
2105
2662
|
phase: {
|
|
2106
|
-
kind: "
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2663
|
+
kind: "turn_in_flight",
|
|
2664
|
+
prompt,
|
|
2665
|
+
continuation: continuation2,
|
|
2666
|
+
configOverride: prepared2.configOverride
|
|
2667
|
+
},
|
|
2668
|
+
memory: {
|
|
2669
|
+
...boundedMemory,
|
|
2670
|
+
iteration,
|
|
2671
|
+
lastRestartHash: hash,
|
|
2672
|
+
lastStopPrompt: prompt,
|
|
2673
|
+
lastStopContinuation: continuation2,
|
|
2674
|
+
parkedAfterHandover: false
|
|
2115
2675
|
},
|
|
2116
|
-
memory,
|
|
2117
2676
|
actions: [
|
|
2677
|
+
{ type: "persist" },
|
|
2118
2678
|
{
|
|
2119
|
-
type: "
|
|
2120
|
-
|
|
2121
|
-
|
|
2679
|
+
type: "notify_handover_completed",
|
|
2680
|
+
completion: {
|
|
2681
|
+
iteration: memory.iteration,
|
|
2682
|
+
checkpointPath: checkpoint.path,
|
|
2683
|
+
checkpointTokens: checkpoint.contract.tokens
|
|
2684
|
+
}
|
|
2685
|
+
},
|
|
2686
|
+
{ type: "notify_iteration_complete" },
|
|
2687
|
+
{
|
|
2688
|
+
type: "start_turn",
|
|
2689
|
+
restartTokens: checkpoint.contract.tokens,
|
|
2690
|
+
prompt,
|
|
2691
|
+
continuation: continuation2,
|
|
2692
|
+
configOverride: prepared2.configOverride
|
|
2122
2693
|
}
|
|
2123
2694
|
]
|
|
2124
2695
|
};
|
|
2125
2696
|
}
|
|
2126
|
-
if (event.
|
|
2697
|
+
if (event.interruption) {
|
|
2698
|
+
const deferred = protocolInterruptionFor(event.interruption);
|
|
2699
|
+
if (deferred) {
|
|
2700
|
+
return {
|
|
2701
|
+
phase: {
|
|
2702
|
+
kind: "awaiting_attention",
|
|
2703
|
+
stopReason: deferred.message,
|
|
2704
|
+
interruption: deferred
|
|
2705
|
+
},
|
|
2706
|
+
memory,
|
|
2707
|
+
actions: [
|
|
2708
|
+
{ type: "record_interruption", interruption: deferred },
|
|
2709
|
+
{ type: "persist" }
|
|
2710
|
+
]
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2127
2713
|
return {
|
|
2128
|
-
phase: {
|
|
2714
|
+
phase: {
|
|
2715
|
+
kind: "awaiting_attention",
|
|
2716
|
+
stopReason: describeInterruption(event.interruption)
|
|
2717
|
+
},
|
|
2129
2718
|
memory,
|
|
2130
2719
|
actions: [{ type: "persist" }]
|
|
2131
2720
|
};
|
|
@@ -2238,23 +2827,29 @@ function handleTurnInFlight(phase, memory, event, cfg) {
|
|
|
2238
2827
|
kind: "awaiting_attention",
|
|
2239
2828
|
stopReason: outcome.stopReason ?? "stopped"
|
|
2240
2829
|
};
|
|
2830
|
+
const deprecation = outcome.kind === "suspend" ? outcome.deprecation : void 0;
|
|
2241
2831
|
return {
|
|
2242
2832
|
phase: nextPhase,
|
|
2243
2833
|
memory: memoryAfterSuccess,
|
|
2244
|
-
actions: [
|
|
2834
|
+
actions: [
|
|
2835
|
+
...deprecation ? [{ type: "warn", message: deprecation }] : [],
|
|
2836
|
+
{ type: "persist" }
|
|
2837
|
+
]
|
|
2245
2838
|
};
|
|
2246
2839
|
}
|
|
2247
|
-
const
|
|
2840
|
+
const journalHash = hashJournalContent(event.journalContent);
|
|
2248
2841
|
let nudgeStreak = memoryAfterSuccess.nudgeStreak;
|
|
2249
|
-
if (
|
|
2842
|
+
if (journalHash !== memoryAfterSuccess.lastJournalHash) {
|
|
2250
2843
|
nudgeStreak = 0;
|
|
2251
2844
|
}
|
|
2252
2845
|
const memoryWithHash = {
|
|
2253
2846
|
...memoryAfterSuccess,
|
|
2254
|
-
|
|
2847
|
+
lastJournalHash: journalHash,
|
|
2255
2848
|
nudgeStreak
|
|
2256
2849
|
};
|
|
2257
|
-
const sizeNudgeSuffix = estimateTokenCount(event.
|
|
2850
|
+
const sizeNudgeSuffix = (estimateTokenCount(event.journalContent) > DEFAULT_JOURNAL_TOKEN_BOUND ? buildJournalSizeNudgeSuffix(cfg.journalPromptPath) : "") + // Shed-integrity nudge (ADR 0018 §7): a half-executed shed is named
|
|
2851
|
+
// the Turn after it happens, on the same terms as the size nudge.
|
|
2852
|
+
(event.shedIntegrity ? buildShedIntegrityNudgeSuffix(event.shedIntegrity) : "");
|
|
2258
2853
|
if (event.adapterSessionId) {
|
|
2259
2854
|
const nextNudgeStreak = nudgeStreak + 1;
|
|
2260
2855
|
const nudgeCap = loop.nudgeCap ?? DEFAULT_NUDGE_CAP;
|
|
@@ -2262,18 +2857,16 @@ function handleTurnInFlight(phase, memory, event, cfg) {
|
|
|
2262
2857
|
return {
|
|
2263
2858
|
phase: {
|
|
2264
2859
|
kind: "awaiting_attention",
|
|
2265
|
-
stopReason: `nudge cap reached: ${nudgeCap} nudge${nudgeCap === 1 ? "" : "s"} (nudgeCap) without
|
|
2860
|
+
stopReason: `nudge cap reached: ${nudgeCap} nudge${nudgeCap === 1 ? "" : "s"} (nudgeCap) without journal progress or a terminal marker`
|
|
2266
2861
|
},
|
|
2267
2862
|
memory: { ...memoryWithHash, nudgeStreak: nextNudgeStreak },
|
|
2268
2863
|
actions: [{ type: "persist" }]
|
|
2269
2864
|
};
|
|
2270
2865
|
}
|
|
2271
2866
|
const promptOverride = buildNudgePrompt(
|
|
2272
|
-
{ ...loop,
|
|
2867
|
+
{ ...loop, journalPath: cfg.journalPromptPath ?? loop.journalPath },
|
|
2273
2868
|
{
|
|
2274
|
-
skeletonNotReplaced: event.
|
|
2275
|
-
TRACKER_SKELETON_MARKER
|
|
2276
|
-
)
|
|
2869
|
+
skeletonNotReplaced: hasSkeletonMarker(event.journalContent)
|
|
2277
2870
|
}
|
|
2278
2871
|
) + sizeNudgeSuffix;
|
|
2279
2872
|
const nextIteration2 = memoryWithHash.iteration + 1;
|
|
@@ -2349,24 +2942,6 @@ function handleBackingOff(phase, memory, event, cfg) {
|
|
|
2349
2942
|
if (event.cancelled) {
|
|
2350
2943
|
return { phase: { kind: "cancelled" }, memory, actions: [{ type: "persist" }] };
|
|
2351
2944
|
}
|
|
2352
|
-
if (phase.resume.kind === "fork") {
|
|
2353
|
-
return {
|
|
2354
|
-
phase: {
|
|
2355
|
-
kind: "handing_over",
|
|
2356
|
-
handle: phase.resume.handle,
|
|
2357
|
-
configOverride: phase.resume.configOverride,
|
|
2358
|
-
retried: true
|
|
2359
|
-
},
|
|
2360
|
-
memory,
|
|
2361
|
-
actions: [
|
|
2362
|
-
{
|
|
2363
|
-
type: "start_fork_turn",
|
|
2364
|
-
handle: phase.resume.handle,
|
|
2365
|
-
configOverride: phase.resume.configOverride
|
|
2366
|
-
}
|
|
2367
|
-
]
|
|
2368
|
-
};
|
|
2369
|
-
}
|
|
2370
2945
|
const continuation = event.adapterSessionId ? { mode: "resume", handle: event.adapterSessionId } : phase.resume.continuation;
|
|
2371
2946
|
const prompt = continuation.mode === "fresh" ? phase.resume.prompt : cfg.loop ? buildContinuePrompt(cfg.loop) : "Continue.";
|
|
2372
2947
|
const prepared = prepareWorkflowTurn(cfg.workflowState, {
|
|
@@ -2397,98 +2972,33 @@ function handleBackingOff(phase, memory, event, cfg) {
|
|
|
2397
2972
|
]
|
|
2398
2973
|
};
|
|
2399
2974
|
}
|
|
2400
|
-
function
|
|
2401
|
-
if (event.
|
|
2402
|
-
return { phase: { kind: "cancelled" }, memory, actions: [{ type: "persist" }] };
|
|
2403
|
-
}
|
|
2975
|
+
function handleAwaitingAttention(phase, memory, event, cfg) {
|
|
2976
|
+
if (event.checkpoint) memory = { ...memory, checkpoint: event.checkpoint };
|
|
2404
2977
|
const nextIteration = memory.iteration + 1;
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2978
|
+
const wakesFresh = memory.parkedAfterHandover || phase.interruption?.kind === "cap_exhausted" && phase.interruption.resource?.fresh === true;
|
|
2979
|
+
if (wakesFresh && memory.checkpoint && !readRestartContract(
|
|
2980
|
+
`## Restart
|
|
2981
|
+
${memory.checkpoint.contract.text}`,
|
|
2982
|
+
cfg.loop?.maxRestartTokens,
|
|
2983
|
+
cfg.runId
|
|
2984
|
+
)) {
|
|
2985
|
+
return parkResource(memory, {
|
|
2986
|
+
cause: "restart",
|
|
2987
|
+
limit: cfg.loop?.maxRestartTokens ?? 2e3,
|
|
2988
|
+
used: estimateTokenCount(memory.checkpoint.contract.text),
|
|
2989
|
+
fresh: true,
|
|
2990
|
+
detail: "Retained Restart checkpoint does not satisfy the current contract limit or Run identity",
|
|
2991
|
+
checkpointPath: memory.checkpoint.path
|
|
2415
2992
|
});
|
|
2416
|
-
return {
|
|
2417
|
-
phase: {
|
|
2418
|
-
kind: "turn_in_flight",
|
|
2419
|
-
prompt: seedPrompt,
|
|
2420
|
-
continuation: continuation2,
|
|
2421
|
-
configOverride: prepared2.configOverride
|
|
2422
|
-
},
|
|
2423
|
-
memory: {
|
|
2424
|
-
...memory,
|
|
2425
|
-
iteration: nextIteration,
|
|
2426
|
-
lastStopPrompt: seedPrompt,
|
|
2427
|
-
lastStopContinuation: continuation2,
|
|
2428
|
-
lastHandoffSizeBytes: event.handoffSizeBytes
|
|
2429
|
-
},
|
|
2430
|
-
actions: [
|
|
2431
|
-
{ type: "purge_handoffs" },
|
|
2432
|
-
{ type: "persist" },
|
|
2433
|
-
{
|
|
2434
|
-
type: "start_turn",
|
|
2435
|
-
prompt: seedPrompt,
|
|
2436
|
-
continuation: continuation2,
|
|
2437
|
-
configOverride: prepared2.configOverride
|
|
2438
|
-
}
|
|
2439
|
-
]
|
|
2440
|
-
};
|
|
2441
|
-
}
|
|
2442
|
-
if (event.transient && !phase.retried) {
|
|
2443
|
-
const ms = cfg.loop?.retryBackoffMs ?? DEFAULT_RETRY_BACKOFF_MS;
|
|
2444
|
-
return {
|
|
2445
|
-
phase: {
|
|
2446
|
-
kind: "backing_off",
|
|
2447
|
-
ms,
|
|
2448
|
-
resume: {
|
|
2449
|
-
kind: "fork",
|
|
2450
|
-
handle: phase.handle,
|
|
2451
|
-
configOverride: phase.configOverride
|
|
2452
|
-
}
|
|
2453
|
-
},
|
|
2454
|
-
memory,
|
|
2455
|
-
actions: [{ type: "wait", ms }]
|
|
2456
|
-
};
|
|
2457
2993
|
}
|
|
2458
|
-
const continuation = { mode: "
|
|
2459
|
-
const
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
kind: "turn_in_flight",
|
|
2467
|
-
prompt: prepared.prompt,
|
|
2468
|
-
continuation,
|
|
2469
|
-
configOverride: prepared.configOverride
|
|
2470
|
-
},
|
|
2471
|
-
memory: {
|
|
2472
|
-
...memory,
|
|
2473
|
-
iteration: nextIteration,
|
|
2474
|
-
lastStopPrompt: prepared.prompt,
|
|
2475
|
-
lastStopContinuation: continuation
|
|
2476
|
-
},
|
|
2477
|
-
actions: [
|
|
2478
|
-
{ type: "degrade_handover", handle: phase.handle },
|
|
2479
|
-
{ type: "persist" },
|
|
2480
|
-
{
|
|
2481
|
-
type: "start_turn",
|
|
2482
|
-
prompt: prepared.prompt,
|
|
2483
|
-
continuation,
|
|
2484
|
-
configOverride: prepared.configOverride
|
|
2485
|
-
}
|
|
2486
|
-
]
|
|
2487
|
-
};
|
|
2488
|
-
}
|
|
2489
|
-
function handleAwaitingAttention(memory, event, cfg) {
|
|
2490
|
-
const nextIteration = memory.iteration + 1;
|
|
2491
|
-
const prompt = buildWakePrompt(cfg.initialPrompt, cfg.trackerPromptPath);
|
|
2994
|
+
const continuation = wakesFresh ? { mode: "fresh" } : event.continuation;
|
|
2995
|
+
const prompt = wakesFresh && memory.checkpoint ? `The human replied: ${cfg.initialPrompt}
|
|
2996
|
+
|
|
2997
|
+
${seedFromRestart(memory.checkpoint.contract, memory.checkpoint.path, cfg.journalPromptPath)}` : buildWakePrompt(
|
|
2998
|
+
cfg.initialPrompt,
|
|
2999
|
+
cfg.journalPromptPath,
|
|
3000
|
+
phase.interruption
|
|
3001
|
+
);
|
|
2492
3002
|
const prepared = prepareWorkflowTurn(cfg.workflowState, {
|
|
2493
3003
|
prompt: cfg.initialPrompt,
|
|
2494
3004
|
iteration: nextIteration,
|
|
@@ -2498,29 +3008,34 @@ function handleAwaitingAttention(memory, event, cfg) {
|
|
|
2498
3008
|
phase: {
|
|
2499
3009
|
kind: "turn_in_flight",
|
|
2500
3010
|
prompt,
|
|
2501
|
-
continuation
|
|
3011
|
+
continuation,
|
|
2502
3012
|
configOverride: prepared.configOverride
|
|
2503
3013
|
},
|
|
2504
3014
|
memory: {
|
|
2505
3015
|
...memory,
|
|
2506
3016
|
iteration: nextIteration,
|
|
2507
3017
|
lastStopPrompt: prompt,
|
|
2508
|
-
lastStopContinuation:
|
|
3018
|
+
lastStopContinuation: continuation,
|
|
3019
|
+
parkedAfterHandover: false,
|
|
3020
|
+
lastRestartHash: wakesFresh && memory.checkpoint ? hashJournalContent(memory.checkpoint.contract.text) : memory.lastRestartHash
|
|
2509
3021
|
},
|
|
2510
3022
|
actions: [
|
|
2511
3023
|
{ type: "persist" },
|
|
2512
3024
|
{
|
|
2513
3025
|
type: "start_turn",
|
|
3026
|
+
restartTokens: wakesFresh ? memory.checkpoint?.contract.tokens : void 0,
|
|
2514
3027
|
prompt,
|
|
2515
|
-
continuation
|
|
3028
|
+
continuation,
|
|
2516
3029
|
configOverride: prepared.configOverride
|
|
2517
3030
|
}
|
|
2518
3031
|
]
|
|
2519
3032
|
};
|
|
2520
3033
|
}
|
|
2521
|
-
function
|
|
3034
|
+
function transition(phase, memory, event, cfg) {
|
|
2522
3035
|
switch (phase.kind) {
|
|
2523
3036
|
case "turn_in_flight": {
|
|
3037
|
+
if (event.type === "steer")
|
|
3038
|
+
return handleSteer(phase, memory, event.steer);
|
|
2524
3039
|
if (event.type !== "turn_finished") {
|
|
2525
3040
|
throw new Error(
|
|
2526
3041
|
`runMachine: phase 'turn_in_flight' received unexpected event '${event.type}'`
|
|
@@ -2529,6 +3044,8 @@ function step(phase, memory, event, cfg) {
|
|
|
2529
3044
|
return handleTurnInFlight(phase, memory, event, cfg);
|
|
2530
3045
|
}
|
|
2531
3046
|
case "backing_off": {
|
|
3047
|
+
if (event.type === "steer")
|
|
3048
|
+
return handleSteer(phase, memory, event.steer);
|
|
2532
3049
|
if (event.type !== "backoff_elapsed") {
|
|
2533
3050
|
throw new Error(
|
|
2534
3051
|
`runMachine: phase 'backing_off' received unexpected event '${event.type}'`
|
|
@@ -2536,21 +3053,13 @@ function step(phase, memory, event, cfg) {
|
|
|
2536
3053
|
}
|
|
2537
3054
|
return handleBackingOff(phase, memory, event, cfg);
|
|
2538
3055
|
}
|
|
2539
|
-
case "handing_over": {
|
|
2540
|
-
if (event.type !== "fork_finished") {
|
|
2541
|
-
throw new Error(
|
|
2542
|
-
`runMachine: phase 'handing_over' received unexpected event '${event.type}'`
|
|
2543
|
-
);
|
|
2544
|
-
}
|
|
2545
|
-
return handleHandingOver(phase, memory, event, cfg);
|
|
2546
|
-
}
|
|
2547
3056
|
case "awaiting_attention": {
|
|
2548
3057
|
if (event.type !== "woken") {
|
|
2549
3058
|
throw new Error(
|
|
2550
3059
|
`runMachine: phase 'awaiting_attention' received unexpected event '${event.type}'`
|
|
2551
3060
|
);
|
|
2552
3061
|
}
|
|
2553
|
-
return handleAwaitingAttention(memory, event, cfg);
|
|
3062
|
+
return handleAwaitingAttention(phase, memory, event, cfg);
|
|
2554
3063
|
}
|
|
2555
3064
|
case "completed":
|
|
2556
3065
|
case "failed":
|
|
@@ -2567,6 +3076,85 @@ function step(phase, memory, event, cfg) {
|
|
|
2567
3076
|
}
|
|
2568
3077
|
}
|
|
2569
3078
|
}
|
|
3079
|
+
function admitResult(result, previous, cfg) {
|
|
3080
|
+
const action = result.actions.find(
|
|
3081
|
+
(a) => a.type === "start_turn" || a.type === "wait"
|
|
3082
|
+
);
|
|
3083
|
+
if (!action) return result;
|
|
3084
|
+
const memory = result.memory;
|
|
3085
|
+
const bounded = memory.contextKey === cfg.contextKey ? memory.lastBoundedTurn : null;
|
|
3086
|
+
const fresh = action.type === "start_turn" && action.continuation.mode === "fresh";
|
|
3087
|
+
const context = bounded?.openingContextTokens != null && bounded.lastContextTokens != null ? {
|
|
3088
|
+
opening: bounded.openingContextTokens,
|
|
3089
|
+
ceiling: bounded.lastContextTokens,
|
|
3090
|
+
required: Math.max(
|
|
3091
|
+
0,
|
|
3092
|
+
(memory.checkpoint?.contract.tokens ?? 0) - (bounded.openingRestartTokens ?? 0)
|
|
3093
|
+
),
|
|
3094
|
+
source: "conservative last API occupancy, not a measured compaction threshold"
|
|
3095
|
+
} : void 0;
|
|
3096
|
+
const stop = admitContinuation({
|
|
3097
|
+
loop: cfg.loop,
|
|
3098
|
+
iteration: memory.iteration,
|
|
3099
|
+
tokens: memory.cumulativeTokens,
|
|
3100
|
+
checkpointPath: memory.checkpoint?.path,
|
|
3101
|
+
context: fresh ? context : void 0
|
|
3102
|
+
});
|
|
3103
|
+
if (!stop)
|
|
3104
|
+
return deliverPendingSteers(
|
|
3105
|
+
fresh ? {
|
|
3106
|
+
...result,
|
|
3107
|
+
memory: { ...memory, activeRestartTokens: action.restartTokens ?? 0 }
|
|
3108
|
+
} : result
|
|
3109
|
+
);
|
|
3110
|
+
return parkResource(
|
|
3111
|
+
{
|
|
3112
|
+
...memory,
|
|
3113
|
+
iteration: previous?.iteration ?? memory.iteration,
|
|
3114
|
+
lastRestartHash: previous?.lastRestartHash,
|
|
3115
|
+
lastStopPrompt: previous?.lastStopPrompt ?? memory.lastStopPrompt,
|
|
3116
|
+
lastStopContinuation: previous?.lastStopContinuation ?? memory.lastStopContinuation,
|
|
3117
|
+
pendingSteers: previous?.pendingSteers ?? memory.pendingSteers
|
|
3118
|
+
},
|
|
3119
|
+
{
|
|
3120
|
+
...stop,
|
|
3121
|
+
fresh: stop.fresh || !!memory.checkpoint || memory.parkedAfterHandover
|
|
3122
|
+
},
|
|
3123
|
+
result.actions.filter(
|
|
3124
|
+
(a) => a.type === "notify_handover_completed" || a.type === "notify_iteration_complete"
|
|
3125
|
+
)
|
|
3126
|
+
);
|
|
3127
|
+
}
|
|
3128
|
+
function parkResource(memory, stop, notifications = []) {
|
|
3129
|
+
const interruption = resourceInterruption(stop);
|
|
3130
|
+
return {
|
|
3131
|
+
phase: {
|
|
3132
|
+
kind: "awaiting_attention",
|
|
3133
|
+
stopReason: interruption.message,
|
|
3134
|
+
interruption
|
|
3135
|
+
},
|
|
3136
|
+
memory: { ...memory, parkedAfterHandover: stop.fresh },
|
|
3137
|
+
actions: [
|
|
3138
|
+
{ type: "record_interruption", interruption },
|
|
3139
|
+
{ type: "persist" },
|
|
3140
|
+
...notifications
|
|
3141
|
+
]
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3144
|
+
function createInitialRun(cfg, opts) {
|
|
3145
|
+
return admitResult(initialRun(cfg, opts), opts.resumedMemory, cfg);
|
|
3146
|
+
}
|
|
3147
|
+
function step(phase, memory, event, cfg) {
|
|
3148
|
+
if (event.type === "resource_exhausted")
|
|
3149
|
+
return parkResource(
|
|
3150
|
+
{
|
|
3151
|
+
...memory,
|
|
3152
|
+
cumulativeTokens: event.cumulativeTokens
|
|
3153
|
+
},
|
|
3154
|
+
{ ...event.stop, checkpointPath: memory.checkpoint?.path }
|
|
3155
|
+
);
|
|
3156
|
+
return admitResult(transition(phase, memory, event, cfg), memory, cfg);
|
|
3157
|
+
}
|
|
2570
3158
|
function serializeRunMemory(memory) {
|
|
2571
3159
|
return JSON.stringify(memory);
|
|
2572
3160
|
}
|
|
@@ -2580,12 +3168,47 @@ function deserializeRunMemory(json) {
|
|
|
2580
3168
|
}
|
|
2581
3169
|
if (!parsed || typeof parsed !== "object") return null;
|
|
2582
3170
|
const candidate = parsed;
|
|
3171
|
+
if (!("lastJournalHash" in candidate) && "lastTrackerHash" in candidate) {
|
|
3172
|
+
candidate.lastJournalHash = candidate.lastTrackerHash;
|
|
3173
|
+
delete candidate.lastTrackerHash;
|
|
3174
|
+
}
|
|
2583
3175
|
const continuation = candidate.lastStopContinuation;
|
|
2584
|
-
if (
|
|
3176
|
+
if (!("pendingSteers" in candidate)) candidate.pendingSteers = [];
|
|
3177
|
+
if (typeof candidate.parkedAfterHandover !== "boolean") {
|
|
3178
|
+
candidate.parkedAfterHandover = false;
|
|
3179
|
+
}
|
|
3180
|
+
if (!isBoundedTurn(candidate.lastBoundedTurn)) {
|
|
3181
|
+
candidate.lastBoundedTurn = null;
|
|
3182
|
+
}
|
|
3183
|
+
if (candidate.checkpoint !== void 0) {
|
|
3184
|
+
const checkpoint = candidate.checkpoint;
|
|
3185
|
+
if (!checkpoint || typeof checkpoint.path !== "string" || !checkpoint.contract || typeof checkpoint.contract.text !== "string" || typeof checkpoint.contract.tokens !== "number" || !Number.isFinite(checkpoint.contract.tokens) || checkpoint.contract.tokens < 0)
|
|
3186
|
+
delete candidate.checkpoint;
|
|
3187
|
+
}
|
|
3188
|
+
if (typeof candidate.lastRestartHash !== "string")
|
|
3189
|
+
delete candidate.lastRestartHash;
|
|
3190
|
+
if (typeof candidate.cumulativeTokens !== "number" || !Number.isFinite(candidate.cumulativeTokens) || candidate.cumulativeTokens < 0)
|
|
3191
|
+
candidate.cumulativeTokens = null;
|
|
3192
|
+
if (typeof candidate.iteration !== "number" || typeof candidate.nudgeStreak !== "number" || typeof candidate.retryStreak !== "number" || candidate.lastJournalHash !== null && typeof candidate.lastJournalHash !== "string" || typeof candidate.lastStopPrompt !== "string" || !continuation || typeof continuation.mode !== "string" || !Array.isArray(candidate.pendingSteers) || !candidate.pendingSteers.every(isQueuedSteer)) {
|
|
2585
3193
|
return null;
|
|
2586
3194
|
}
|
|
2587
3195
|
return candidate;
|
|
2588
3196
|
}
|
|
3197
|
+
function wakesFreshAfterHandover(runMemoryJson) {
|
|
3198
|
+
return deserializeRunMemory(runMemoryJson)?.parkedAfterHandover === true;
|
|
3199
|
+
}
|
|
3200
|
+
function isBoundedTurn(value) {
|
|
3201
|
+
if (!value || typeof value !== "object") return false;
|
|
3202
|
+
const turn = value;
|
|
3203
|
+
return ["openingContextTokens", "lastContextTokens", "toolCalls"].every(
|
|
3204
|
+
(key) => turn[key] === null || typeof turn[key] === "number"
|
|
3205
|
+
);
|
|
3206
|
+
}
|
|
3207
|
+
function isQueuedSteer(value) {
|
|
3208
|
+
if (!value || typeof value !== "object") return false;
|
|
3209
|
+
const steer = value;
|
|
3210
|
+
return typeof steer.text === "string" && (steer.origin === "hub" || steer.origin === "local") && typeof steer.receivedAt === "number";
|
|
3211
|
+
}
|
|
2589
3212
|
|
|
2590
3213
|
// src/core/workflows/workflowRunner.ts
|
|
2591
3214
|
var NULL_TOKENS = {
|
|
@@ -2597,17 +3220,17 @@ var NULL_TOKENS = {
|
|
|
2597
3220
|
contextSize: null,
|
|
2598
3221
|
contextWindowSize: null
|
|
2599
3222
|
};
|
|
2600
|
-
var
|
|
2601
|
-
# Workflow
|
|
3223
|
+
var JOURNAL_SKELETON_TEMPLATE = `${JOURNAL_SKELETON_MARKER}
|
|
3224
|
+
# Workflow Journal
|
|
2602
3225
|
|
|
2603
3226
|
**Session**: {sessionId}
|
|
2604
|
-
**
|
|
3227
|
+
**Journal**: {journalPath}
|
|
2605
3228
|
**Goal**: {input}
|
|
2606
3229
|
|
|
2607
3230
|
---
|
|
2608
3231
|
|
|
2609
|
-
> This
|
|
2610
|
-
> See the Turn Protocol for
|
|
3232
|
+
> This journal was created by the runner. Update it as you work.
|
|
3233
|
+
> See the Turn Protocol for journal conventions.
|
|
2611
3234
|
|
|
2612
3235
|
## Status
|
|
2613
3236
|
|
|
@@ -2621,10 +3244,10 @@ _To be created during orientation._
|
|
|
2621
3244
|
|
|
2622
3245
|
_No progress yet._
|
|
2623
3246
|
`;
|
|
2624
|
-
function openRunSection(
|
|
3247
|
+
function openRunSection(journalPath, opts) {
|
|
2625
3248
|
let existing;
|
|
2626
3249
|
try {
|
|
2627
|
-
existing = fs12.readFileSync(
|
|
3250
|
+
existing = fs12.readFileSync(journalPath, "utf-8");
|
|
2628
3251
|
} catch {
|
|
2629
3252
|
return;
|
|
2630
3253
|
}
|
|
@@ -2641,13 +3264,59 @@ _Sections above belong to earlier Workflow Runs in this Athena Session._
|
|
|
2641
3264
|
`;
|
|
2642
3265
|
try {
|
|
2643
3266
|
fs12.writeFileSync(
|
|
2644
|
-
|
|
3267
|
+
journalPath,
|
|
2645
3268
|
demoteTerminalMarkers(existing.trimEnd(), opts.markers) + banner,
|
|
2646
3269
|
"utf-8"
|
|
2647
3270
|
);
|
|
2648
3271
|
} catch {
|
|
2649
3272
|
}
|
|
2650
3273
|
}
|
|
3274
|
+
function recordSteersInJournal(journalPath, steers, iteration, markers) {
|
|
3275
|
+
let existing;
|
|
3276
|
+
try {
|
|
3277
|
+
existing = fs12.readFileSync(journalPath, "utf-8");
|
|
3278
|
+
} catch {
|
|
3279
|
+
return;
|
|
3280
|
+
}
|
|
3281
|
+
const entries = steers.map((steer) => formatSteerJournalEntry(steer, iteration)).join("");
|
|
3282
|
+
try {
|
|
3283
|
+
fs12.writeFileSync(
|
|
3284
|
+
journalPath,
|
|
3285
|
+
insertAboveTerminalMarker(existing, entries, markers),
|
|
3286
|
+
"utf-8"
|
|
3287
|
+
);
|
|
3288
|
+
} catch {
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
function appendInterruptionNote(journalPath, interruption) {
|
|
3292
|
+
const lines = [
|
|
3293
|
+
"",
|
|
3294
|
+
"",
|
|
3295
|
+
"---",
|
|
3296
|
+
"",
|
|
3297
|
+
"## Needs human (runner note)",
|
|
3298
|
+
"",
|
|
3299
|
+
interruption.message,
|
|
3300
|
+
""
|
|
3301
|
+
];
|
|
3302
|
+
if (interruption.kind === "question") {
|
|
3303
|
+
if (interruption.question) lines.push(`- call: ${interruption.question}`);
|
|
3304
|
+
if (interruption.requestId)
|
|
3305
|
+
lines.push(`- request: ${interruption.requestId}`);
|
|
3306
|
+
lines.push(
|
|
3307
|
+
"",
|
|
3308
|
+
"_Written by the runner: the request above was deferred and this run is parked until a human answers it. On continue the agent re-issues the call; a stored answer is replayed into it without asking again._"
|
|
3309
|
+
);
|
|
3310
|
+
} else {
|
|
3311
|
+
lines.push(
|
|
3312
|
+
"_Written by the runner: this run is parked until a human replies._"
|
|
3313
|
+
);
|
|
3314
|
+
}
|
|
3315
|
+
try {
|
|
3316
|
+
fs12.appendFileSync(journalPath, lines.join("\n") + "\n", "utf-8");
|
|
3317
|
+
} catch {
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
2651
3320
|
function mergeTokens(base, next) {
|
|
2652
3321
|
const input = (base.input ?? 0) + (next.input ?? 0);
|
|
2653
3322
|
const output = (base.output ?? 0) + (next.output ?? 0);
|
|
@@ -2670,9 +3339,6 @@ function mergeTokens(base, next) {
|
|
|
2670
3339
|
contextWindowSize: next.contextWindowSize ?? base.contextWindowSize
|
|
2671
3340
|
};
|
|
2672
3341
|
}
|
|
2673
|
-
function buildHandoffInvocationPrompt(handoffPath) {
|
|
2674
|
-
return `Invoke the handoff skill to write a Handoff file to ${handoffPath}. Do nothing else: no code changes, no tracker updates \u2014 only the Handoff file.`;
|
|
2675
|
-
}
|
|
2676
3342
|
async function delayWithCancel(ms, isCancelled) {
|
|
2677
3343
|
const slice = 250;
|
|
2678
3344
|
for (let waited = 0; waited < ms && !isCancelled(); waited += slice) {
|
|
@@ -2681,36 +3347,30 @@ async function delayWithCancel(ms, isCancelled) {
|
|
|
2681
3347
|
);
|
|
2682
3348
|
}
|
|
2683
3349
|
}
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
3350
|
+
function readUnitRecords(journalAbsPath) {
|
|
3351
|
+
const unitsDir = path10.join(path10.dirname(journalAbsPath), "units");
|
|
3352
|
+
let names;
|
|
2687
3353
|
try {
|
|
2688
|
-
|
|
3354
|
+
names = fs12.readdirSync(unitsDir).filter((name) => name.endsWith(".md"));
|
|
2689
3355
|
} catch {
|
|
2690
3356
|
return [];
|
|
2691
3357
|
}
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
return path10.join(dir, `${String(seq).padStart(3, "0")}.md`);
|
|
2695
|
-
}
|
|
2696
|
-
function nextHandoffPath(dir) {
|
|
2697
|
-
const next = (listHandoffSeqs(dir).at(-1) ?? 0) + 1;
|
|
2698
|
-
fs12.mkdirSync(dir, { recursive: true });
|
|
2699
|
-
return handoffPathFor(dir, next);
|
|
2700
|
-
}
|
|
2701
|
-
function purgeHandoffs(dir, keep) {
|
|
2702
|
-
const seqs = listHandoffSeqs(dir);
|
|
2703
|
-
for (const seq of seqs.slice(0, Math.max(0, seqs.length - keep))) {
|
|
3358
|
+
const records = [];
|
|
3359
|
+
for (const name of names.sort()) {
|
|
2704
3360
|
try {
|
|
2705
|
-
|
|
3361
|
+
records.push({
|
|
3362
|
+
recordPath: `units/${name}`,
|
|
3363
|
+
content: fs12.readFileSync(path10.join(unitsDir, name), "utf-8")
|
|
3364
|
+
});
|
|
2706
3365
|
} catch {
|
|
2707
3366
|
}
|
|
2708
3367
|
}
|
|
3368
|
+
return records;
|
|
2709
3369
|
}
|
|
2710
|
-
function
|
|
2711
|
-
fs12.mkdirSync(path10.dirname(
|
|
3370
|
+
function defaultCreateJournal(journalPath, content) {
|
|
3371
|
+
fs12.mkdirSync(path10.dirname(journalPath), { recursive: true });
|
|
2712
3372
|
try {
|
|
2713
|
-
fs12.writeFileSync(
|
|
3373
|
+
fs12.writeFileSync(journalPath, content, { encoding: "utf-8", flag: "wx" });
|
|
2714
3374
|
} catch (e) {
|
|
2715
3375
|
if (e.code !== "EEXIST") throw e;
|
|
2716
3376
|
}
|
|
@@ -2731,19 +3391,25 @@ function terminalPhaseToStatus(phase) {
|
|
|
2731
3391
|
}
|
|
2732
3392
|
}
|
|
2733
3393
|
function createWorkflowRunner(input) {
|
|
2734
|
-
const runId = input.resumeRunId ??
|
|
3394
|
+
const runId = input.resumeRunId ?? crypto3.randomUUID();
|
|
2735
3395
|
let cancelled = false;
|
|
2736
3396
|
let status = "running";
|
|
2737
|
-
let cumulativeTokens = {
|
|
3397
|
+
let cumulativeTokens = {
|
|
3398
|
+
...input.resumedRunMemory?.usage ?? NULL_TOKENS,
|
|
3399
|
+
total: input.resumedRunMemory?.cumulativeTokens ?? null
|
|
3400
|
+
};
|
|
2738
3401
|
let stopReason;
|
|
3402
|
+
let interruption;
|
|
2739
3403
|
let memory;
|
|
2740
|
-
const
|
|
3404
|
+
const preStartSteers = [];
|
|
3405
|
+
let applySteer = null;
|
|
3406
|
+
const journalResolved = resolveJournalPath({
|
|
2741
3407
|
projectDir: input.projectDir,
|
|
2742
3408
|
sessionId: input.sessionId,
|
|
2743
3409
|
workflow: input.workflow
|
|
2744
3410
|
});
|
|
2745
|
-
const
|
|
2746
|
-
const
|
|
3411
|
+
const journalAbsPath = journalResolved?.absolutePath ?? null;
|
|
3412
|
+
const journalPromptPath = journalResolved?.promptPath;
|
|
2747
3413
|
function snapshot() {
|
|
2748
3414
|
const adapterSessionId = input.currentAdapterSessionId?.() ?? void 0;
|
|
2749
3415
|
return {
|
|
@@ -2754,50 +3420,67 @@ function createWorkflowRunner(input) {
|
|
|
2754
3420
|
maxIterations: input.workflow?.loop?.maxIterations ?? 1,
|
|
2755
3421
|
status,
|
|
2756
3422
|
stopReason,
|
|
2757
|
-
|
|
3423
|
+
journalPath: journalPromptPath,
|
|
2758
3424
|
...adapterSessionId ? { adapterSessionId } : {},
|
|
2759
|
-
...memory ? { runMemoryJson: serializeRunMemory(memory) } : {}
|
|
3425
|
+
...memory ? { runMemoryJson: serializeRunMemory(memory) } : {},
|
|
3426
|
+
...interruption ? { interruption } : {}
|
|
2760
3427
|
};
|
|
2761
3428
|
}
|
|
3429
|
+
let persistenceFailure;
|
|
3430
|
+
const hasPersistenceFailure = () => persistenceFailure !== void 0;
|
|
2762
3431
|
function persist() {
|
|
3432
|
+
if (persistenceFailure) throw persistenceFailure;
|
|
2763
3433
|
try {
|
|
2764
3434
|
input.persistRunState(snapshot());
|
|
2765
|
-
} catch {
|
|
3435
|
+
} catch (cause) {
|
|
3436
|
+
persistenceFailure = new Error(
|
|
3437
|
+
"Workflow state could not be saved; continuation is unsafe.",
|
|
3438
|
+
{ cause }
|
|
3439
|
+
);
|
|
3440
|
+
cancelled = true;
|
|
3441
|
+
input.abortCurrentTurn?.();
|
|
3442
|
+
throw persistenceFailure;
|
|
2766
3443
|
}
|
|
2767
3444
|
}
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
3445
|
+
const phaseTracker = createPhaseTracker();
|
|
3446
|
+
function observePhase(journalContent, turn) {
|
|
3447
|
+
const observation = phaseTracker.observe(journalContent);
|
|
3448
|
+
if (observation.kind === "new_step") {
|
|
3449
|
+
const { name, index, total } = observation.step;
|
|
3450
|
+
input.onPhaseChange?.({
|
|
3451
|
+
runId,
|
|
3452
|
+
turn,
|
|
3453
|
+
step: name,
|
|
3454
|
+
...index !== void 0 ? { stepIndex: index } : {},
|
|
3455
|
+
...total !== void 0 ? { stepTotal: total } : {}
|
|
3456
|
+
});
|
|
3457
|
+
} else if (observation.kind === "malformed" && observation.warning) {
|
|
3458
|
+
input.onWarning?.(observation.warning);
|
|
3459
|
+
}
|
|
2777
3460
|
}
|
|
2778
3461
|
const result = (async () => {
|
|
2779
3462
|
await Promise.resolve();
|
|
2780
|
-
if (
|
|
2781
|
-
const content = substituteVariables(
|
|
3463
|
+
if (journalAbsPath && input.workflow?.loop?.enabled) {
|
|
3464
|
+
const content = substituteVariables(JOURNAL_SKELETON_TEMPLATE, {
|
|
2782
3465
|
sessionId: input.sessionId,
|
|
2783
|
-
|
|
3466
|
+
journalPath: journalPromptPath,
|
|
2784
3467
|
input: input.prompt
|
|
2785
3468
|
});
|
|
2786
|
-
const write = input.
|
|
2787
|
-
const
|
|
2788
|
-
write(
|
|
2789
|
-
if (
|
|
2790
|
-
openRunSection(
|
|
3469
|
+
const write = input.createJournal ?? defaultCreateJournal;
|
|
3470
|
+
const journalExisted = fs12.existsSync(journalAbsPath);
|
|
3471
|
+
write(journalAbsPath, content);
|
|
3472
|
+
if (journalExisted && !input.resumeRunId) {
|
|
3473
|
+
openRunSection(journalAbsPath, {
|
|
2791
3474
|
runId,
|
|
2792
3475
|
goal: input.prompt,
|
|
2793
3476
|
markers: {
|
|
2794
3477
|
completionMarker: input.workflow.loop.completionMarker,
|
|
3478
|
+
needsHumanMarker: input.workflow.loop.needsHumanMarker,
|
|
2795
3479
|
blockedMarker: input.workflow.loop.blockedMarker
|
|
2796
3480
|
}
|
|
2797
3481
|
});
|
|
2798
3482
|
}
|
|
2799
3483
|
}
|
|
2800
|
-
persist();
|
|
2801
3484
|
const workflowState = createWorkflowRunState({
|
|
2802
3485
|
projectDir: input.projectDir,
|
|
2803
3486
|
sessionId: input.sessionId,
|
|
@@ -2806,26 +3489,168 @@ function createWorkflowRunner(input) {
|
|
|
2806
3489
|
});
|
|
2807
3490
|
const loop = input.workflow?.loop;
|
|
2808
3491
|
const cfg = {
|
|
3492
|
+
runId,
|
|
3493
|
+
contextKey: crypto3.randomUUID(),
|
|
2809
3494
|
workflowState,
|
|
2810
3495
|
initialPrompt: input.prompt,
|
|
2811
3496
|
loop,
|
|
2812
|
-
|
|
2813
|
-
|
|
3497
|
+
journalAbsPath,
|
|
3498
|
+
journalPromptPath
|
|
2814
3499
|
};
|
|
2815
3500
|
const initial = createInitialRun(cfg, {
|
|
2816
3501
|
initialContinuation: input.initialContinuation,
|
|
2817
3502
|
waking: !!(input.resumeRunId && loop?.enabled),
|
|
2818
3503
|
resumedMemory: input.resumedRunMemory,
|
|
3504
|
+
initialSteers: preStartSteers.splice(0),
|
|
3505
|
+
parkedInterruption: input.parkedInterruption,
|
|
2819
3506
|
awaitingAttentionStopReason: input.resumedStopReason
|
|
2820
3507
|
});
|
|
2821
3508
|
let phase = initial.phase;
|
|
2822
3509
|
memory = initial.memory;
|
|
3510
|
+
persist();
|
|
3511
|
+
applySteer = (steer) => {
|
|
3512
|
+
if (persistenceFailure) return false;
|
|
3513
|
+
if (isTerminalPhase(phase)) return false;
|
|
3514
|
+
const stepResult = step(phase, memory, { type: "steer", steer }, cfg);
|
|
3515
|
+
phase = stepResult.phase;
|
|
3516
|
+
memory = stepResult.memory;
|
|
3517
|
+
try {
|
|
3518
|
+
runSideEffects(stepResult.actions);
|
|
3519
|
+
} catch (error) {
|
|
3520
|
+
if (!hasPersistenceFailure()) throw error;
|
|
3521
|
+
return false;
|
|
3522
|
+
}
|
|
3523
|
+
return true;
|
|
3524
|
+
};
|
|
3525
|
+
async function executeOperation(turn) {
|
|
3526
|
+
const baseline = { ...cumulativeTokens };
|
|
3527
|
+
const startTokens = memory.cumulativeTokens ?? 0;
|
|
3528
|
+
let operationTokens = 0;
|
|
3529
|
+
let finished = false;
|
|
3530
|
+
let openingChecked = false;
|
|
3531
|
+
let stopped = null;
|
|
3532
|
+
const isStopped = () => stopped !== null;
|
|
3533
|
+
let resolveStop;
|
|
3534
|
+
const stopPromise = new Promise((resolve) => {
|
|
3535
|
+
resolveStop = resolve;
|
|
3536
|
+
});
|
|
3537
|
+
function finalStop(event) {
|
|
3538
|
+
const total = memory.cumulativeTokens ?? startTokens;
|
|
3539
|
+
return {
|
|
3540
|
+
...event,
|
|
3541
|
+
cumulativeTokens: total,
|
|
3542
|
+
stop: {
|
|
3543
|
+
...event.stop,
|
|
3544
|
+
...event.stop.cause === "tokens" ? { used: total } : {}
|
|
3545
|
+
}
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
function stop(reason) {
|
|
3549
|
+
if (finished || stopped || cancelled) return;
|
|
3550
|
+
stopped = reason;
|
|
3551
|
+
const checkpoint = journalAbsPath ? checkpointFromJournal(readJournal(journalAbsPath)) : null;
|
|
3552
|
+
if (checkpoint) memory = { ...memory, checkpoint };
|
|
3553
|
+
input.abortCurrentTurn?.();
|
|
3554
|
+
resolveStop({
|
|
3555
|
+
type: "resource_exhausted",
|
|
3556
|
+
stop: reason,
|
|
3557
|
+
cumulativeTokens: memory.cumulativeTokens ?? startTokens
|
|
3558
|
+
});
|
|
3559
|
+
}
|
|
3560
|
+
function observe(usage, enforce = true) {
|
|
3561
|
+
if (finished) return;
|
|
3562
|
+
if (usage.total !== null && Number.isFinite(usage.total) && usage.total >= 0) {
|
|
3563
|
+
operationTokens = Math.max(operationTokens, usage.total);
|
|
3564
|
+
memory = {
|
|
3565
|
+
...memory,
|
|
3566
|
+
cumulativeTokens: startTokens + operationTokens
|
|
3567
|
+
};
|
|
3568
|
+
cumulativeTokens = {
|
|
3569
|
+
...mergeTokens(baseline, usage),
|
|
3570
|
+
total: memory.cumulativeTokens
|
|
3571
|
+
};
|
|
3572
|
+
memory = { ...memory, usage: cumulativeTokens };
|
|
3573
|
+
persist();
|
|
3574
|
+
}
|
|
3575
|
+
if (!enforce || stopped) return;
|
|
3576
|
+
const limit = admitContinuation({
|
|
3577
|
+
loop,
|
|
3578
|
+
iteration: memory.iteration,
|
|
3579
|
+
tokens: memory.cumulativeTokens
|
|
3580
|
+
});
|
|
3581
|
+
if (limit) {
|
|
3582
|
+
stop(limit);
|
|
3583
|
+
return;
|
|
3584
|
+
}
|
|
3585
|
+
if (!openingChecked && turn.continuation.mode === "fresh" && usage.openingContextSize != null) {
|
|
3586
|
+
openingChecked = true;
|
|
3587
|
+
const previous = memory.contextKey === cfg.contextKey ? memory.lastBoundedTurn : null;
|
|
3588
|
+
const required = memory.checkpoint ? 0 : estimateTokenCount(
|
|
3589
|
+
journalAbsPath ? readJournal(journalAbsPath) : ""
|
|
3590
|
+
);
|
|
3591
|
+
const decision = admitContinuation({
|
|
3592
|
+
loop,
|
|
3593
|
+
iteration: memory.iteration,
|
|
3594
|
+
tokens: memory.cumulativeTokens,
|
|
3595
|
+
context: {
|
|
3596
|
+
opening: usage.openingContextSize,
|
|
3597
|
+
required,
|
|
3598
|
+
ceiling: previous?.lastContextTokens ?? loop?.maxTurnTokenCount ?? DEFAULT_MAX_TURN_TOKEN_COUNT,
|
|
3599
|
+
source: previous ? "conservative prior API occupancy" : "configured ceiling estimate; actual compaction point unknown"
|
|
3600
|
+
}
|
|
3601
|
+
});
|
|
3602
|
+
if (decision) stop(decision);
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
try {
|
|
3606
|
+
const operation = input.startTurn({
|
|
3607
|
+
...turn,
|
|
3608
|
+
onUsage: (usage) => {
|
|
3609
|
+
if (persistenceFailure) return;
|
|
3610
|
+
try {
|
|
3611
|
+
observe(usage);
|
|
3612
|
+
} catch (error) {
|
|
3613
|
+
if (!hasPersistenceFailure()) throw error;
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
});
|
|
3617
|
+
const result2 = await Promise.race([operation, stopPromise]);
|
|
3618
|
+
if (persistenceFailure) throw persistenceFailure;
|
|
3619
|
+
if ("type" in result2) {
|
|
3620
|
+
let timer;
|
|
3621
|
+
try {
|
|
3622
|
+
const final = await Promise.race([
|
|
3623
|
+
operation.catch(() => null),
|
|
3624
|
+
new Promise((resolve) => {
|
|
3625
|
+
timer = setTimeout(() => resolve(null), 6e3);
|
|
3626
|
+
})
|
|
3627
|
+
]);
|
|
3628
|
+
if (final) observe(final.tokens, false);
|
|
3629
|
+
} finally {
|
|
3630
|
+
if (timer) clearTimeout(timer);
|
|
3631
|
+
}
|
|
3632
|
+
return finalStop(result2);
|
|
3633
|
+
}
|
|
3634
|
+
observe(result2.tokens, false);
|
|
3635
|
+
if (isStopped()) return finalStop(await stopPromise);
|
|
3636
|
+
return result2;
|
|
3637
|
+
} finally {
|
|
3638
|
+
finished = true;
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
2823
3641
|
async function performStartTurn(prompt, continuation, configOverride) {
|
|
2824
|
-
const turnResult = await
|
|
2825
|
-
prompt,
|
|
3642
|
+
const turnResult = await executeOperation({
|
|
3643
|
+
prompt: loop?.enabled && journalAbsPath && (continuation.mode === "fresh" || memory.iteration === 1) ? prompt + "\n\n" + restartInstructions(journalAbsPath, runId, loop.maxRestartTokens) : prompt,
|
|
2826
3644
|
continuation,
|
|
2827
|
-
configOverride
|
|
3645
|
+
configOverride,
|
|
3646
|
+
iteration: memory.iteration
|
|
2828
3647
|
});
|
|
3648
|
+
if ("type" in turnResult) return turnResult;
|
|
3649
|
+
const measured = {
|
|
3650
|
+
openingContextTokens: turnResult.tokens.openingContextSize ?? null,
|
|
3651
|
+
lastContextTokens: turnResult.tokens.contextSize,
|
|
3652
|
+
toolCalls: input.currentTurnToolCalls?.() ?? null
|
|
3653
|
+
};
|
|
2829
3654
|
if (cancelled) {
|
|
2830
3655
|
return {
|
|
2831
3656
|
type: "turn_finished",
|
|
@@ -2835,15 +3660,19 @@ function createWorkflowRunner(input) {
|
|
|
2835
3660
|
streamMessage: null,
|
|
2836
3661
|
transportBroken: false,
|
|
2837
3662
|
handoverRequestHandle: null,
|
|
2838
|
-
|
|
3663
|
+
interruption: null,
|
|
2839
3664
|
adapterSessionId: null,
|
|
2840
3665
|
outcome: null,
|
|
2841
|
-
|
|
3666
|
+
journalContent: "",
|
|
3667
|
+
...measured,
|
|
3668
|
+
shedIntegrity: null,
|
|
3669
|
+
cumulativeTokens: null
|
|
2842
3670
|
};
|
|
2843
3671
|
}
|
|
2844
|
-
|
|
3672
|
+
const runTotal = cumulativeTokens.total;
|
|
2845
3673
|
const handoverRequest = input.handover?.takeRequest() ?? null;
|
|
2846
3674
|
if (handoverRequest) {
|
|
3675
|
+
const journalContent2 = loop?.enabled && journalAbsPath ? readJournal(journalAbsPath) : "";
|
|
2847
3676
|
return {
|
|
2848
3677
|
type: "turn_finished",
|
|
2849
3678
|
cancelled: false,
|
|
@@ -2852,14 +3681,18 @@ function createWorkflowRunner(input) {
|
|
|
2852
3681
|
streamMessage: null,
|
|
2853
3682
|
transportBroken: false,
|
|
2854
3683
|
handoverRequestHandle: handoverRequest.handle,
|
|
2855
|
-
|
|
3684
|
+
interruption: null,
|
|
2856
3685
|
adapterSessionId: null,
|
|
2857
3686
|
outcome: null,
|
|
2858
|
-
|
|
3687
|
+
journalContent: journalContent2,
|
|
3688
|
+
checkpoint: journalAbsPath ? checkpointFromJournal(journalContent2) : null,
|
|
3689
|
+
...measured,
|
|
3690
|
+
shedIntegrity: observeShedIntegrity(journalContent2),
|
|
3691
|
+
cumulativeTokens: runTotal
|
|
2859
3692
|
};
|
|
2860
3693
|
}
|
|
2861
|
-
const
|
|
2862
|
-
if (
|
|
3694
|
+
const interruption2 = input.checkInterruption?.() ?? null;
|
|
3695
|
+
if (interruption2) {
|
|
2863
3696
|
return {
|
|
2864
3697
|
type: "turn_finished",
|
|
2865
3698
|
cancelled: false,
|
|
@@ -2868,10 +3701,13 @@ function createWorkflowRunner(input) {
|
|
|
2868
3701
|
streamMessage: null,
|
|
2869
3702
|
transportBroken: false,
|
|
2870
3703
|
handoverRequestHandle: null,
|
|
2871
|
-
|
|
3704
|
+
interruption: interruption2,
|
|
2872
3705
|
adapterSessionId: null,
|
|
2873
3706
|
outcome: null,
|
|
2874
|
-
|
|
3707
|
+
journalContent: "",
|
|
3708
|
+
...measured,
|
|
3709
|
+
shedIntegrity: null,
|
|
3710
|
+
cumulativeTokens: runTotal
|
|
2875
3711
|
};
|
|
2876
3712
|
}
|
|
2877
3713
|
const adapterSessionId = input.currentAdapterSessionId?.() ?? null;
|
|
@@ -2889,23 +3725,27 @@ function createWorkflowRunner(input) {
|
|
|
2889
3725
|
streamMessage: turnResult.streamMessage,
|
|
2890
3726
|
transportBroken: false,
|
|
2891
3727
|
handoverRequestHandle: null,
|
|
2892
|
-
|
|
3728
|
+
interruption: null,
|
|
2893
3729
|
adapterSessionId,
|
|
2894
3730
|
outcome: null,
|
|
2895
|
-
|
|
3731
|
+
journalContent: "",
|
|
3732
|
+
...measured,
|
|
3733
|
+
shedIntegrity: null,
|
|
3734
|
+
cumulativeTokens: runTotal
|
|
2896
3735
|
};
|
|
2897
3736
|
}
|
|
2898
3737
|
const transport = turnResult.diagnostics?.transport;
|
|
2899
3738
|
const transportBroken = !!(transport && transport.streamToolUses > 0 && transport.preToolUseEvents === 0);
|
|
2900
|
-
let
|
|
3739
|
+
let journalContent = "";
|
|
2901
3740
|
let outcome = null;
|
|
2902
|
-
if (!transportBroken && loop?.enabled &&
|
|
2903
|
-
|
|
3741
|
+
if (!transportBroken && loop?.enabled && journalAbsPath) {
|
|
3742
|
+
journalContent = readJournal(journalAbsPath);
|
|
2904
3743
|
outcome = resolveTurnOutcome({
|
|
2905
|
-
|
|
3744
|
+
journalPath: journalAbsPath,
|
|
2906
3745
|
loop,
|
|
2907
3746
|
iteration: memory.iteration
|
|
2908
3747
|
});
|
|
3748
|
+
observePhase(journalContent, memory.iteration);
|
|
2909
3749
|
}
|
|
2910
3750
|
return {
|
|
2911
3751
|
type: "turn_finished",
|
|
@@ -2915,57 +3755,34 @@ function createWorkflowRunner(input) {
|
|
|
2915
3755
|
streamMessage: turnResult.streamMessage,
|
|
2916
3756
|
transportBroken,
|
|
2917
3757
|
handoverRequestHandle: null,
|
|
2918
|
-
|
|
3758
|
+
interruption: null,
|
|
2919
3759
|
adapterSessionId,
|
|
2920
3760
|
outcome,
|
|
2921
|
-
|
|
3761
|
+
journalContent,
|
|
3762
|
+
checkpoint: checkpointFromJournal(journalContent),
|
|
3763
|
+
...measured,
|
|
3764
|
+
shedIntegrity: journalContent === "" ? null : observeShedIntegrity(journalContent),
|
|
3765
|
+
cumulativeTokens: runTotal
|
|
2922
3766
|
};
|
|
2923
3767
|
}
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
const handoffAbsPath = nextHandoffPath(handoffDir);
|
|
2927
|
-
input.handover?.onForkStateChange?.(true);
|
|
2928
|
-
let forkOk = false;
|
|
2929
|
-
let transient = false;
|
|
3768
|
+
function observeShedIntegrity(journalContent) {
|
|
3769
|
+
if (!journalAbsPath || !loop?.enabled) return null;
|
|
2930
3770
|
try {
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
forkOk = !forkResult.error && (forkResult.exitCode === null || forkResult.exitCode === 0) && fs12.existsSync(handoffAbsPath);
|
|
2938
|
-
if (!forkOk) {
|
|
2939
|
-
transient = classifyTurnFailure({
|
|
2940
|
-
errorMessage: forkResult.error?.message,
|
|
2941
|
-
lastStderr: forkResult.stderrTail ?? forkResult.lastStderr,
|
|
2942
|
-
lastMessage: forkResult.streamMessage
|
|
2943
|
-
}).kind === "transient";
|
|
2944
|
-
}
|
|
2945
|
-
} catch (e) {
|
|
2946
|
-
forkOk = false;
|
|
2947
|
-
transient = classifyTurnFailure({
|
|
2948
|
-
errorMessage: e instanceof Error ? e.message : String(e)
|
|
2949
|
-
}).kind === "transient";
|
|
2950
|
-
} finally {
|
|
2951
|
-
input.handover?.onForkStateChange?.(false);
|
|
2952
|
-
}
|
|
2953
|
-
let handoffSizeBytes = null;
|
|
2954
|
-
if (forkOk) {
|
|
2955
|
-
try {
|
|
2956
|
-
handoffSizeBytes = fs12.statSync(handoffAbsPath).size;
|
|
2957
|
-
} catch {
|
|
2958
|
-
handoffSizeBytes = null;
|
|
2959
|
-
}
|
|
3771
|
+
return checkShedIntegrity(
|
|
3772
|
+
journalContent,
|
|
3773
|
+
readUnitRecords(journalAbsPath)
|
|
3774
|
+
);
|
|
3775
|
+
} catch {
|
|
3776
|
+
return null;
|
|
2960
3777
|
}
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
};
|
|
3778
|
+
}
|
|
3779
|
+
function checkpointFromJournal(content) {
|
|
3780
|
+
const contract = readRestartContract(
|
|
3781
|
+
content,
|
|
3782
|
+
loop?.maxRestartTokens,
|
|
3783
|
+
runId
|
|
3784
|
+
);
|
|
3785
|
+
return contract && journalAbsPath ? { path: journalAbsPath, contract } : null;
|
|
2969
3786
|
}
|
|
2970
3787
|
async function performWait(ms) {
|
|
2971
3788
|
await delayWithCancel(ms, () => cancelled);
|
|
@@ -2980,9 +3797,9 @@ function createWorkflowRunner(input) {
|
|
|
2980
3797
|
return { type: "backoff_elapsed", cancelled: false, adapterSessionId };
|
|
2981
3798
|
}
|
|
2982
3799
|
function isKickoffAction(action) {
|
|
2983
|
-
return action.type === "start_turn" || action.type === "
|
|
3800
|
+
return action.type === "start_turn" || action.type === "wait";
|
|
2984
3801
|
}
|
|
2985
|
-
|
|
3802
|
+
function runSideEffects(actions) {
|
|
2986
3803
|
let kickoff = null;
|
|
2987
3804
|
for (const action of actions) {
|
|
2988
3805
|
if (isKickoffAction(action)) {
|
|
@@ -2992,25 +3809,58 @@ function createWorkflowRunner(input) {
|
|
|
2992
3809
|
switch (action.type) {
|
|
2993
3810
|
case "persist":
|
|
2994
3811
|
persist();
|
|
2995
|
-
if (
|
|
3812
|
+
if (journalAbsPath && input.projectTasks) {
|
|
2996
3813
|
try {
|
|
2997
|
-
const tasks =
|
|
3814
|
+
const tasks = projectJournalTasks(journalAbsPath);
|
|
2998
3815
|
if (tasks) input.projectTasks(tasks);
|
|
2999
3816
|
} catch {
|
|
3000
3817
|
}
|
|
3001
3818
|
}
|
|
3002
3819
|
break;
|
|
3820
|
+
case "warn":
|
|
3821
|
+
input.onWarning?.(action.message);
|
|
3822
|
+
break;
|
|
3003
3823
|
case "notify_iteration_complete":
|
|
3004
|
-
input.onIterationComplete?.(snapshot());
|
|
3824
|
+
input.onIterationComplete?.(snapshot(), cumulativeTokens);
|
|
3005
3825
|
break;
|
|
3006
|
-
case "
|
|
3007
|
-
|
|
3826
|
+
case "notify_handover_completed":
|
|
3827
|
+
input.onHandoverCompleted?.({
|
|
3828
|
+
...action.completion,
|
|
3829
|
+
tokens: cumulativeTokens
|
|
3830
|
+
});
|
|
3008
3831
|
break;
|
|
3009
|
-
case "
|
|
3010
|
-
|
|
3832
|
+
case "steers_delivered":
|
|
3833
|
+
if (journalAbsPath && loop?.enabled) {
|
|
3834
|
+
recordSteersInJournal(
|
|
3835
|
+
journalAbsPath,
|
|
3836
|
+
action.steers,
|
|
3837
|
+
action.iteration,
|
|
3838
|
+
{
|
|
3839
|
+
completionMarker: loop.completionMarker,
|
|
3840
|
+
needsHumanMarker: loop.needsHumanMarker,
|
|
3841
|
+
blockedMarker: loop.blockedMarker
|
|
3842
|
+
}
|
|
3843
|
+
);
|
|
3844
|
+
}
|
|
3845
|
+
input.onSteerDelivered?.(
|
|
3846
|
+
action.steers.map((steer) => ({
|
|
3847
|
+
...steer,
|
|
3848
|
+
iteration: action.iteration
|
|
3849
|
+
}))
|
|
3850
|
+
);
|
|
3851
|
+
break;
|
|
3852
|
+
case "record_interruption":
|
|
3853
|
+
interruption = action.interruption;
|
|
3854
|
+
if (journalAbsPath && !(action.interruption.kind === "cap_exhausted" && action.interruption.resource)) {
|
|
3855
|
+
appendInterruptionNote(journalAbsPath, action.interruption);
|
|
3856
|
+
}
|
|
3011
3857
|
break;
|
|
3012
3858
|
}
|
|
3013
3859
|
}
|
|
3860
|
+
return kickoff;
|
|
3861
|
+
}
|
|
3862
|
+
async function runActions(actions) {
|
|
3863
|
+
const kickoff = runSideEffects(actions);
|
|
3014
3864
|
if (!kickoff) return null;
|
|
3015
3865
|
switch (kickoff.type) {
|
|
3016
3866
|
case "start_turn":
|
|
@@ -3019,37 +3869,29 @@ function createWorkflowRunner(input) {
|
|
|
3019
3869
|
kickoff.continuation,
|
|
3020
3870
|
kickoff.configOverride
|
|
3021
3871
|
);
|
|
3022
|
-
case "start_fork_turn":
|
|
3023
|
-
return performForkTurn(kickoff.handle, kickoff.configOverride);
|
|
3024
3872
|
case "wait":
|
|
3025
3873
|
return performWait(kickoff.ms);
|
|
3026
3874
|
default:
|
|
3027
3875
|
return null;
|
|
3028
3876
|
}
|
|
3029
3877
|
}
|
|
3030
|
-
let bootstrapActions;
|
|
3031
|
-
if (phase.kind === "awaiting_attention") {
|
|
3878
|
+
let bootstrapActions = initial.actions;
|
|
3879
|
+
if (phase.kind === "awaiting_attention" && input.resumeRunId && initial.actions.length === 0) {
|
|
3032
3880
|
const wokenEvent = {
|
|
3033
3881
|
type: "woken",
|
|
3034
|
-
continuation: input.initialContinuation ?? { mode: "fresh" }
|
|
3882
|
+
continuation: input.initialContinuation ?? { mode: "fresh" },
|
|
3883
|
+
// Prefer a repaired current-run checkpoint when waking.
|
|
3884
|
+
checkpoint: journalAbsPath ? checkpointFromJournal(readJournal(journalAbsPath)) : null
|
|
3035
3885
|
};
|
|
3036
3886
|
const stepResult = step(phase, memory, wokenEvent, cfg);
|
|
3037
3887
|
phase = stepResult.phase;
|
|
3038
3888
|
memory = stepResult.memory;
|
|
3039
3889
|
bootstrapActions = stepResult.actions;
|
|
3040
|
-
}
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
continuation: phase.continuation,
|
|
3046
|
-
configOverride: phase.configOverride
|
|
3047
|
-
}
|
|
3048
|
-
];
|
|
3049
|
-
} else if (phase.kind === "backing_off") {
|
|
3050
|
-
bootstrapActions = [{ type: "wait", ms: phase.ms }];
|
|
3051
|
-
} else {
|
|
3052
|
-
bootstrapActions = [];
|
|
3890
|
+
}
|
|
3891
|
+
if (isTerminalPhase(phase)) {
|
|
3892
|
+
const terminal = terminalPhaseToStatus(phase);
|
|
3893
|
+
status = terminal.status;
|
|
3894
|
+
stopReason = terminal.stopReason;
|
|
3053
3895
|
}
|
|
3054
3896
|
let pendingEvent = await runActions(bootstrapActions);
|
|
3055
3897
|
while (pendingEvent) {
|
|
@@ -3070,9 +3912,19 @@ function createWorkflowRunner(input) {
|
|
|
3070
3912
|
status,
|
|
3071
3913
|
iterations: memory.iteration,
|
|
3072
3914
|
stopReason,
|
|
3915
|
+
...interruption ? { interruption } : {},
|
|
3073
3916
|
tokens: cumulativeTokens
|
|
3074
3917
|
};
|
|
3075
|
-
})()
|
|
3918
|
+
})().catch((error) => {
|
|
3919
|
+
if (!persistenceFailure) throw error;
|
|
3920
|
+
return {
|
|
3921
|
+
runId,
|
|
3922
|
+
status: "failed",
|
|
3923
|
+
iterations: memory?.iteration ?? 0,
|
|
3924
|
+
stopReason: persistenceFailure.message,
|
|
3925
|
+
tokens: cumulativeTokens
|
|
3926
|
+
};
|
|
3927
|
+
});
|
|
3076
3928
|
return {
|
|
3077
3929
|
runId,
|
|
3078
3930
|
result,
|
|
@@ -3082,109 +3934,11 @@ function createWorkflowRunner(input) {
|
|
|
3082
3934
|
kill() {
|
|
3083
3935
|
cancelled = true;
|
|
3084
3936
|
input.abortCurrentTurn?.();
|
|
3085
|
-
}
|
|
3086
|
-
};
|
|
3087
|
-
}
|
|
3088
|
-
|
|
3089
|
-
// src/core/workflows/useWorkflowSessionController.ts
|
|
3090
|
-
function useWorkflowSessionController(base, input) {
|
|
3091
|
-
const [isRunning, setIsRunning] = useState(false);
|
|
3092
|
-
const runnerRef = useRef(null);
|
|
3093
|
-
const activeRunIdRef = useRef(null);
|
|
3094
|
-
const cancelCurrentRun = useCallback(async () => {
|
|
3095
|
-
const runner = runnerRef.current;
|
|
3096
|
-
if (runner) {
|
|
3097
|
-
runner.kill();
|
|
3098
|
-
await runner.result.catch(() => {
|
|
3099
|
-
});
|
|
3100
|
-
runnerRef.current = null;
|
|
3101
|
-
activeRunIdRef.current = null;
|
|
3102
|
-
}
|
|
3103
|
-
}, []);
|
|
3104
|
-
const interrupt = useCallback(() => {
|
|
3105
|
-
const runner = runnerRef.current;
|
|
3106
|
-
if (runner) {
|
|
3107
|
-
runner.kill();
|
|
3108
|
-
runnerRef.current = null;
|
|
3109
|
-
activeRunIdRef.current = null;
|
|
3110
|
-
} else {
|
|
3111
|
-
void base.kill().catch(() => {
|
|
3112
|
-
});
|
|
3113
|
-
}
|
|
3114
|
-
setIsRunning(false);
|
|
3115
|
-
}, [base]);
|
|
3116
|
-
const kill = useCallback(async () => {
|
|
3117
|
-
if (runnerRef.current) {
|
|
3118
|
-
await cancelCurrentRun();
|
|
3119
|
-
} else {
|
|
3120
|
-
await base.kill();
|
|
3121
|
-
}
|
|
3122
|
-
setIsRunning(false);
|
|
3123
|
-
}, [base, cancelCurrentRun]);
|
|
3124
|
-
const spawn = useCallback(
|
|
3125
|
-
async (prompt, continuation, _configOverride) => {
|
|
3126
|
-
await cancelCurrentRun();
|
|
3127
|
-
setIsRunning(true);
|
|
3128
|
-
const handle = createWorkflowRunner({
|
|
3129
|
-
sessionId: input.sessionId ?? "",
|
|
3130
|
-
projectDir: input.projectDir,
|
|
3131
|
-
harness: input.harness,
|
|
3132
|
-
workflow: input.workflow,
|
|
3133
|
-
prompt,
|
|
3134
|
-
initialContinuation: continuation,
|
|
3135
|
-
startTurn: (turnInput) => base.startTurn(
|
|
3136
|
-
turnInput.prompt,
|
|
3137
|
-
turnInput.continuation,
|
|
3138
|
-
turnInput.configOverride
|
|
3139
|
-
),
|
|
3140
|
-
persistRunState: input.persistRunState ?? (() => {
|
|
3141
|
-
}),
|
|
3142
|
-
abortCurrentTurn: () => void base.kill().catch(() => {
|
|
3143
|
-
})
|
|
3144
|
-
});
|
|
3145
|
-
runnerRef.current = handle;
|
|
3146
|
-
activeRunIdRef.current = handle.runId;
|
|
3147
|
-
try {
|
|
3148
|
-
const runResult = await handle.result;
|
|
3149
|
-
return {
|
|
3150
|
-
exitCode: runResult.status === "failed" ? 1 : 0,
|
|
3151
|
-
error: runResult.status === "failed" ? new Error(runResult.stopReason ?? "Run failed") : null,
|
|
3152
|
-
tokens: runResult.tokens,
|
|
3153
|
-
streamMessage: null
|
|
3154
|
-
};
|
|
3155
|
-
} finally {
|
|
3156
|
-
if (runnerRef.current === handle) {
|
|
3157
|
-
runnerRef.current = null;
|
|
3158
|
-
activeRunIdRef.current = null;
|
|
3159
|
-
setIsRunning(false);
|
|
3160
|
-
}
|
|
3161
|
-
}
|
|
3162
3937
|
},
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
input.sessionId,
|
|
3168
|
-
input.harness,
|
|
3169
|
-
input.workflow,
|
|
3170
|
-
input.persistRunState
|
|
3171
|
-
]
|
|
3172
|
-
);
|
|
3173
|
-
useEffect(() => {
|
|
3174
|
-
return () => {
|
|
3175
|
-
runnerRef.current?.kill();
|
|
3176
|
-
runnerRef.current = null;
|
|
3177
|
-
activeRunIdRef.current = null;
|
|
3178
|
-
};
|
|
3179
|
-
}, []);
|
|
3180
|
-
return {
|
|
3181
|
-
...base,
|
|
3182
|
-
startTurn: spawn,
|
|
3183
|
-
isRunning,
|
|
3184
|
-
interrupt,
|
|
3185
|
-
kill,
|
|
3186
|
-
get activeRunId() {
|
|
3187
|
-
return activeRunIdRef.current;
|
|
3938
|
+
steer(steer) {
|
|
3939
|
+
if (applySteer) return applySteer(steer);
|
|
3940
|
+
preStartSteers.push(steer);
|
|
3941
|
+
return true;
|
|
3188
3942
|
}
|
|
3189
3943
|
};
|
|
3190
3944
|
}
|
|
@@ -3216,9 +3970,11 @@ function collectMcpServersWithOptions(pluginDirs) {
|
|
|
3216
3970
|
|
|
3217
3971
|
export {
|
|
3218
3972
|
DEFAULT_MAX_TURN_TOKEN_COUNT,
|
|
3973
|
+
DEFAULT_PERMISSION_GRACE_MS,
|
|
3974
|
+
createSteerQueue,
|
|
3219
3975
|
deserializeRunMemory,
|
|
3976
|
+
wakesFreshAfterHandover,
|
|
3220
3977
|
createWorkflowRunner,
|
|
3221
|
-
useWorkflowSessionController,
|
|
3222
3978
|
isMarketplaceRef,
|
|
3223
3979
|
findMarketplaceRepoDir,
|
|
3224
3980
|
resolveWorkflowMarketplaceSource,
|
|
@@ -3239,6 +3995,8 @@ export {
|
|
|
3239
3995
|
installWorkflowPlugins,
|
|
3240
3996
|
resolveWorkflowPlugins,
|
|
3241
3997
|
listBuiltinWorkflows,
|
|
3998
|
+
readWorkflowSourceMetadata,
|
|
3999
|
+
workflowRegistryDir,
|
|
3242
4000
|
resolveWorkflow,
|
|
3243
4001
|
installWorkflowFromSource,
|
|
3244
4002
|
updateWorkflow,
|
|
@@ -3248,4 +4006,4 @@ export {
|
|
|
3248
4006
|
compileWorkflowPlan,
|
|
3249
4007
|
collectMcpServersWithOptions
|
|
3250
4008
|
};
|
|
3251
|
-
//# sourceMappingURL=chunk-
|
|
4009
|
+
//# sourceMappingURL=chunk-QVRTU2MM.js.map
|