@drisp/cli 0.5.26 → 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-5FENJHBW.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-QVRTU2MM.js +4009 -0
- package/dist/{chunk-QYB6N2OT.js → chunk-USK5VSQ7.js} +12 -2
- package/dist/cli.js +9891 -10147
- package/dist/dashboard-daemon.js +7 -248
- package/dist/hook-forwarder.js +2 -1
- package/dist/runner.js +11 -0
- package/package.json +14 -7
- package/dist/athena-gateway.js +0 -4148
- package/dist/chunk-3U37HT4T.js +0 -1088
- package/dist/chunk-4NX4WFPB.js +0 -76
- package/dist/chunk-73V7GXV6.js +0 -2229
- package/dist/chunk-YZ7RCBKO.js +0 -18563
|
@@ -0,0 +1,4009 @@
|
|
|
1
|
+
// src/infra/plugins/workflowResolver.ts
|
|
2
|
+
import fs3 from "fs";
|
|
3
|
+
import path2 from "path";
|
|
4
|
+
|
|
5
|
+
// src/infra/plugins/marketplaceShared.ts
|
|
6
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
7
|
+
import fs2 from "fs";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import path from "path";
|
|
10
|
+
|
|
11
|
+
// src/infra/plugins/marketplaceRefresh.ts
|
|
12
|
+
import { execFileSync } from "child_process";
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
var MarketplaceRefreshError = class extends Error {
|
|
15
|
+
kind;
|
|
16
|
+
marketplace;
|
|
17
|
+
constructor(outcome) {
|
|
18
|
+
super(outcome.message);
|
|
19
|
+
this.name = "MarketplaceRefreshError";
|
|
20
|
+
this.kind = outcome.kind;
|
|
21
|
+
this.marketplace = outcome.marketplace;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var NETWORK_OR_AUTH_SIGNATURES = [
|
|
25
|
+
"could not resolve host",
|
|
26
|
+
"could not resolve proxy",
|
|
27
|
+
"couldn\u2019t resolve host",
|
|
28
|
+
"name or service not known",
|
|
29
|
+
"temporary failure in name resolution",
|
|
30
|
+
"failed to connect",
|
|
31
|
+
"connection timed out",
|
|
32
|
+
"connection refused",
|
|
33
|
+
"connection reset",
|
|
34
|
+
"network is unreachable",
|
|
35
|
+
"network unavailable",
|
|
36
|
+
"unable to access",
|
|
37
|
+
"ssl certificate problem",
|
|
38
|
+
"gnutls_handshake",
|
|
39
|
+
"authentication failed",
|
|
40
|
+
"could not read username",
|
|
41
|
+
"could not read password",
|
|
42
|
+
"permission denied (publickey)",
|
|
43
|
+
"terminal prompts disabled",
|
|
44
|
+
"invalid username or password",
|
|
45
|
+
"403 forbidden",
|
|
46
|
+
"access denied",
|
|
47
|
+
"repository not found",
|
|
48
|
+
"remote: not found"
|
|
49
|
+
];
|
|
50
|
+
function classifyGitFailure(message) {
|
|
51
|
+
const haystack = message.toLowerCase();
|
|
52
|
+
for (const signature of NETWORK_OR_AUTH_SIGNATURES) {
|
|
53
|
+
if (haystack.includes(signature)) {
|
|
54
|
+
return "network-or-auth";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return "unrecoverable-cache";
|
|
58
|
+
}
|
|
59
|
+
function repoUrlFor(owner, repo) {
|
|
60
|
+
return `https://github.com/${owner}/${repo}.git`;
|
|
61
|
+
}
|
|
62
|
+
var GIT_STDIO = ["ignore", "ignore", "pipe"];
|
|
63
|
+
function gitFailureText(error) {
|
|
64
|
+
const err = error;
|
|
65
|
+
const stderr = err.stderr == null ? "" : err.stderr.toString().trim();
|
|
66
|
+
const message = err.message ?? "";
|
|
67
|
+
return stderr ? `${message} ${stderr}`.trim() : message;
|
|
68
|
+
}
|
|
69
|
+
function cloneInto(repoUrl, repoDir) {
|
|
70
|
+
fs.mkdirSync(repoDir, { recursive: true });
|
|
71
|
+
try {
|
|
72
|
+
execFileSync("git", ["clone", "--depth", "1", repoUrl, repoDir], {
|
|
73
|
+
stdio: GIT_STDIO
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
fs.rmSync(repoDir, { recursive: true, force: true });
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function failureOutcome(kind, marketplace, cause, backupDir) {
|
|
81
|
+
const message = kind === "network-or-auth" ? `Could not refresh the "${marketplace}" marketplace: the remote could not be reached (connectivity or authentication problem). ${cause}` : `Could not refresh the "${marketplace}" marketplace: the cached copy is corrupt and could not be rebuilt.${backupDir ? ` Previous cache preserved at ${backupDir}.` : ""} ${cause}`;
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
kind,
|
|
85
|
+
marketplace,
|
|
86
|
+
message,
|
|
87
|
+
cause,
|
|
88
|
+
...backupDir ? { backupDir } : {}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function refreshMarketplaceRepo(owner, repo) {
|
|
92
|
+
const marketplace = `${owner}/${repo}`;
|
|
93
|
+
const repoDir = marketplaceRepoCacheDir(owner, repo);
|
|
94
|
+
const repoUrl = repoUrlFor(owner, repo);
|
|
95
|
+
if (!fs.existsSync(repoDir)) {
|
|
96
|
+
try {
|
|
97
|
+
cloneInto(repoUrl, repoDir);
|
|
98
|
+
return { ok: true, repoDir };
|
|
99
|
+
} catch (cloneError) {
|
|
100
|
+
const cause = gitFailureText(cloneError);
|
|
101
|
+
return failureOutcome(classifyGitFailure(cause), marketplace, cause);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
execFileSync("git", ["pull", "--ff-only"], {
|
|
106
|
+
cwd: repoDir,
|
|
107
|
+
stdio: GIT_STDIO
|
|
108
|
+
});
|
|
109
|
+
return { ok: true, repoDir };
|
|
110
|
+
} catch {
|
|
111
|
+
const backupDir = `${repoDir}.backup-${Date.now()}`;
|
|
112
|
+
try {
|
|
113
|
+
fs.renameSync(repoDir, backupDir);
|
|
114
|
+
cloneInto(repoUrl, repoDir);
|
|
115
|
+
return { ok: true, repoDir, selfHealed: true, backupDir };
|
|
116
|
+
} catch (recoveryError) {
|
|
117
|
+
const cause = gitFailureText(recoveryError);
|
|
118
|
+
return failureOutcome(
|
|
119
|
+
classifyGitFailure(cause),
|
|
120
|
+
marketplace,
|
|
121
|
+
cause,
|
|
122
|
+
backupDir
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function ensureFailureOutcome(kind, marketplace, cause) {
|
|
128
|
+
const message = kind === "network-or-auth" ? `Could not reach the "${marketplace}" marketplace: the remote could not be reached (connectivity or authentication problem). ${cause}` : `Could not clone the "${marketplace}" marketplace: the local cache could not be created. ${cause}`;
|
|
129
|
+
return { ok: false, kind, marketplace, message, cause };
|
|
130
|
+
}
|
|
131
|
+
function ensureRepo(owner, repo) {
|
|
132
|
+
const repoDir = marketplaceRepoCacheDir(owner, repo);
|
|
133
|
+
if (!fs.existsSync(repoDir)) {
|
|
134
|
+
const marketplace = `${owner}/${repo}`;
|
|
135
|
+
try {
|
|
136
|
+
cloneInto(repoUrlFor(owner, repo), repoDir);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const cause = gitFailureText(error);
|
|
139
|
+
throw new MarketplaceRefreshError(
|
|
140
|
+
ensureFailureOutcome(classifyGitFailure(cause), marketplace, cause)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return repoDir;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/infra/plugins/marketplaceShared.ts
|
|
148
|
+
var MARKETPLACE_REF_RE = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
149
|
+
var MARKETPLACE_SLUG_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
150
|
+
function marketplacesCacheDir() {
|
|
151
|
+
return path.join(os.homedir(), ".config", "athena", "marketplaces");
|
|
152
|
+
}
|
|
153
|
+
function marketplaceRepoCacheDir(owner, repo) {
|
|
154
|
+
return path.join(marketplacesCacheDir(), owner, repo);
|
|
155
|
+
}
|
|
156
|
+
function resolvePluginManifestPath(repoDir) {
|
|
157
|
+
const preferredManifestPath = path.join(
|
|
158
|
+
repoDir,
|
|
159
|
+
".agents",
|
|
160
|
+
"plugins",
|
|
161
|
+
"marketplace.json"
|
|
162
|
+
);
|
|
163
|
+
const legacyManifestPath = resolveLegacyPluginManifestPath(repoDir);
|
|
164
|
+
return fs2.existsSync(preferredManifestPath) ? preferredManifestPath : legacyManifestPath;
|
|
165
|
+
}
|
|
166
|
+
function resolveLegacyPluginManifestPath(repoDir) {
|
|
167
|
+
return path.join(repoDir, ".claude-plugin", "marketplace.json");
|
|
168
|
+
}
|
|
169
|
+
function readManifest(manifestPath) {
|
|
170
|
+
if (!fs2.existsSync(manifestPath)) {
|
|
171
|
+
throw new Error(`Marketplace manifest not found: ${manifestPath}`);
|
|
172
|
+
}
|
|
173
|
+
return JSON.parse(
|
|
174
|
+
fs2.readFileSync(manifestPath, "utf-8")
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
function entryRelativeSourcePath(entry, pluginName) {
|
|
178
|
+
let sourcePath;
|
|
179
|
+
if (typeof entry.source === "string") {
|
|
180
|
+
sourcePath = entry.source;
|
|
181
|
+
} else {
|
|
182
|
+
const sourceRecord = entry.source;
|
|
183
|
+
if (sourceRecord.source !== "local" || typeof sourceRecord.path !== "string") {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Plugin "${pluginName}" uses a remote source type which is not supported by athena-cli. Only local relative path sources are supported.`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
sourcePath = sourceRecord.path;
|
|
189
|
+
}
|
|
190
|
+
if (!sourcePath.startsWith("./")) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Plugin "${pluginName}" source must start with "./": ${sourcePath}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const relativeSourcePath = sourcePath.slice(2);
|
|
196
|
+
if (relativeSourcePath.length === 0) {
|
|
197
|
+
throw new Error(`Plugin "${pluginName}" source must not be empty`);
|
|
198
|
+
}
|
|
199
|
+
if (relativeSourcePath.includes("\\")) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Plugin "${pluginName}" source must stay within the marketplace root: ${sourcePath}`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const segments = relativeSourcePath.split("/");
|
|
205
|
+
if (segments.some(
|
|
206
|
+
(segment) => segment === "" || segment === "." || segment === ".."
|
|
207
|
+
)) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Plugin "${pluginName}" source must stay within the marketplace root: ${sourcePath}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return relativeSourcePath;
|
|
213
|
+
}
|
|
214
|
+
function resolvePluginDirFromManifest(pluginName, repoDir, manifestPath) {
|
|
215
|
+
const manifest = readManifest(manifestPath);
|
|
216
|
+
if (!Array.isArray(manifest.plugins)) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Invalid marketplace manifest at ${manifestPath}: "plugins" must be an array`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const entry = manifest.plugins.find((p) => p.name === pluginName);
|
|
222
|
+
if (!entry) {
|
|
223
|
+
const available = manifest.plugins.map((p) => p.name).join(", ");
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Plugin "${pluginName}" not found in marketplace manifest ${manifestPath}. Available plugins: ${available}`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const pluginDir = path.join(
|
|
229
|
+
repoDir,
|
|
230
|
+
entryRelativeSourcePath(entry, pluginName)
|
|
231
|
+
);
|
|
232
|
+
if (!fs2.existsSync(pluginDir)) {
|
|
233
|
+
throw new Error(`Plugin source directory not found: ${pluginDir}`);
|
|
234
|
+
}
|
|
235
|
+
return pluginDir;
|
|
236
|
+
}
|
|
237
|
+
function isMarketplaceRef(entry) {
|
|
238
|
+
return MARKETPLACE_REF_RE.test(entry);
|
|
239
|
+
}
|
|
240
|
+
function isMarketplaceSlug(entry) {
|
|
241
|
+
return MARKETPLACE_SLUG_RE.test(entry);
|
|
242
|
+
}
|
|
243
|
+
function parseRef(ref) {
|
|
244
|
+
const atIdx = ref.indexOf("@");
|
|
245
|
+
const pluginName = ref.slice(0, atIdx);
|
|
246
|
+
const slug = ref.slice(atIdx + 1);
|
|
247
|
+
const slashIdx = slug.indexOf("/");
|
|
248
|
+
return {
|
|
249
|
+
pluginName,
|
|
250
|
+
owner: slug.slice(0, slashIdx),
|
|
251
|
+
repo: slug.slice(slashIdx + 1)
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function buildMarketplacePluginResolution(ref, repoDir, manifestPath) {
|
|
255
|
+
const { pluginName } = parseRef(ref);
|
|
256
|
+
const pluginDir = resolvePluginDirFromManifest(
|
|
257
|
+
pluginName,
|
|
258
|
+
repoDir,
|
|
259
|
+
manifestPath
|
|
260
|
+
);
|
|
261
|
+
const version = resolvePluginVersionFromDir(pluginDir);
|
|
262
|
+
const artifactLayout = version ? resolvePackagedArtifactLayout(pluginDir, version) : void 0;
|
|
263
|
+
return {
|
|
264
|
+
ref,
|
|
265
|
+
pluginName,
|
|
266
|
+
marketplacePath: artifactLayout?.codexMarketplacePath ?? manifestPath,
|
|
267
|
+
pluginDir: artifactLayout?.claudeArtifactDir ?? pluginDir,
|
|
268
|
+
codexPluginDir: artifactLayout?.codexPluginDir ?? pluginDir
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function isPathWithinRoot(rootDir, targetPath) {
|
|
272
|
+
const relative = path.relative(rootDir, targetPath);
|
|
273
|
+
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
274
|
+
}
|
|
275
|
+
function resolvePluginVersionFromDir(pluginDir) {
|
|
276
|
+
const manifestPaths = [
|
|
277
|
+
path.join(pluginDir, ".codex-plugin", "plugin.json"),
|
|
278
|
+
path.join(pluginDir, ".claude-plugin", "plugin.json")
|
|
279
|
+
];
|
|
280
|
+
for (const manifestPath of manifestPaths) {
|
|
281
|
+
if (!fs2.existsSync(manifestPath)) continue;
|
|
282
|
+
try {
|
|
283
|
+
const manifest = JSON.parse(fs2.readFileSync(manifestPath, "utf-8"));
|
|
284
|
+
if (typeof manifest.version === "string" && manifest.version.length > 0) {
|
|
285
|
+
return manifest.version;
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return void 0;
|
|
291
|
+
}
|
|
292
|
+
function resolvePackagedArtifactLayout(pluginRoot, version) {
|
|
293
|
+
const releasePath = path.join(pluginRoot, "dist", version, "release.json");
|
|
294
|
+
if (!fs2.existsSync(releasePath)) {
|
|
295
|
+
return void 0;
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const release = JSON.parse(
|
|
299
|
+
fs2.readFileSync(releasePath, "utf-8")
|
|
300
|
+
);
|
|
301
|
+
if (release.version !== version) {
|
|
302
|
+
return void 0;
|
|
303
|
+
}
|
|
304
|
+
const claudePath = release.artifacts?.claude?.path;
|
|
305
|
+
const codexMarketplacePath = release.artifacts?.codex?.marketplacePath;
|
|
306
|
+
const codexPluginPath = release.artifacts?.codex?.pluginPath;
|
|
307
|
+
if (typeof claudePath !== "string" || typeof codexMarketplacePath !== "string" || typeof codexPluginPath !== "string") {
|
|
308
|
+
return void 0;
|
|
309
|
+
}
|
|
310
|
+
const releaseDir = path.dirname(releasePath);
|
|
311
|
+
const claudeArtifactDir = path.resolve(releaseDir, claudePath);
|
|
312
|
+
const resolvedCodexMarketplacePath = path.resolve(
|
|
313
|
+
releaseDir,
|
|
314
|
+
codexMarketplacePath
|
|
315
|
+
);
|
|
316
|
+
const codexPluginDir = path.resolve(releaseDir, codexPluginPath);
|
|
317
|
+
if (!isPathWithinRoot(releaseDir, claudeArtifactDir) || !isPathWithinRoot(releaseDir, resolvedCodexMarketplacePath) || !isPathWithinRoot(releaseDir, codexPluginDir)) {
|
|
318
|
+
return void 0;
|
|
319
|
+
}
|
|
320
|
+
if (!fs2.existsSync(claudeArtifactDir) || !fs2.existsSync(resolvedCodexMarketplacePath) || !fs2.existsSync(codexPluginDir)) {
|
|
321
|
+
return void 0;
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
claudeArtifactDir,
|
|
325
|
+
codexMarketplacePath: resolvedCodexMarketplacePath,
|
|
326
|
+
codexPluginDir
|
|
327
|
+
};
|
|
328
|
+
} catch {
|
|
329
|
+
return void 0;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function requireGitForMarketplace(kind) {
|
|
333
|
+
try {
|
|
334
|
+
execFileSync2("git", ["--version"], { stdio: "ignore" });
|
|
335
|
+
} catch {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`git is not installed. Install git to use marketplace ${kind}.`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/infra/plugins/workflowSourceErrors.ts
|
|
343
|
+
var WorkflowAmbiguityError = class extends Error {
|
|
344
|
+
workflowName;
|
|
345
|
+
candidates;
|
|
346
|
+
constructor(workflowName, candidates) {
|
|
347
|
+
const list = candidates.map((c) => ` - ${c.sourceLabel}: use ${c.disambiguator}`).join("\n");
|
|
348
|
+
super(
|
|
349
|
+
`Workflow "${workflowName}" is provided by multiple configured marketplaces:
|
|
350
|
+
${list}
|
|
351
|
+
Run \`athena-flow workflow install <disambiguator>\` to pick one.`
|
|
352
|
+
);
|
|
353
|
+
this.name = "WorkflowAmbiguityError";
|
|
354
|
+
this.workflowName = workflowName;
|
|
355
|
+
this.candidates = candidates;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
var WorkflowNotFoundError = class extends Error {
|
|
359
|
+
workflowName;
|
|
360
|
+
searchedSources;
|
|
361
|
+
constructor(workflowName, searchedSources) {
|
|
362
|
+
const sourceList = searchedSources.length ? searchedSources.join(", ") : "(no marketplaces configured)";
|
|
363
|
+
super(
|
|
364
|
+
`Workflow "${workflowName}" not found in any configured marketplace (searched: ${sourceList}). If this workflow was recently added, run \`athena-flow marketplace refresh\` and try again.`
|
|
365
|
+
);
|
|
366
|
+
this.name = "WorkflowNotFoundError";
|
|
367
|
+
this.workflowName = workflowName;
|
|
368
|
+
this.searchedSources = searchedSources;
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
var WorkflowVersionNotFoundError = class extends Error {
|
|
372
|
+
workflowName;
|
|
373
|
+
requestedVersion;
|
|
374
|
+
availableVersion;
|
|
375
|
+
sourceLabel;
|
|
376
|
+
constructor(workflowName, requestedVersion, availableVersion, sourceLabel) {
|
|
377
|
+
const availableText = availableVersion ? `found version ${availableVersion}` : "marketplace entry does not declare a version";
|
|
378
|
+
super(
|
|
379
|
+
`Workflow "${workflowName}" version ${requestedVersion} not found in ${sourceLabel} (${availableText}).`
|
|
380
|
+
);
|
|
381
|
+
this.name = "WorkflowVersionNotFoundError";
|
|
382
|
+
this.workflowName = workflowName;
|
|
383
|
+
this.requestedVersion = requestedVersion;
|
|
384
|
+
this.availableVersion = availableVersion;
|
|
385
|
+
this.sourceLabel = sourceLabel;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// src/infra/plugins/workflowResolver.ts
|
|
390
|
+
function resolveWorkflowManifestPath(repoDir) {
|
|
391
|
+
const preferred = path2.join(repoDir, ".athena-workflow", "marketplace.json");
|
|
392
|
+
const legacy = path2.join(repoDir, ".claude-plugin", "marketplace.json");
|
|
393
|
+
return fs3.existsSync(preferred) ? preferred : legacy;
|
|
394
|
+
}
|
|
395
|
+
function preferCanonicalWorkflowPath(repoDir, workflowPath) {
|
|
396
|
+
const relativePath = path2.relative(repoDir, workflowPath);
|
|
397
|
+
const segments = relativePath.split(path2.sep);
|
|
398
|
+
if (segments[0] !== ".workflows") return workflowPath;
|
|
399
|
+
const canonical = path2.join(repoDir, "workflows", ...segments.slice(1));
|
|
400
|
+
return fs3.existsSync(canonical) ? canonical : workflowPath;
|
|
401
|
+
}
|
|
402
|
+
function resolveWorkflowEntryPath(entry, manifest, repoDir) {
|
|
403
|
+
if (typeof entry.source !== "string") {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`Workflow "${entry.name}" uses a remote source type which is not supported.`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
let sourcePath = entry.source;
|
|
409
|
+
const { workflowRoot } = manifest.metadata ?? {};
|
|
410
|
+
if (workflowRoot && !path2.isAbsolute(sourcePath) && !sourcePath.startsWith("./") && !sourcePath.startsWith("../")) {
|
|
411
|
+
sourcePath = path2.join(workflowRoot, sourcePath);
|
|
412
|
+
}
|
|
413
|
+
const workflowPath = path2.resolve(repoDir, sourcePath);
|
|
414
|
+
if (!workflowPath.startsWith(repoDir + path2.sep) && workflowPath !== repoDir) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
`Workflow "${entry.name}" source resolves outside the marketplace repo: ${workflowPath}`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const resolved = preferCanonicalWorkflowPath(repoDir, workflowPath);
|
|
420
|
+
if (!fs3.existsSync(resolved)) {
|
|
421
|
+
throw new Error(`Workflow source not found: ${resolved}`);
|
|
422
|
+
}
|
|
423
|
+
return resolved;
|
|
424
|
+
}
|
|
425
|
+
function resolveWorkflowPathFromManifest(workflowName, repoDir, manifestPath) {
|
|
426
|
+
const manifest = readManifest(manifestPath);
|
|
427
|
+
const workflows = manifest.workflows ?? [];
|
|
428
|
+
const entry = workflows.find((w) => w.name === workflowName);
|
|
429
|
+
if (!entry) {
|
|
430
|
+
const available = workflows.map((w) => w.name).join(", ") || "(none)";
|
|
431
|
+
throw new Error(
|
|
432
|
+
`Workflow "${workflowName}" not found in marketplace manifest ${manifestPath}. Available workflows: ${available}`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
return resolveWorkflowEntryPath(entry, manifest, repoDir);
|
|
436
|
+
}
|
|
437
|
+
function listWorkflowEntriesFromManifest(repoDir, manifestPath, source) {
|
|
438
|
+
const manifest = readManifest(manifestPath);
|
|
439
|
+
const workflows = manifest.workflows ?? [];
|
|
440
|
+
return workflows.filter(
|
|
441
|
+
(entry) => typeof entry.source === "string"
|
|
442
|
+
).map((entry) => ({
|
|
443
|
+
name: entry.name,
|
|
444
|
+
description: entry.description,
|
|
445
|
+
version: entry.version,
|
|
446
|
+
workflowPath: resolveWorkflowEntryPath(entry, manifest, repoDir),
|
|
447
|
+
ref: source.kind === "remote" ? `${entry.name}@${source.owner}/${source.repo}` : void 0,
|
|
448
|
+
source
|
|
449
|
+
}));
|
|
450
|
+
}
|
|
451
|
+
function findMarketplaceRepoDir(startPath) {
|
|
452
|
+
let currentDir = path2.resolve(startPath);
|
|
453
|
+
for (; ; ) {
|
|
454
|
+
if (fs3.existsSync(resolveWorkflowManifestPath(currentDir)) || fs3.existsSync(resolvePluginManifestPath(currentDir))) {
|
|
455
|
+
return currentDir;
|
|
456
|
+
}
|
|
457
|
+
const parentDir = path2.dirname(currentDir);
|
|
458
|
+
if (parentDir === currentDir) return void 0;
|
|
459
|
+
currentDir = parentDir;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function resolveWorkflowMarketplaceSource(source) {
|
|
463
|
+
const trimmed = source.trim();
|
|
464
|
+
const resolvedPath = path2.resolve(trimmed);
|
|
465
|
+
if (!fs3.existsSync(resolvedPath) && isMarketplaceSlug(trimmed)) {
|
|
466
|
+
const slashIdx = trimmed.indexOf("/");
|
|
467
|
+
return {
|
|
468
|
+
kind: "remote",
|
|
469
|
+
slug: trimmed,
|
|
470
|
+
owner: trimmed.slice(0, slashIdx),
|
|
471
|
+
repo: trimmed.slice(slashIdx + 1)
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
const repoDir = findMarketplaceRepoDir(trimmed);
|
|
475
|
+
if (!repoDir) {
|
|
476
|
+
throw new Error(
|
|
477
|
+
`Local marketplace not found from source: ${trimmed}. Expected a marketplace repo root or a path inside one.`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
return { kind: "local", path: resolvedPath, repoDir };
|
|
481
|
+
}
|
|
482
|
+
function listMarketplaceWorkflows(owner, repo) {
|
|
483
|
+
requireGitForMarketplace("workflows");
|
|
484
|
+
const repoDir = ensureRepo(owner, repo);
|
|
485
|
+
return listWorkflowEntriesFromManifest(
|
|
486
|
+
repoDir,
|
|
487
|
+
resolveWorkflowManifestPath(repoDir),
|
|
488
|
+
{ kind: "remote", slug: `${owner}/${repo}`, owner, repo }
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
function listMarketplaceWorkflowsFromRepo(repoDir) {
|
|
492
|
+
return listWorkflowEntriesFromManifest(
|
|
493
|
+
repoDir,
|
|
494
|
+
resolveWorkflowManifestPath(repoDir),
|
|
495
|
+
{ kind: "local", repoDir }
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
function resolveMarketplaceWorkflow(ref) {
|
|
499
|
+
requireGitForMarketplace("workflows");
|
|
500
|
+
const { pluginName: workflowName, owner, repo } = parseRef(ref);
|
|
501
|
+
const repoDir = ensureRepo(owner, repo);
|
|
502
|
+
return resolveWorkflowPathFromManifest(
|
|
503
|
+
workflowName,
|
|
504
|
+
repoDir,
|
|
505
|
+
resolveWorkflowManifestPath(repoDir)
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
function gatherMarketplaceWorkflowSources(source) {
|
|
509
|
+
const trimmed = source.trim();
|
|
510
|
+
const resolvedPath = path2.resolve(trimmed);
|
|
511
|
+
if (fs3.existsSync(resolvedPath) && fs3.statSync(resolvedPath).isFile()) {
|
|
512
|
+
return [{ kind: "filesystem", workflowPath: fs3.realpathSync(resolvedPath) }];
|
|
513
|
+
}
|
|
514
|
+
if (!fs3.existsSync(resolvedPath) && isMarketplaceSlug(trimmed)) {
|
|
515
|
+
const slashIdx = trimmed.indexOf("/");
|
|
516
|
+
const owner = trimmed.slice(0, slashIdx);
|
|
517
|
+
const repo = trimmed.slice(slashIdx + 1);
|
|
518
|
+
requireGitForMarketplace("workflows");
|
|
519
|
+
const repoDir2 = ensureRepo(owner, repo);
|
|
520
|
+
const manifestPath2 = resolveWorkflowManifestPath(repoDir2);
|
|
521
|
+
return listWorkflowEntriesFromManifest(repoDir2, manifestPath2, {
|
|
522
|
+
kind: "remote",
|
|
523
|
+
slug: trimmed,
|
|
524
|
+
owner,
|
|
525
|
+
repo
|
|
526
|
+
}).map((entry) => ({
|
|
527
|
+
kind: "marketplace-remote",
|
|
528
|
+
slug: trimmed,
|
|
529
|
+
owner,
|
|
530
|
+
repo,
|
|
531
|
+
workflowName: entry.name,
|
|
532
|
+
version: entry.version,
|
|
533
|
+
ref: entry.ref,
|
|
534
|
+
manifestPath: manifestPath2,
|
|
535
|
+
workflowPath: entry.workflowPath
|
|
536
|
+
}));
|
|
537
|
+
}
|
|
538
|
+
const repoDir = findMarketplaceRepoDir(trimmed);
|
|
539
|
+
if (!repoDir) {
|
|
540
|
+
throw new Error(
|
|
541
|
+
`Marketplace source not found: ${trimmed}. Expected a marketplace repo root, a path inside one, or an owner/repo slug.`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
const canonicalRepoDir = fs3.realpathSync(repoDir);
|
|
545
|
+
const manifestPath = resolveWorkflowManifestPath(canonicalRepoDir);
|
|
546
|
+
return listWorkflowEntriesFromManifest(canonicalRepoDir, manifestPath, {
|
|
547
|
+
kind: "local",
|
|
548
|
+
repoDir: canonicalRepoDir
|
|
549
|
+
}).map((entry) => ({
|
|
550
|
+
kind: "marketplace-local",
|
|
551
|
+
repoDir: canonicalRepoDir,
|
|
552
|
+
workflowName: entry.name,
|
|
553
|
+
version: entry.version,
|
|
554
|
+
manifestPath,
|
|
555
|
+
workflowPath: entry.workflowPath
|
|
556
|
+
}));
|
|
557
|
+
}
|
|
558
|
+
function parseBareWorkflowName(source) {
|
|
559
|
+
const atIdx = source.indexOf("@");
|
|
560
|
+
if (atIdx <= 0 || atIdx === source.length - 1) {
|
|
561
|
+
return { bareName: source, pinnedVersion: void 0 };
|
|
562
|
+
}
|
|
563
|
+
const suffix = source.slice(atIdx + 1);
|
|
564
|
+
if (suffix.includes("/")) {
|
|
565
|
+
return { bareName: source, pinnedVersion: void 0 };
|
|
566
|
+
}
|
|
567
|
+
return { bareName: source.slice(0, atIdx), pinnedVersion: suffix };
|
|
568
|
+
}
|
|
569
|
+
function resolvedSourceLabel(s) {
|
|
570
|
+
if (s.kind === "marketplace-remote") return `marketplace ${s.slug}`;
|
|
571
|
+
if (s.kind === "marketplace-local") return `local marketplace ${s.repoDir}`;
|
|
572
|
+
return `file ${s.workflowPath}`;
|
|
573
|
+
}
|
|
574
|
+
function resolvedSourceDisambiguator(s) {
|
|
575
|
+
if (s.kind === "marketplace-remote") return s.ref;
|
|
576
|
+
return s.workflowPath;
|
|
577
|
+
}
|
|
578
|
+
function resolveWorkflowInstall(sourceOrName, configuredSources) {
|
|
579
|
+
if (isMarketplaceRef(sourceOrName)) {
|
|
580
|
+
const { pluginName: workflowName, owner, repo } = parseRef(sourceOrName);
|
|
581
|
+
requireGitForMarketplace("workflows");
|
|
582
|
+
const repoDir = ensureRepo(owner, repo);
|
|
583
|
+
const manifestPath = resolveWorkflowManifestPath(repoDir);
|
|
584
|
+
const workflowPath = resolveWorkflowPathFromManifest(
|
|
585
|
+
workflowName,
|
|
586
|
+
repoDir,
|
|
587
|
+
manifestPath
|
|
588
|
+
);
|
|
589
|
+
const entry = listWorkflowEntriesFromManifest(repoDir, manifestPath, {
|
|
590
|
+
kind: "remote",
|
|
591
|
+
slug: `${owner}/${repo}`,
|
|
592
|
+
owner,
|
|
593
|
+
repo
|
|
594
|
+
}).find((e) => e.name === workflowName);
|
|
595
|
+
return {
|
|
596
|
+
kind: "marketplace-remote",
|
|
597
|
+
slug: `${owner}/${repo}`,
|
|
598
|
+
owner,
|
|
599
|
+
repo,
|
|
600
|
+
workflowName,
|
|
601
|
+
version: entry?.version,
|
|
602
|
+
ref: sourceOrName,
|
|
603
|
+
manifestPath,
|
|
604
|
+
workflowPath
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
const resolvedPath = path2.resolve(sourceOrName);
|
|
608
|
+
if (fs3.existsSync(resolvedPath) && fs3.statSync(resolvedPath).isFile()) {
|
|
609
|
+
return { kind: "filesystem", workflowPath: fs3.realpathSync(resolvedPath) };
|
|
610
|
+
}
|
|
611
|
+
const { bareName, pinnedVersion } = parseBareWorkflowName(sourceOrName);
|
|
612
|
+
if (bareName.includes("/") || bareName.includes("\\")) {
|
|
613
|
+
throw new Error(`Workflow source not found: ${sourceOrName}`);
|
|
614
|
+
}
|
|
615
|
+
const allListings = [];
|
|
616
|
+
let versionMismatch;
|
|
617
|
+
for (const configured of configuredSources) {
|
|
618
|
+
let sources;
|
|
619
|
+
try {
|
|
620
|
+
sources = gatherMarketplaceWorkflowSources(configured);
|
|
621
|
+
} catch {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
for (const src of sources) {
|
|
625
|
+
if ((src.kind === "marketplace-remote" || src.kind === "marketplace-local") && src.workflowName === bareName) {
|
|
626
|
+
if (pinnedVersion !== void 0 && src.version !== pinnedVersion) {
|
|
627
|
+
versionMismatch ??= new WorkflowVersionNotFoundError(
|
|
628
|
+
bareName,
|
|
629
|
+
pinnedVersion,
|
|
630
|
+
src.version,
|
|
631
|
+
resolvedSourceLabel(src)
|
|
632
|
+
);
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
allListings.push(src);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (allListings.length === 0) {
|
|
640
|
+
if (versionMismatch) throw versionMismatch;
|
|
641
|
+
throw new WorkflowNotFoundError(bareName, configuredSources);
|
|
642
|
+
}
|
|
643
|
+
if (allListings.length > 1) {
|
|
644
|
+
const candidates = allListings.map((s) => ({
|
|
645
|
+
sourceLabel: resolvedSourceLabel(s),
|
|
646
|
+
disambiguator: resolvedSourceDisambiguator(s)
|
|
647
|
+
}));
|
|
648
|
+
throw new WorkflowAmbiguityError(bareName, candidates);
|
|
649
|
+
}
|
|
650
|
+
return allListings[0];
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/infra/plugins/versionedPluginResolution.ts
|
|
654
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
655
|
+
import fs4 from "fs";
|
|
656
|
+
import os2 from "os";
|
|
657
|
+
import path3 from "path";
|
|
658
|
+
var PLUGIN_NPM_SCOPE = "@athenaflow";
|
|
659
|
+
function pluginNpmPackageName(pluginName) {
|
|
660
|
+
return `${PLUGIN_NPM_SCOPE}/plugin-${pluginName}`;
|
|
661
|
+
}
|
|
662
|
+
function versionedPluginCacheDir() {
|
|
663
|
+
return path3.join(os2.homedir(), ".config", "athena", "plugin-packages");
|
|
664
|
+
}
|
|
665
|
+
function resolveVersionedPluginDir(owner, repo, pluginName, version) {
|
|
666
|
+
const cacheDir = path3.join(
|
|
667
|
+
versionedPluginCacheDir(),
|
|
668
|
+
owner,
|
|
669
|
+
repo,
|
|
670
|
+
pluginName,
|
|
671
|
+
version
|
|
672
|
+
);
|
|
673
|
+
return fs4.existsSync(cacheDir) ? cacheDir : void 0;
|
|
674
|
+
}
|
|
675
|
+
function fetchPluginPackage(owner, repo, pluginName, version) {
|
|
676
|
+
const npmPkg = pluginNpmPackageName(pluginName);
|
|
677
|
+
const destDir = path3.join(
|
|
678
|
+
versionedPluginCacheDir(),
|
|
679
|
+
owner,
|
|
680
|
+
repo,
|
|
681
|
+
pluginName,
|
|
682
|
+
version
|
|
683
|
+
);
|
|
684
|
+
if (fs4.existsSync(destDir)) {
|
|
685
|
+
return destDir;
|
|
686
|
+
}
|
|
687
|
+
const cacheBase = versionedPluginCacheDir();
|
|
688
|
+
fs4.mkdirSync(cacheBase, { recursive: true });
|
|
689
|
+
const tmpDir = fs4.mkdtempSync(path3.join(cacheBase, ".tmp-"));
|
|
690
|
+
try {
|
|
691
|
+
const tarball = execFileSync3(
|
|
692
|
+
"npm",
|
|
693
|
+
["pack", `${npmPkg}@${version}`, "--pack-destination", tmpDir],
|
|
694
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
695
|
+
).trim();
|
|
696
|
+
const tarballPath = path3.join(tmpDir, tarball);
|
|
697
|
+
if (!fs4.existsSync(tarballPath)) {
|
|
698
|
+
throw new Error(`npm pack did not produce expected tarball: ${tarball}`);
|
|
699
|
+
}
|
|
700
|
+
const extractDir = path3.join(tmpDir, "extracted");
|
|
701
|
+
fs4.mkdirSync(extractDir, { recursive: true });
|
|
702
|
+
execFileSync3("tar", ["xzf", tarballPath, "-C", extractDir], {
|
|
703
|
+
stdio: "ignore"
|
|
704
|
+
});
|
|
705
|
+
const packageDir = path3.join(extractDir, "package");
|
|
706
|
+
if (!fs4.existsSync(packageDir)) {
|
|
707
|
+
throw new Error(
|
|
708
|
+
"Extracted tarball does not contain a package/ directory"
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
fs4.mkdirSync(path3.dirname(destDir), { recursive: true });
|
|
712
|
+
try {
|
|
713
|
+
fs4.renameSync(packageDir, destDir);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
716
|
+
if ((code === "EEXIST" || code === "ENOTEMPTY") && fs4.existsSync(destDir)) {
|
|
717
|
+
return destDir;
|
|
718
|
+
}
|
|
719
|
+
throw error;
|
|
720
|
+
}
|
|
721
|
+
return destDir;
|
|
722
|
+
} finally {
|
|
723
|
+
fs4.rmSync(tmpDir, { recursive: true, force: true });
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
function resolvePackagedArtifactsOrThrow(pluginName, version, pluginRoot, stage) {
|
|
727
|
+
const artifactLayout = resolvePackagedArtifactLayout(pluginRoot, version);
|
|
728
|
+
if (!artifactLayout) {
|
|
729
|
+
throw new Error(
|
|
730
|
+
`${stage} for plugin "${pluginName}" version ${version} is missing packaged runtime artifacts`
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
return artifactLayout;
|
|
734
|
+
}
|
|
735
|
+
function readSourceFallbackEntry(repoDir, pluginName) {
|
|
736
|
+
const manifestPath = resolvePluginManifestPath(repoDir);
|
|
737
|
+
const manifest = readManifest(manifestPath);
|
|
738
|
+
const entry = manifest.plugins.find((plugin) => plugin.name === pluginName);
|
|
739
|
+
if (!entry) {
|
|
740
|
+
return void 0;
|
|
741
|
+
}
|
|
742
|
+
const pluginRoot = path3.join(
|
|
743
|
+
repoDir,
|
|
744
|
+
entryRelativeSourcePath(entry, pluginName)
|
|
745
|
+
);
|
|
746
|
+
const version = entry.version ?? resolvePluginVersionFromDir(pluginRoot);
|
|
747
|
+
return { pluginRoot, version };
|
|
748
|
+
}
|
|
749
|
+
function resolveMarketplaceSourceFallback(ref, pluginName, version, repoDir) {
|
|
750
|
+
const entry = readSourceFallbackEntry(repoDir, pluginName);
|
|
751
|
+
if (!entry || entry.version !== version) {
|
|
752
|
+
return void 0;
|
|
753
|
+
}
|
|
754
|
+
resolvePackagedArtifactsOrThrow(
|
|
755
|
+
pluginName,
|
|
756
|
+
version,
|
|
757
|
+
entry.pluginRoot,
|
|
758
|
+
"marketplace source fallback"
|
|
759
|
+
);
|
|
760
|
+
return buildMarketplacePluginResolution(
|
|
761
|
+
ref,
|
|
762
|
+
repoDir,
|
|
763
|
+
resolvePluginManifestPath(repoDir)
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
function describeSourceFallbackRejection(owner, repo, pluginName, version, repoDir) {
|
|
767
|
+
if (!fs4.existsSync(repoDir)) {
|
|
768
|
+
return `source fallback was unavailable because marketplace repo ${owner}/${repo} is not cached locally`;
|
|
769
|
+
}
|
|
770
|
+
let entry;
|
|
771
|
+
try {
|
|
772
|
+
entry = readSourceFallbackEntry(repoDir, pluginName);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
return `source fallback could not determine the marketplace source version: ${error.message}`;
|
|
775
|
+
}
|
|
776
|
+
if (!entry?.version) {
|
|
777
|
+
return `source fallback was considered, but marketplace repo ${owner}/${repo} does not expose a version for plugin "${pluginName}"`;
|
|
778
|
+
}
|
|
779
|
+
return `source fallback was considered, but marketplace repo ${owner}/${repo} exposes version ${entry.version} instead of requested ${version}`;
|
|
780
|
+
}
|
|
781
|
+
function resolveVersionedMarketplacePluginTarget(ref, version, sourceRepoDir, options) {
|
|
782
|
+
const { pluginName, owner, repo } = parseRef(ref);
|
|
783
|
+
const forceRefresh = options?.forceRefresh === true;
|
|
784
|
+
const cachedDir = resolveVersionedPluginDir(owner, repo, pluginName, version);
|
|
785
|
+
if (cachedDir && !forceRefresh) {
|
|
786
|
+
const artifactLayout = resolvePackagedArtifactsOrThrow(
|
|
787
|
+
pluginName,
|
|
788
|
+
version,
|
|
789
|
+
cachedDir,
|
|
790
|
+
"cache"
|
|
791
|
+
);
|
|
792
|
+
return {
|
|
793
|
+
ref,
|
|
794
|
+
pluginName,
|
|
795
|
+
marketplacePath: artifactLayout.codexMarketplacePath,
|
|
796
|
+
pluginDir: artifactLayout.claudeArtifactDir,
|
|
797
|
+
codexPluginDir: artifactLayout.codexPluginDir
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
let npmError;
|
|
801
|
+
try {
|
|
802
|
+
const fetchedDir = fetchPluginPackage(owner, repo, pluginName, version);
|
|
803
|
+
const artifactLayout = resolvePackagedArtifactsOrThrow(
|
|
804
|
+
pluginName,
|
|
805
|
+
version,
|
|
806
|
+
fetchedDir,
|
|
807
|
+
"npm package"
|
|
808
|
+
);
|
|
809
|
+
return {
|
|
810
|
+
ref,
|
|
811
|
+
pluginName,
|
|
812
|
+
marketplacePath: artifactLayout.codexMarketplacePath,
|
|
813
|
+
pluginDir: artifactLayout.claudeArtifactDir,
|
|
814
|
+
codexPluginDir: artifactLayout.codexPluginDir
|
|
815
|
+
};
|
|
816
|
+
} catch (error) {
|
|
817
|
+
npmError = error;
|
|
818
|
+
}
|
|
819
|
+
const repoDir = sourceRepoDir ?? ensureRepo(owner, repo);
|
|
820
|
+
const fallbackTarget = fs4.existsSync(repoDir) ? resolveMarketplaceSourceFallback(ref, pluginName, version, repoDir) : void 0;
|
|
821
|
+
if (fallbackTarget) {
|
|
822
|
+
return fallbackTarget;
|
|
823
|
+
}
|
|
824
|
+
const npmStageMessage = `npm package resolution failed for ${pluginNpmPackageName(pluginName)}@${version}: ${npmError.message}`;
|
|
825
|
+
const fallbackMessage = describeSourceFallbackRejection(
|
|
826
|
+
owner,
|
|
827
|
+
repo,
|
|
828
|
+
pluginName,
|
|
829
|
+
version,
|
|
830
|
+
repoDir
|
|
831
|
+
);
|
|
832
|
+
throw new Error(
|
|
833
|
+
`Plugin "${pluginName}" version ${version} could not be resolved. ${npmStageMessage}. ${fallbackMessage}.`
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
function refreshVersionedMarketplacePluginTarget(ref, version, sourceRepoDir) {
|
|
837
|
+
return resolveVersionedMarketplacePluginTarget(ref, version, sourceRepoDir, {
|
|
838
|
+
forceRefresh: true
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/infra/plugins/marketplace.ts
|
|
843
|
+
function pullMarketplaceRepo(owner, repo) {
|
|
844
|
+
const outcome = refreshMarketplaceRepo(owner, repo);
|
|
845
|
+
if (!outcome.ok) {
|
|
846
|
+
throw new MarketplaceRefreshError(outcome);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
function resolveMarketplacePlugin(ref) {
|
|
850
|
+
requireGitForMarketplace("plugins");
|
|
851
|
+
const { pluginName, owner, repo } = parseRef(ref);
|
|
852
|
+
const repoDir = ensureRepo(owner, repo);
|
|
853
|
+
return resolvePluginDirFromManifest(
|
|
854
|
+
pluginName,
|
|
855
|
+
repoDir,
|
|
856
|
+
resolvePluginManifestPath(repoDir)
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
function resolveMarketplacePluginTarget(ref) {
|
|
860
|
+
requireGitForMarketplace("plugins");
|
|
861
|
+
const { owner, repo } = parseRef(ref);
|
|
862
|
+
const repoDir = ensureRepo(owner, repo);
|
|
863
|
+
const directTarget = buildMarketplacePluginResolution(
|
|
864
|
+
ref,
|
|
865
|
+
repoDir,
|
|
866
|
+
resolvePluginManifestPath(repoDir)
|
|
867
|
+
);
|
|
868
|
+
const pluginVersion = resolvePluginVersionFromDir(directTarget.pluginDir);
|
|
869
|
+
if (!pluginVersion) {
|
|
870
|
+
return directTarget;
|
|
871
|
+
}
|
|
872
|
+
return resolveVersionedMarketplacePluginTarget(ref, pluginVersion, repoDir);
|
|
873
|
+
}
|
|
874
|
+
function resolveMarketplacePluginTargetFromRepo(ref, repoDir) {
|
|
875
|
+
return buildMarketplacePluginResolution(
|
|
876
|
+
ref,
|
|
877
|
+
repoDir,
|
|
878
|
+
resolvePluginManifestPath(repoDir)
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// src/infra/plugins/config.ts
|
|
883
|
+
import fs5 from "fs";
|
|
884
|
+
import os3 from "os";
|
|
885
|
+
import path4 from "path";
|
|
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
|
+
}
|
|
900
|
+
function projectConfigPath(projectDir) {
|
|
901
|
+
return path4.join(projectDir, ".athena", "config.json");
|
|
902
|
+
}
|
|
903
|
+
function readConfig(projectDir) {
|
|
904
|
+
return readConfigFile(projectConfigPath(projectDir), projectDir);
|
|
905
|
+
}
|
|
906
|
+
function resolveActiveWorkflow(input) {
|
|
907
|
+
const { globalConfig, projectConfig, override } = input;
|
|
908
|
+
if (override !== void 0) {
|
|
909
|
+
return {
|
|
910
|
+
name: override,
|
|
911
|
+
source: "override",
|
|
912
|
+
selectionsLayer: projectConfig.activeWorkflow !== void 0 ? projectConfig : globalConfig
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
if (projectConfig.activeWorkflow !== void 0) {
|
|
916
|
+
return {
|
|
917
|
+
name: projectConfig.activeWorkflow,
|
|
918
|
+
source: "project",
|
|
919
|
+
selectionsLayer: projectConfig
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
if (globalConfig.activeWorkflow !== void 0) {
|
|
923
|
+
return {
|
|
924
|
+
name: globalConfig.activeWorkflow,
|
|
925
|
+
source: "global",
|
|
926
|
+
selectionsLayer: globalConfig
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
name: "default",
|
|
931
|
+
source: "default",
|
|
932
|
+
selectionsLayer: globalConfig
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
function readGlobalConfig() {
|
|
936
|
+
const homeDir = os3.homedir();
|
|
937
|
+
const configPath = path4.join(homeDir, ".config", "athena", "config.json");
|
|
938
|
+
return readConfigFile(configPath, homeDir);
|
|
939
|
+
}
|
|
940
|
+
function readConfigFile(configPath, baseDir) {
|
|
941
|
+
if (!fs5.existsSync(configPath)) {
|
|
942
|
+
return EMPTY_CONFIG;
|
|
943
|
+
}
|
|
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
|
+
}
|
|
951
|
+
if ("workflowMarketplaceSource" in raw) {
|
|
952
|
+
throw new Error(
|
|
953
|
+
`Invalid config: "${configPath}" uses deprecated "workflowMarketplaceSource"; use "workflowMarketplaceSources"`
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
if (raw.workflowMarketplaceSources !== void 0 && (!Array.isArray(raw.workflowMarketplaceSources) || !raw.workflowMarketplaceSources.every(
|
|
957
|
+
(source) => typeof source === "string"
|
|
958
|
+
))) {
|
|
959
|
+
throw new Error(
|
|
960
|
+
`Invalid config: "${configPath}" field "workflowMarketplaceSources" must be an array of strings`
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
if (raw.harness !== void 0 && raw.harness !== "claude-code" && raw.harness !== "openai-codex" && raw.harness !== "opencode") {
|
|
964
|
+
throw new Error(
|
|
965
|
+
`Invalid config: "${configPath}" field "harness" must be one of claude-code, openai-codex, opencode`
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
const plugins = (raw.plugins ?? []).map((p) => {
|
|
969
|
+
if (isMarketplaceRef(p)) return p;
|
|
970
|
+
return path4.isAbsolute(p) ? p : path4.resolve(baseDir, p);
|
|
971
|
+
});
|
|
972
|
+
const additionalDirectories = (raw.additionalDirectories ?? []).map(
|
|
973
|
+
(dir) => path4.isAbsolute(dir) ? dir : path4.resolve(baseDir, dir)
|
|
974
|
+
);
|
|
975
|
+
return {
|
|
976
|
+
plugins,
|
|
977
|
+
additionalDirectories,
|
|
978
|
+
model: raw.model,
|
|
979
|
+
theme: raw.theme,
|
|
980
|
+
activeWorkflow: raw.activeWorkflow,
|
|
981
|
+
workflowMarketplaceSources: raw.workflowMarketplaceSources,
|
|
982
|
+
workflowSelections: raw.workflowSelections,
|
|
983
|
+
setupComplete: raw.setupComplete,
|
|
984
|
+
harness: raw.harness,
|
|
985
|
+
telemetry: raw.telemetry,
|
|
986
|
+
telemetryDiagnostics: raw.telemetryDiagnostics,
|
|
987
|
+
deviceId: raw.deviceId,
|
|
988
|
+
permissionGraceMs: raw.permissionGraceMs,
|
|
989
|
+
// Personal capabilities are stored opaque. In particular skill `path`
|
|
990
|
+
// is NOT relative-resolved against baseDir (R1) — Issue 3 resolves it
|
|
991
|
+
// to an absolute path at install time.
|
|
992
|
+
mcpServers: raw.mcpServers,
|
|
993
|
+
skills: raw.skills
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
function readExistingConfig(configPath) {
|
|
997
|
+
try {
|
|
998
|
+
return JSON.parse(fs5.readFileSync(configPath, "utf-8"));
|
|
999
|
+
} catch {
|
|
1000
|
+
return {};
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
function writeConfigFile(configDir, configPath, updates, deleteKeys) {
|
|
1004
|
+
const existing = readExistingConfig(configPath);
|
|
1005
|
+
const merged = { ...existing, ...updates };
|
|
1006
|
+
if (updates.workflowSelections) {
|
|
1007
|
+
const existingSelections = existing["workflowSelections"] ?? {};
|
|
1008
|
+
merged["workflowSelections"] = {
|
|
1009
|
+
...existingSelections,
|
|
1010
|
+
...updates.workflowSelections
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
if (deleteKeys) {
|
|
1014
|
+
for (const key of deleteKeys) {
|
|
1015
|
+
delete merged[key];
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
fs5.mkdirSync(configDir, { recursive: true });
|
|
1019
|
+
fs5.writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
1020
|
+
}
|
|
1021
|
+
var GLOBAL_CONFIG_LEGACY_KEYS = [
|
|
1022
|
+
"workflow",
|
|
1023
|
+
"mcpServerOptions",
|
|
1024
|
+
"workflowMarketplaceSource",
|
|
1025
|
+
...STALE_CONFIG_KEYS
|
|
1026
|
+
];
|
|
1027
|
+
function writeGlobalConfig(updates) {
|
|
1028
|
+
const homeDir = os3.homedir();
|
|
1029
|
+
const configDir = path4.join(homeDir, ".config", "athena");
|
|
1030
|
+
const configPath = path4.join(configDir, "config.json");
|
|
1031
|
+
writeConfigFile(configDir, configPath, updates, GLOBAL_CONFIG_LEGACY_KEYS);
|
|
1032
|
+
}
|
|
1033
|
+
function writeProjectConfig(projectDir, updates) {
|
|
1034
|
+
const configDir = path4.join(projectDir, ".athena");
|
|
1035
|
+
const configPath = path4.join(configDir, "config.json");
|
|
1036
|
+
writeConfigFile(configDir, configPath, updates);
|
|
1037
|
+
}
|
|
1038
|
+
function hasActiveWorkflow(configPath) {
|
|
1039
|
+
try {
|
|
1040
|
+
const raw = JSON.parse(fs5.readFileSync(configPath, "utf-8"));
|
|
1041
|
+
return typeof raw.activeWorkflow === "string" && raw.activeWorkflow !== "";
|
|
1042
|
+
} catch {
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function hasProjectWorkflow(projectDir) {
|
|
1047
|
+
return hasActiveWorkflow(projectConfigPath(projectDir));
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// src/core/workflows/types.ts
|
|
1051
|
+
var DEFAULT_MAX_TURN_TOKEN_COUNT = 13e4;
|
|
1052
|
+
var DEFAULT_NUDGE_CAP = 3;
|
|
1053
|
+
var DEFAULT_RETRY_CAP = 3;
|
|
1054
|
+
var DEFAULT_RETRY_BACKOFF_MS = 1e4;
|
|
1055
|
+
var DEFAULT_PERMISSION_GRACE_MS = 6e4;
|
|
1056
|
+
function pluginSpecRef(spec) {
|
|
1057
|
+
return typeof spec === "string" ? spec : spec.ref;
|
|
1058
|
+
}
|
|
1059
|
+
function pluginSpecVersion(spec) {
|
|
1060
|
+
return typeof spec === "string" ? void 0 : spec.version;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// src/core/workflows/installer.ts
|
|
1064
|
+
function installWorkflowPlugins(workflow) {
|
|
1065
|
+
return resolveWorkflowPlugins(workflow).resolvedPlugins.map(
|
|
1066
|
+
(plugin) => plugin.claudeArtifactDir
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
function marketplaceNameFromRef(ref) {
|
|
1070
|
+
return ref.slice(ref.indexOf("@") + 1);
|
|
1071
|
+
}
|
|
1072
|
+
function toResolvedWorkflowPlugin(target, ref, version) {
|
|
1073
|
+
return {
|
|
1074
|
+
ref: target.ref,
|
|
1075
|
+
pluginName: target.pluginName,
|
|
1076
|
+
marketplaceName: marketplaceNameFromRef(ref),
|
|
1077
|
+
...version !== void 0 && { version },
|
|
1078
|
+
pluginDir: target.pluginDir,
|
|
1079
|
+
claudeArtifactDir: target.pluginDir,
|
|
1080
|
+
codexPluginDir: target.codexPluginDir,
|
|
1081
|
+
codexMarketplacePath: target.marketplacePath
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function resolveWorkflowPlugins(workflow) {
|
|
1085
|
+
const resolvedPlugins = workflow.plugins.map((spec) => {
|
|
1086
|
+
const ref = pluginSpecRef(spec);
|
|
1087
|
+
const version = pluginSpecVersion(spec);
|
|
1088
|
+
try {
|
|
1089
|
+
const source = "__source" in workflow ? workflow.__source : void 0;
|
|
1090
|
+
const repoDir = source?.kind === "marketplace-local" ? source.repoDir : void 0;
|
|
1091
|
+
if (version) {
|
|
1092
|
+
const target2 = resolveVersionedMarketplacePluginTarget(
|
|
1093
|
+
ref,
|
|
1094
|
+
version,
|
|
1095
|
+
repoDir
|
|
1096
|
+
);
|
|
1097
|
+
return toResolvedWorkflowPlugin(target2, ref, version);
|
|
1098
|
+
}
|
|
1099
|
+
if (repoDir) {
|
|
1100
|
+
const target2 = resolveMarketplacePluginTargetFromRepo(ref, repoDir);
|
|
1101
|
+
return toResolvedWorkflowPlugin(target2, ref);
|
|
1102
|
+
}
|
|
1103
|
+
const target = resolveMarketplacePluginTarget(ref);
|
|
1104
|
+
return toResolvedWorkflowPlugin(target, ref);
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
throw new Error(
|
|
1107
|
+
`Workflow "${workflow.name}": failed to install plugin "${ref}": ${error.message}`
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
return {
|
|
1112
|
+
resolvedPlugins,
|
|
1113
|
+
localPlugins: resolvedPlugins.map((p) => ({
|
|
1114
|
+
ref: p.ref,
|
|
1115
|
+
pluginDir: p.claudeArtifactDir
|
|
1116
|
+
})),
|
|
1117
|
+
codexPlugins: resolvedPlugins.map((p) => ({
|
|
1118
|
+
ref: p.ref,
|
|
1119
|
+
pluginName: p.pluginName,
|
|
1120
|
+
marketplacePath: p.codexMarketplacePath,
|
|
1121
|
+
...p.version !== void 0 && { version: p.version }
|
|
1122
|
+
}))
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
function refreshPinnedWorkflowPlugins(workflow) {
|
|
1126
|
+
const source = "__source" in workflow ? workflow.__source : void 0;
|
|
1127
|
+
for (const spec of workflow.plugins) {
|
|
1128
|
+
const ref = pluginSpecRef(spec);
|
|
1129
|
+
const version = pluginSpecVersion(spec);
|
|
1130
|
+
if (!version) {
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
try {
|
|
1134
|
+
refreshVersionedMarketplacePluginTarget(
|
|
1135
|
+
ref,
|
|
1136
|
+
version,
|
|
1137
|
+
source?.kind === "marketplace-local" ? source.repoDir : void 0
|
|
1138
|
+
);
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
throw new Error(
|
|
1141
|
+
`Workflow "${workflow.name}": failed to refresh pinned plugin "${ref}" at version ${version}: ${error.message}`
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// src/core/workflows/registry.ts
|
|
1148
|
+
import fs9 from "fs";
|
|
1149
|
+
import os5 from "os";
|
|
1150
|
+
import path8 from "path";
|
|
1151
|
+
|
|
1152
|
+
// src/core/workflows/builtins/index.ts
|
|
1153
|
+
import fs7 from "fs";
|
|
1154
|
+
import os4 from "os";
|
|
1155
|
+
import path6 from "path";
|
|
1156
|
+
|
|
1157
|
+
// src/core/workflows/journalReader.ts
|
|
1158
|
+
import fs6 from "fs";
|
|
1159
|
+
import path5 from "path";
|
|
1160
|
+
|
|
1161
|
+
// src/core/workflows/templateVars.ts
|
|
1162
|
+
function substituteVariables(text, ctx) {
|
|
1163
|
+
let result = text;
|
|
1164
|
+
if (ctx.input !== void 0) {
|
|
1165
|
+
result = result.replaceAll("{input}", ctx.input);
|
|
1166
|
+
}
|
|
1167
|
+
if (ctx.sessionId !== void 0) {
|
|
1168
|
+
result = result.replaceAll("{sessionId}", ctx.sessionId);
|
|
1169
|
+
result = result.replaceAll("<session_id>", ctx.sessionId);
|
|
1170
|
+
}
|
|
1171
|
+
if (ctx.journalPath !== void 0) {
|
|
1172
|
+
result = result.replaceAll("{journalPath}", ctx.journalPath);
|
|
1173
|
+
result = result.replaceAll("{trackerPath}", ctx.journalPath);
|
|
1174
|
+
}
|
|
1175
|
+
return result;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// src/core/workflows/journalReader.ts
|
|
1179
|
+
var DEPRECATION_REMOVAL_RELEASE = "0.7.0";
|
|
1180
|
+
var DEFAULT_COMPLETION_MARKER = "<!-- WORKFLOW_COMPLETE -->";
|
|
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) {
|
|
1212
|
+
try {
|
|
1213
|
+
return fs6.readFileSync(journalPath, "utf-8");
|
|
1214
|
+
} catch {
|
|
1215
|
+
return "";
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
function getNonEmptyLines(content) {
|
|
1219
|
+
return content.trimEnd().split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1220
|
+
}
|
|
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));
|
|
1226
|
+
}
|
|
1227
|
+
function isTerminalMarkerLine(line, completionMarker, needsHumanMarkers) {
|
|
1228
|
+
return line === completionMarker || matchNeedsHumanMarker(line, needsHumanMarkers) !== void 0;
|
|
1229
|
+
}
|
|
1230
|
+
function getMisplacedTerminalMarker(lines, completionMarker, needsHumanMarkers) {
|
|
1231
|
+
if (lines.length < 2) return void 0;
|
|
1232
|
+
const terminalLine = lines.at(-1);
|
|
1233
|
+
if (terminalLine && isTerminalMarkerLine(terminalLine, completionMarker, needsHumanMarkers)) {
|
|
1234
|
+
return void 0;
|
|
1235
|
+
}
|
|
1236
|
+
return lines.slice(0, -1).find(
|
|
1237
|
+
(line) => isTerminalMarkerLine(line, completionMarker, needsHumanMarkers)
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
function extractNeedsHumanReason(line, marker) {
|
|
1241
|
+
if (!line.startsWith(marker)) return void 0;
|
|
1242
|
+
const afterMarker = line.slice(marker.length);
|
|
1243
|
+
const match = afterMarker.match(/^:\s*(.+?)(?:\s*-->|$)/);
|
|
1244
|
+
return match?.[1]?.trim();
|
|
1245
|
+
}
|
|
1246
|
+
function parseJournalState(content, markers = {}) {
|
|
1247
|
+
const completionMarker = markers.completionMarker ?? DEFAULT_COMPLETION_MARKER;
|
|
1248
|
+
const needsHumanMarkers = needsHumanMarkersFor(markers);
|
|
1249
|
+
const lines = getNonEmptyLines(content);
|
|
1250
|
+
const terminalLine = lines.at(-1);
|
|
1251
|
+
const completed = terminalLine === completionMarker;
|
|
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;
|
|
1255
|
+
return {
|
|
1256
|
+
completed,
|
|
1257
|
+
needsHuman,
|
|
1258
|
+
needsHumanReason,
|
|
1259
|
+
misplacedTerminalMarker: getMisplacedTerminalMarker(
|
|
1260
|
+
lines,
|
|
1261
|
+
completionMarker,
|
|
1262
|
+
needsHumanMarkers
|
|
1263
|
+
),
|
|
1264
|
+
skeletonNotReplaced: hasSkeletonMarker(content),
|
|
1265
|
+
...matchedMarker === LEGACY_BLOCKED_MARKER ? { deprecatedMarker: LEGACY_BLOCKED_MARKER } : {}
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
function demoteTerminalMarkers(content, markers = {}) {
|
|
1269
|
+
const completionMarker = markers.completionMarker ?? DEFAULT_COMPLETION_MARKER;
|
|
1270
|
+
const needsHumanMarkers = needsHumanMarkersFor(markers);
|
|
1271
|
+
return content.split("\n").map((line) => {
|
|
1272
|
+
const trimmed = line.trim();
|
|
1273
|
+
if (trimmed === completionMarker) return "> _Prior Run ended: complete._";
|
|
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._";
|
|
1278
|
+
}
|
|
1279
|
+
return line;
|
|
1280
|
+
}).join("\n");
|
|
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
|
+
}
|
|
1297
|
+
function buildContinuePrompt(loop) {
|
|
1298
|
+
const template = loop.continuePrompt ?? DEFAULT_CONTINUE_PROMPT;
|
|
1299
|
+
return substituteVariables(template, { journalPath: loopJournalPath(loop) });
|
|
1300
|
+
}
|
|
1301
|
+
function buildNudgePrompt(loop, opts) {
|
|
1302
|
+
const completionMarker = loop.completionMarker ?? DEFAULT_COMPLETION_MARKER;
|
|
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.`;
|
|
1307
|
+
}
|
|
1308
|
+
function estimateTokenCount(content) {
|
|
1309
|
+
return Math.ceil(content.length / 4);
|
|
1310
|
+
}
|
|
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.`;
|
|
1314
|
+
}
|
|
1315
|
+
var UNITS_HEADING_RE = /^##\s+Units\s*$/;
|
|
1316
|
+
function splitTableRow(line) {
|
|
1317
|
+
const trimmed = line.trim();
|
|
1318
|
+
const withoutEdgePipes = trimmed.replace(/^\|/, "").replace(/\|$/, "");
|
|
1319
|
+
return withoutEdgePipes.split("|").map((cell) => cell.trim());
|
|
1320
|
+
}
|
|
1321
|
+
function isSeparatorRow(cells) {
|
|
1322
|
+
return cells.length > 0 && cells.every((cell) => /^:?-+:?$/.test(cell));
|
|
1323
|
+
}
|
|
1324
|
+
function parseUnitTable(content) {
|
|
1325
|
+
const lines = content.split("\n");
|
|
1326
|
+
const headingIndex = lines.findIndex(
|
|
1327
|
+
(line) => UNITS_HEADING_RE.test(line.trim())
|
|
1328
|
+
);
|
|
1329
|
+
if (headingIndex === -1) return null;
|
|
1330
|
+
let i = headingIndex + 1;
|
|
1331
|
+
while (i < lines.length && lines[i].trim() === "") i++;
|
|
1332
|
+
const tableLines = [];
|
|
1333
|
+
while (i < lines.length) {
|
|
1334
|
+
const trimmed = lines[i].trim();
|
|
1335
|
+
if (!trimmed.startsWith("|")) break;
|
|
1336
|
+
tableLines.push(trimmed);
|
|
1337
|
+
i++;
|
|
1338
|
+
}
|
|
1339
|
+
if (tableLines.length < 2) return null;
|
|
1340
|
+
const rows = [];
|
|
1341
|
+
for (const line of tableLines.slice(1)) {
|
|
1342
|
+
const cells = splitTableRow(line);
|
|
1343
|
+
if (isSeparatorRow(cells)) continue;
|
|
1344
|
+
if (cells.length !== 2) continue;
|
|
1345
|
+
const [label, recordPath] = cells;
|
|
1346
|
+
if (!label || !recordPath) continue;
|
|
1347
|
+
rows.push({ label, recordPath });
|
|
1348
|
+
}
|
|
1349
|
+
return rows;
|
|
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
|
+
}
|
|
1400
|
+
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/;
|
|
1401
|
+
function parseUnitRecordFrontmatter(content) {
|
|
1402
|
+
const match = FRONTMATTER_RE.exec(content);
|
|
1403
|
+
if (!match) return null;
|
|
1404
|
+
const block = match[1];
|
|
1405
|
+
const statusMatch = /^status:\s*(\S+)\s*$/m.exec(block);
|
|
1406
|
+
const raw = statusMatch?.[1]?.trim().toLowerCase();
|
|
1407
|
+
if (raw !== "open" && raw !== "closed") return null;
|
|
1408
|
+
return { status: raw };
|
|
1409
|
+
}
|
|
1410
|
+
function projectJournalTasks(journalAbsPath) {
|
|
1411
|
+
const content = readJournal(journalAbsPath);
|
|
1412
|
+
if (!content) return null;
|
|
1413
|
+
const rows = parseUnitTable(content);
|
|
1414
|
+
if (!rows) return null;
|
|
1415
|
+
const baseDir = path5.dirname(journalAbsPath);
|
|
1416
|
+
const tasks = [];
|
|
1417
|
+
for (const row of rows) {
|
|
1418
|
+
const recordAbsPath = path5.resolve(baseDir, row.recordPath);
|
|
1419
|
+
const relToBase = path5.relative(baseDir, recordAbsPath);
|
|
1420
|
+
if (relToBase.startsWith("..") || path5.isAbsolute(relToBase)) continue;
|
|
1421
|
+
let recordContent;
|
|
1422
|
+
try {
|
|
1423
|
+
recordContent = fs6.readFileSync(recordAbsPath, "utf-8");
|
|
1424
|
+
} catch {
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1427
|
+
const frontmatter = parseUnitRecordFrontmatter(recordContent);
|
|
1428
|
+
if (!frontmatter) continue;
|
|
1429
|
+
const taskId = path5.basename(row.recordPath, path5.extname(row.recordPath));
|
|
1430
|
+
if (!taskId) continue;
|
|
1431
|
+
tasks.push({
|
|
1432
|
+
taskId,
|
|
1433
|
+
content: row.label,
|
|
1434
|
+
status: frontmatter.status === "closed" ? "completed" : "pending"
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
return tasks;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// src/core/workflows/builtins/index.ts
|
|
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.
|
|
1444
|
+
|
|
1445
|
+
## Journal File
|
|
1446
|
+
|
|
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.
|
|
1448
|
+
|
|
1449
|
+
If no journal file exists, create one by:
|
|
1450
|
+
1. Analyzing the user's request to understand the full scope
|
|
1451
|
+
2. Breaking the task into concrete, actionable steps
|
|
1452
|
+
3. Writing the plan to the journal file
|
|
1453
|
+
|
|
1454
|
+
### Journal Format
|
|
1455
|
+
|
|
1456
|
+
Use this markdown format for the journal:
|
|
1457
|
+
|
|
1458
|
+
\`\`\`
|
|
1459
|
+
# Task: <one-line summary>
|
|
1460
|
+
|
|
1461
|
+
## Plan
|
|
1462
|
+
- [x] Step 1 description
|
|
1463
|
+
- [x] Step 2 description
|
|
1464
|
+
- [ ] Step 3 description (current)
|
|
1465
|
+
- [ ] Step 4 description
|
|
1466
|
+
|
|
1467
|
+
## Current Status
|
|
1468
|
+
<brief description of where things stand and what to do next>
|
|
1469
|
+
|
|
1470
|
+
## Notes
|
|
1471
|
+
<any important context, decisions, or blockers discovered along the way>
|
|
1472
|
+
\`\`\`
|
|
1473
|
+
|
|
1474
|
+
### Updating the Journal
|
|
1475
|
+
|
|
1476
|
+
After completing meaningful work, update the journal:
|
|
1477
|
+
- Check off completed steps
|
|
1478
|
+
- Update the current status section
|
|
1479
|
+
- Add any important notes or decisions
|
|
1480
|
+
|
|
1481
|
+
### Completion
|
|
1482
|
+
|
|
1483
|
+
When all steps are complete:
|
|
1484
|
+
1. Update the journal with all steps checked off
|
|
1485
|
+
2. Put any final summary or outcome notes above the terminal marker
|
|
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
|
|
1488
|
+
|
|
1489
|
+
### Needs a human
|
|
1490
|
+
|
|
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
|
|
1493
|
+
2. Explain what needs to happen to unblock the task whenever possible above the terminal marker
|
|
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
|
|
1496
|
+
`;
|
|
1497
|
+
function ensureSystemPromptFile() {
|
|
1498
|
+
const dir = path6.join(
|
|
1499
|
+
os4.homedir(),
|
|
1500
|
+
".config",
|
|
1501
|
+
"athena",
|
|
1502
|
+
"builtins",
|
|
1503
|
+
"default"
|
|
1504
|
+
);
|
|
1505
|
+
const filePath = path6.join(dir, "system_prompt.md");
|
|
1506
|
+
if (!fs7.existsSync(filePath) || fs7.readFileSync(filePath, "utf-8") !== SYSTEM_PROMPT) {
|
|
1507
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
1508
|
+
fs7.writeFileSync(filePath, SYSTEM_PROMPT, "utf-8");
|
|
1509
|
+
}
|
|
1510
|
+
return filePath;
|
|
1511
|
+
}
|
|
1512
|
+
function resolveBuiltinWorkflow(name) {
|
|
1513
|
+
if (name !== "default") {
|
|
1514
|
+
return void 0;
|
|
1515
|
+
}
|
|
1516
|
+
return {
|
|
1517
|
+
name: "default",
|
|
1518
|
+
description: "General-purpose workflow for long-horizon tasks \u2014 breaks work into steps, tracks progress across sessions, and loops until complete",
|
|
1519
|
+
promptTemplate: "{input}",
|
|
1520
|
+
loop: {
|
|
1521
|
+
enabled: true,
|
|
1522
|
+
completionMarker: DEFAULT_COMPLETION_MARKER,
|
|
1523
|
+
needsHumanMarker: DEFAULT_NEEDS_HUMAN_MARKER,
|
|
1524
|
+
maxIterations: 20
|
|
1525
|
+
},
|
|
1526
|
+
plugins: [],
|
|
1527
|
+
workflowFile: ensureSystemPromptFile()
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
function listBuiltinWorkflows() {
|
|
1531
|
+
return ["default"];
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// src/core/workflows/sourceMetadata.ts
|
|
1535
|
+
import fs8 from "fs";
|
|
1536
|
+
import path7 from "path";
|
|
1537
|
+
function legacyLocalToNew(legacyPath, legacyRepoDir) {
|
|
1538
|
+
const repoDir = legacyRepoDir ?? findMarketplaceRepoDir(legacyPath);
|
|
1539
|
+
if (!repoDir) {
|
|
1540
|
+
return { kind: "filesystem", path: legacyPath };
|
|
1541
|
+
}
|
|
1542
|
+
try {
|
|
1543
|
+
const canonicalRepoDir = fs8.realpathSync(repoDir);
|
|
1544
|
+
const listings = listMarketplaceWorkflowsFromRepo(canonicalRepoDir);
|
|
1545
|
+
const absolutePath = fs8.realpathSync(legacyPath);
|
|
1546
|
+
const match = listings.find(
|
|
1547
|
+
(l) => fs8.realpathSync(l.workflowPath) === absolutePath
|
|
1548
|
+
);
|
|
1549
|
+
if (match) {
|
|
1550
|
+
return {
|
|
1551
|
+
kind: "marketplace-local",
|
|
1552
|
+
repoDir: canonicalRepoDir,
|
|
1553
|
+
workflowName: match.name,
|
|
1554
|
+
...match.version ? { version: match.version } : {}
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
} catch {
|
|
1558
|
+
}
|
|
1559
|
+
return { kind: "filesystem", path: legacyPath };
|
|
1560
|
+
}
|
|
1561
|
+
function readWorkflowSourceMetadata(workflowDir) {
|
|
1562
|
+
const sourceFile = path7.join(workflowDir, "source.json");
|
|
1563
|
+
if (!fs8.existsSync(sourceFile)) return void 0;
|
|
1564
|
+
let raw;
|
|
1565
|
+
try {
|
|
1566
|
+
raw = JSON.parse(fs8.readFileSync(sourceFile, "utf-8"));
|
|
1567
|
+
} catch {
|
|
1568
|
+
throw new Error(`Invalid source.json: ${sourceFile} is not valid JSON`);
|
|
1569
|
+
}
|
|
1570
|
+
if (!raw || typeof raw !== "object") {
|
|
1571
|
+
throw new Error(
|
|
1572
|
+
`Invalid source.json: ${sourceFile} must contain an object`
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
const r = raw;
|
|
1576
|
+
if (r["v"] === 2) {
|
|
1577
|
+
if (r["kind"] === "marketplace-remote" && typeof r["ref"] === "string") {
|
|
1578
|
+
return {
|
|
1579
|
+
kind: "marketplace-remote",
|
|
1580
|
+
ref: r["ref"],
|
|
1581
|
+
...typeof r["version"] === "string" ? { version: r["version"] } : {}
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
if (r["kind"] === "marketplace-local" && typeof r["repoDir"] === "string" && typeof r["workflowName"] === "string") {
|
|
1585
|
+
return {
|
|
1586
|
+
kind: "marketplace-local",
|
|
1587
|
+
repoDir: r["repoDir"],
|
|
1588
|
+
workflowName: r["workflowName"],
|
|
1589
|
+
...typeof r["version"] === "string" ? { version: r["version"] } : {}
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
if (r["kind"] === "filesystem" && typeof r["path"] === "string") {
|
|
1593
|
+
return { kind: "filesystem", path: r["path"] };
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
if (r["kind"] === "marketplace" && typeof r["ref"] === "string") {
|
|
1597
|
+
return { kind: "marketplace-remote", ref: r["ref"] };
|
|
1598
|
+
}
|
|
1599
|
+
if (r["kind"] === "local" && typeof r["path"] === "string") {
|
|
1600
|
+
return legacyLocalToNew(
|
|
1601
|
+
r["path"],
|
|
1602
|
+
typeof r["repoDir"] === "string" ? r["repoDir"] : void 0
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
throw new Error(`Invalid source.json: ${sourceFile} kind is not supported`);
|
|
1606
|
+
}
|
|
1607
|
+
function writeWorkflowSourceMetadata(workflowDir, metadata) {
|
|
1608
|
+
fs8.mkdirSync(workflowDir, { recursive: true });
|
|
1609
|
+
fs8.writeFileSync(
|
|
1610
|
+
path7.join(workflowDir, "source.json"),
|
|
1611
|
+
JSON.stringify({ v: 2, ...metadata }),
|
|
1612
|
+
"utf-8"
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// src/core/workflows/registry.ts
|
|
1617
|
+
function workflowRegistryDir() {
|
|
1618
|
+
return path8.join(os5.homedir(), ".config", "athena", "workflows");
|
|
1619
|
+
}
|
|
1620
|
+
function registryDir() {
|
|
1621
|
+
return workflowRegistryDir();
|
|
1622
|
+
}
|
|
1623
|
+
function ensurePathWithinRoot(rootDir, targetPath, label) {
|
|
1624
|
+
const relative = path8.relative(rootDir, targetPath);
|
|
1625
|
+
if (relative === "" || !relative.startsWith("..") && !path8.isAbsolute(relative)) {
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
throw new Error(`${label} resolves outside the workflow root: ${targetPath}`);
|
|
1629
|
+
}
|
|
1630
|
+
function resolveWorkflow(name) {
|
|
1631
|
+
const workflowDir = path8.join(registryDir(), name);
|
|
1632
|
+
const workflowPath = path8.join(workflowDir, "workflow.json");
|
|
1633
|
+
if (!fs9.existsSync(workflowPath)) {
|
|
1634
|
+
const builtin = resolveBuiltinWorkflow(name);
|
|
1635
|
+
if (builtin) {
|
|
1636
|
+
return builtin;
|
|
1637
|
+
}
|
|
1638
|
+
throw new Error(
|
|
1639
|
+
`Workflow "${name}" not found. Install with: athena workflow install <source> --name ${name}`
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
const source = readWorkflowSourceMetadata(workflowDir);
|
|
1643
|
+
const raw = JSON.parse(fs9.readFileSync(workflowPath, "utf-8"));
|
|
1644
|
+
if (!Array.isArray(raw["plugins"])) {
|
|
1645
|
+
throw new Error(
|
|
1646
|
+
`Invalid workflow.json: "plugins" must be an array (got ${typeof raw["plugins"]})`
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
for (const entry of raw["plugins"]) {
|
|
1650
|
+
if (typeof entry === "string") {
|
|
1651
|
+
if (isMarketplaceRef(entry.trim())) {
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
throw new Error(
|
|
1655
|
+
`Invalid workflow.json: each plugin must be a valid marketplace ref string or {ref, version} object`
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
if (typeof entry === "object" && entry !== null) {
|
|
1659
|
+
const r = entry["ref"];
|
|
1660
|
+
const v = entry["version"];
|
|
1661
|
+
if (typeof r === "string" && isMarketplaceRef(r.trim()) && r.trim().length > 0 && typeof v === "string" && v.trim().length > 0) {
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
throw new Error(
|
|
1666
|
+
`Invalid workflow.json: each plugin must be a valid marketplace ref string or {ref, version} object`
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
if (typeof raw["promptTemplate"] !== "string") {
|
|
1670
|
+
throw new Error(`Invalid workflow.json: "promptTemplate" must be a string`);
|
|
1671
|
+
}
|
|
1672
|
+
if (raw["examplePrompts"] !== void 0 && (!Array.isArray(raw["examplePrompts"]) || !raw["examplePrompts"].every((e) => typeof e === "string"))) {
|
|
1673
|
+
throw new Error(
|
|
1674
|
+
`Invalid workflow.json: "examplePrompts" must be an array of strings`
|
|
1675
|
+
);
|
|
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
|
+
}
|
|
1684
|
+
if (typeof raw["workflowFile"] !== "string" || raw["workflowFile"].length === 0) {
|
|
1685
|
+
throw new Error(
|
|
1686
|
+
'Invalid workflow.json: "workflowFile" is required and must point to the workflow instructions file'
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
const workflowFile = raw["workflowFile"];
|
|
1690
|
+
if (typeof workflowFile === "string" && !path8.isAbsolute(workflowFile)) {
|
|
1691
|
+
const workflowFilePath = path8.resolve(workflowDir, workflowFile);
|
|
1692
|
+
if (!fs9.existsSync(workflowFilePath)) {
|
|
1693
|
+
throw new Error(
|
|
1694
|
+
`Invalid workflow.json: workflowFile "${workflowFile}" not found at ${workflowFilePath}`
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
raw["workflowFile"] = workflowFilePath;
|
|
1698
|
+
} else if (typeof workflowFile === "string" && !fs9.existsSync(workflowFile)) {
|
|
1699
|
+
throw new Error(
|
|
1700
|
+
`Invalid workflow.json: workflowFile "${workflowFile}" not found`
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
return {
|
|
1704
|
+
...raw,
|
|
1705
|
+
...source ? { __source: source } : {}
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
function readWorkflowSource(sourcePath) {
|
|
1709
|
+
const content = fs9.readFileSync(sourcePath, "utf-8");
|
|
1710
|
+
return { content, workflow: JSON.parse(content) };
|
|
1711
|
+
}
|
|
1712
|
+
function copyWorkflowFiles(sourcePath, destDir) {
|
|
1713
|
+
const { content, workflow } = readWorkflowSource(sourcePath);
|
|
1714
|
+
const absoluteSourcePath = path8.resolve(sourcePath);
|
|
1715
|
+
const sourceDir = path8.dirname(absoluteSourcePath);
|
|
1716
|
+
const absoluteDestDir = path8.resolve(destDir);
|
|
1717
|
+
fs9.mkdirSync(absoluteDestDir, { recursive: true });
|
|
1718
|
+
fs9.writeFileSync(
|
|
1719
|
+
path8.join(absoluteDestDir, "workflow.json"),
|
|
1720
|
+
content,
|
|
1721
|
+
"utf-8"
|
|
1722
|
+
);
|
|
1723
|
+
const copyRelativeAsset = (assetPath) => {
|
|
1724
|
+
if (!assetPath || path8.isAbsolute(assetPath)) return;
|
|
1725
|
+
const sourceAssetPath = path8.resolve(sourceDir, assetPath);
|
|
1726
|
+
ensurePathWithinRoot(sourceDir, sourceAssetPath, "Workflow asset");
|
|
1727
|
+
if (!fs9.existsSync(sourceAssetPath)) return;
|
|
1728
|
+
const destAssetPath = path8.resolve(absoluteDestDir, assetPath);
|
|
1729
|
+
ensurePathWithinRoot(absoluteDestDir, destAssetPath, "Workflow asset");
|
|
1730
|
+
fs9.mkdirSync(path8.dirname(destAssetPath), { recursive: true });
|
|
1731
|
+
fs9.copyFileSync(sourceAssetPath, destAssetPath);
|
|
1732
|
+
};
|
|
1733
|
+
const rawWorkflow = workflow;
|
|
1734
|
+
const promptAsset = rawWorkflow["workflowFile"];
|
|
1735
|
+
if (typeof promptAsset !== "string" || promptAsset.length === 0) {
|
|
1736
|
+
throw new Error(
|
|
1737
|
+
'Invalid workflow.json: "workflowFile" is required and must point to the workflow instructions file'
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
copyRelativeAsset(promptAsset);
|
|
1741
|
+
}
|
|
1742
|
+
function toStoredMetadata(source) {
|
|
1743
|
+
if (source.kind === "marketplace-remote") {
|
|
1744
|
+
return {
|
|
1745
|
+
kind: "marketplace-remote",
|
|
1746
|
+
ref: source.ref,
|
|
1747
|
+
...source.version ? { version: source.version } : {}
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
if (source.kind === "marketplace-local") {
|
|
1751
|
+
return {
|
|
1752
|
+
kind: "marketplace-local",
|
|
1753
|
+
repoDir: source.repoDir,
|
|
1754
|
+
workflowName: source.workflowName,
|
|
1755
|
+
...source.version ? { version: source.version } : {}
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
return { kind: "filesystem", path: source.workflowPath };
|
|
1759
|
+
}
|
|
1760
|
+
function installWorkflowFromSource(source, name) {
|
|
1761
|
+
const { workflow } = readWorkflowSource(source.workflowPath);
|
|
1762
|
+
const workflowName = name ?? workflow.name;
|
|
1763
|
+
if (!workflowName) {
|
|
1764
|
+
throw new Error(
|
|
1765
|
+
'Workflow has no "name" field. Provide --name to specify one.'
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
const destDir = path8.join(registryDir(), workflowName);
|
|
1769
|
+
copyWorkflowFiles(source.workflowPath, destDir);
|
|
1770
|
+
const metadata = toStoredMetadata(source);
|
|
1771
|
+
writeWorkflowSourceMetadata(destDir, metadata);
|
|
1772
|
+
return workflowName;
|
|
1773
|
+
}
|
|
1774
|
+
function reResolveFromMetadata(metadata, refresh = refreshMarketplaceRepo, onSelfHeal) {
|
|
1775
|
+
if (metadata.kind === "marketplace-remote") {
|
|
1776
|
+
const slug = metadata.ref.slice(metadata.ref.indexOf("@") + 1);
|
|
1777
|
+
const slashIdx = slug.indexOf("/");
|
|
1778
|
+
const owner = slug.slice(0, slashIdx);
|
|
1779
|
+
const repo = slug.slice(slashIdx + 1);
|
|
1780
|
+
const refreshed = refresh(owner, repo);
|
|
1781
|
+
if (!refreshed.ok) {
|
|
1782
|
+
throw new MarketplaceRefreshError(refreshed);
|
|
1783
|
+
}
|
|
1784
|
+
if (refreshed.selfHealed) {
|
|
1785
|
+
onSelfHeal?.({
|
|
1786
|
+
marketplace: `${owner}/${repo}`,
|
|
1787
|
+
backupDir: refreshed.backupDir
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
return resolveWorkflowInstall(metadata.ref, []);
|
|
1791
|
+
}
|
|
1792
|
+
if (metadata.kind === "marketplace-local") {
|
|
1793
|
+
const manifestPath = resolveWorkflowManifestPath(metadata.repoDir);
|
|
1794
|
+
const listings = listMarketplaceWorkflowsFromRepo(metadata.repoDir);
|
|
1795
|
+
const entry = listings.find((l) => l.name === metadata.workflowName);
|
|
1796
|
+
if (!entry) {
|
|
1797
|
+
throw new Error(
|
|
1798
|
+
`Workflow "${metadata.workflowName}" is no longer in the local marketplace at ${metadata.repoDir}.`
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
return {
|
|
1802
|
+
kind: "marketplace-local",
|
|
1803
|
+
repoDir: metadata.repoDir,
|
|
1804
|
+
workflowName: metadata.workflowName,
|
|
1805
|
+
...entry.version ? { version: entry.version } : {},
|
|
1806
|
+
manifestPath,
|
|
1807
|
+
workflowPath: entry.workflowPath
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
return { kind: "filesystem", workflowPath: metadata.path };
|
|
1811
|
+
}
|
|
1812
|
+
function updateWorkflow(name, refresh = refreshMarketplaceRepo, onSelfHeal) {
|
|
1813
|
+
const workflowDir = path8.join(registryDir(), name);
|
|
1814
|
+
const metadata = readWorkflowSourceMetadata(workflowDir);
|
|
1815
|
+
if (!metadata) {
|
|
1816
|
+
throw new Error(
|
|
1817
|
+
`Workflow "${name}" has no recorded source. Reinstall it with: athena-flow workflow install <source>`
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
const source = reResolveFromMetadata(metadata, refresh, onSelfHeal);
|
|
1821
|
+
const installedName = installWorkflowFromSource(source, name);
|
|
1822
|
+
refreshPinnedWorkflowPlugins(resolveWorkflow(installedName));
|
|
1823
|
+
return installedName;
|
|
1824
|
+
}
|
|
1825
|
+
function updateWorkflows(names) {
|
|
1826
|
+
const refreshCache = /* @__PURE__ */ new Map();
|
|
1827
|
+
const refresh = (owner, repo) => {
|
|
1828
|
+
const key = `${owner}/${repo}`;
|
|
1829
|
+
const cached = refreshCache.get(key);
|
|
1830
|
+
if (cached) return cached;
|
|
1831
|
+
const outcome = refreshMarketplaceRepo(owner, repo);
|
|
1832
|
+
refreshCache.set(key, outcome);
|
|
1833
|
+
return outcome;
|
|
1834
|
+
};
|
|
1835
|
+
const upgraded = [];
|
|
1836
|
+
const marketplaceFailures = [];
|
|
1837
|
+
const reportedMarketplaces = /* @__PURE__ */ new Set();
|
|
1838
|
+
const workflowFailures = [];
|
|
1839
|
+
const selfHealed = [];
|
|
1840
|
+
const selfHealedMarketplaces = /* @__PURE__ */ new Set();
|
|
1841
|
+
const onSelfHeal = (info) => {
|
|
1842
|
+
if (!selfHealedMarketplaces.has(info.marketplace)) {
|
|
1843
|
+
selfHealedMarketplaces.add(info.marketplace);
|
|
1844
|
+
selfHealed.push(info);
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1847
|
+
for (const name of names) {
|
|
1848
|
+
try {
|
|
1849
|
+
upgraded.push(updateWorkflow(name, refresh, onSelfHeal));
|
|
1850
|
+
} catch (error) {
|
|
1851
|
+
if (error instanceof MarketplaceRefreshError) {
|
|
1852
|
+
if (!reportedMarketplaces.has(error.marketplace)) {
|
|
1853
|
+
reportedMarketplaces.add(error.marketplace);
|
|
1854
|
+
marketplaceFailures.push({ marketplace: error.marketplace, error });
|
|
1855
|
+
}
|
|
1856
|
+
} else {
|
|
1857
|
+
workflowFailures.push({
|
|
1858
|
+
name,
|
|
1859
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
return { upgraded, marketplaceFailures, workflowFailures, selfHealed };
|
|
1865
|
+
}
|
|
1866
|
+
function listWorkflows() {
|
|
1867
|
+
const dir = registryDir();
|
|
1868
|
+
const installed = fs9.existsSync(dir) ? fs9.readdirSync(dir, { withFileTypes: true }).filter(
|
|
1869
|
+
(entry) => entry.isDirectory() && fs9.existsSync(path8.join(dir, entry.name, "workflow.json"))
|
|
1870
|
+
).map((entry) => entry.name) : [];
|
|
1871
|
+
const builtins = listBuiltinWorkflows().filter(
|
|
1872
|
+
(name) => !installed.includes(name)
|
|
1873
|
+
);
|
|
1874
|
+
return [...builtins, ...installed];
|
|
1875
|
+
}
|
|
1876
|
+
function removeWorkflow(name) {
|
|
1877
|
+
const dir = path8.join(registryDir(), name);
|
|
1878
|
+
if (!fs9.existsSync(dir)) {
|
|
1879
|
+
throw new Error(`Workflow "${name}" not found.`);
|
|
1880
|
+
}
|
|
1881
|
+
fs9.rmSync(dir, { recursive: true, force: true });
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// src/core/workflows/applyWorkflow.ts
|
|
1885
|
+
function applyPromptTemplate(template, input, ctx) {
|
|
1886
|
+
return substituteVariables(template, { input, ...ctx });
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// src/core/workflows/plan.ts
|
|
1890
|
+
function compileWorkflowPlan(input) {
|
|
1891
|
+
if (!input.workflow) {
|
|
1892
|
+
return void 0;
|
|
1893
|
+
}
|
|
1894
|
+
const resolved = !input.resolvedPlugins && (!input.localPlugins || !input.codexPlugins) ? resolveWorkflowPlugins(input.workflow) : void 0;
|
|
1895
|
+
const resolvedPlugins = input.resolvedPlugins ?? resolved?.resolvedPlugins ?? [];
|
|
1896
|
+
const localPlugins = input.localPlugins ?? resolved?.localPlugins ?? [];
|
|
1897
|
+
const codexPlugins = input.codexPlugins ?? resolved?.codexPlugins ?? [];
|
|
1898
|
+
return {
|
|
1899
|
+
workflow: input.workflow,
|
|
1900
|
+
resolvedPlugins: resolvedPlugins.filter(
|
|
1901
|
+
(plugin, index, array) => array.findIndex((candidate) => candidate.ref === plugin.ref) === index
|
|
1902
|
+
),
|
|
1903
|
+
localPlugins: localPlugins.filter(
|
|
1904
|
+
(plugin, index, array) => array.findIndex((candidate) => candidate.ref === plugin.ref) === index
|
|
1905
|
+
),
|
|
1906
|
+
agentRoots: resolvedPlugins.map((plugin) => `${plugin.claudeArtifactDir}/agents`).filter((root, index, array) => array.indexOf(root) === index),
|
|
1907
|
+
codexPlugins: codexPlugins.filter(
|
|
1908
|
+
(target, index, array) => array.findIndex((candidate) => candidate.ref === target.ref) === index
|
|
1909
|
+
),
|
|
1910
|
+
pluginMcpConfig: input.pluginMcpConfig
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// src/core/workflows/sessionPlan.ts
|
|
1915
|
+
import fs10 from "fs";
|
|
1916
|
+
import crypto from "crypto";
|
|
1917
|
+
import path9 from "path";
|
|
1918
|
+
|
|
1919
|
+
// src/core/workflows/stateMachine.md
|
|
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";
|
|
1921
|
+
|
|
1922
|
+
// src/core/workflows/stateMachine.ts
|
|
1923
|
+
function buildTaskToolInstructions(harness) {
|
|
1924
|
+
switch (harness) {
|
|
1925
|
+
case "openai-codex":
|
|
1926
|
+
return `Use the \`update_plan\` tool to create and maintain the task list shown to the user for the full harness session.
|
|
1927
|
+
|
|
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
|
|
1930
|
+
- Do not carry forward prior session task IDs or assume prior session plan items still exist in the tool
|
|
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`;
|
|
1934
|
+
case "claude-code":
|
|
1935
|
+
case "opencode":
|
|
1936
|
+
default:
|
|
1937
|
+
return `Use \`TaskCreate\` and \`TaskUpdate\` to create and maintain the task list shown to the user for the full harness session.
|
|
1938
|
+
|
|
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
|
|
1941
|
+
- Do not refer to task IDs created in earlier sessions; they are session-scoped UI artifacts, not durable workflow state
|
|
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`;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
function buildStateMachineContent(harness = "claude-code") {
|
|
1948
|
+
return stateMachine_default.replace(
|
|
1949
|
+
"{{TASK_TOOL_INSTRUCTIONS}}",
|
|
1950
|
+
buildTaskToolInstructions(harness)
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
var STATE_MACHINE_CONTENT = buildStateMachineContent();
|
|
1954
|
+
|
|
1955
|
+
// src/core/workflows/sessionPlan.ts
|
|
1956
|
+
function readWorkflowOverride(projectDir, workflow, sessionId, journalPath, harness = "claude-code") {
|
|
1957
|
+
if (!workflow?.workflowFile) {
|
|
1958
|
+
return { workflowOverride: void 0, warnings: [] };
|
|
1959
|
+
}
|
|
1960
|
+
const resolvedPath = path9.isAbsolute(workflow.workflowFile) ? workflow.workflowFile : path9.resolve(projectDir, workflow.workflowFile);
|
|
1961
|
+
let workflowContent;
|
|
1962
|
+
try {
|
|
1963
|
+
workflowContent = fs10.readFileSync(resolvedPath, "utf-8");
|
|
1964
|
+
} catch {
|
|
1965
|
+
return {
|
|
1966
|
+
workflowOverride: void 0,
|
|
1967
|
+
warnings: [
|
|
1968
|
+
`Workflow file not found: ${workflow.workflowFile}. Continuing without workflow system instructions.`
|
|
1969
|
+
]
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
let composed = workflow.loop?.enabled ? buildStateMachineContent(harness) + "\n\n" + workflowContent : workflowContent;
|
|
1973
|
+
composed = substituteVariables(composed, {
|
|
1974
|
+
sessionId,
|
|
1975
|
+
journalPath: journalPath ?? void 0
|
|
1976
|
+
});
|
|
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
|
+
}
|
|
1993
|
+
return {
|
|
1994
|
+
workflowOverride: {
|
|
1995
|
+
appendSystemPromptFile: composedPath,
|
|
1996
|
+
developerInstructions: composed
|
|
1997
|
+
},
|
|
1998
|
+
warnings: []
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
function mergeOverrides(base, workflowOverride) {
|
|
2002
|
+
if (!base) return workflowOverride;
|
|
2003
|
+
if (!workflowOverride) return base;
|
|
2004
|
+
return {
|
|
2005
|
+
...base,
|
|
2006
|
+
...workflowOverride
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
function resolveJournalPath(input) {
|
|
2010
|
+
const loop = input.workflow?.loop;
|
|
2011
|
+
if (!loop?.enabled) {
|
|
2012
|
+
return null;
|
|
2013
|
+
}
|
|
2014
|
+
const rawPath = loopJournalPath(loop);
|
|
2015
|
+
if (!input.sessionId && rawPath.includes("{sessionId}")) {
|
|
2016
|
+
return null;
|
|
2017
|
+
}
|
|
2018
|
+
const promptPath = input.sessionId ? rawPath.replaceAll("{sessionId}", input.sessionId) : rawPath;
|
|
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
|
+
}
|
|
2036
|
+
return {
|
|
2037
|
+
absolutePath,
|
|
2038
|
+
promptPath
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
function createWorkflowRunState(input) {
|
|
2042
|
+
const { projectDir, sessionId, workflow, harness } = input;
|
|
2043
|
+
const journalResolved = resolveJournalPath({ projectDir, sessionId, workflow });
|
|
2044
|
+
const { workflowOverride, warnings } = readWorkflowOverride(
|
|
2045
|
+
projectDir,
|
|
2046
|
+
workflow,
|
|
2047
|
+
sessionId,
|
|
2048
|
+
journalResolved?.promptPath,
|
|
2049
|
+
harness
|
|
2050
|
+
);
|
|
2051
|
+
return {
|
|
2052
|
+
workflow,
|
|
2053
|
+
journalPathForPrompt: journalResolved?.promptPath,
|
|
2054
|
+
workflowOverride,
|
|
2055
|
+
warnings
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
function prepareWorkflowTurn(state, input) {
|
|
2059
|
+
const { workflow } = state;
|
|
2060
|
+
const iteration = input.iteration ?? 1;
|
|
2061
|
+
const isContinuation = workflow?.loop?.enabled === true && iteration > 1;
|
|
2062
|
+
const prompt = isContinuation ? buildContinuePrompt({
|
|
2063
|
+
...workflow.loop,
|
|
2064
|
+
journalPath: state.journalPathForPrompt ?? workflow.loop?.journalPath
|
|
2065
|
+
}) : workflow ? applyPromptTemplate(workflow.promptTemplate, input.prompt) : input.prompt;
|
|
2066
|
+
return {
|
|
2067
|
+
prompt,
|
|
2068
|
+
configOverride: mergeOverrides(
|
|
2069
|
+
input.configOverride,
|
|
2070
|
+
state.workflowOverride
|
|
2071
|
+
),
|
|
2072
|
+
warnings: state.warnings
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// src/core/workflows/workflowRunner.ts
|
|
2077
|
+
import crypto3 from "crypto";
|
|
2078
|
+
import fs12 from "fs";
|
|
2079
|
+
import path10 from "path";
|
|
2080
|
+
|
|
2081
|
+
// src/core/workflows/terminalOutcome.ts
|
|
2082
|
+
import fs11 from "fs";
|
|
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";
|
|
2085
|
+
function resolveTurnOutcome(input) {
|
|
2086
|
+
const { journalPath, loop } = input;
|
|
2087
|
+
if (!fs11.existsSync(journalPath)) {
|
|
2088
|
+
return {
|
|
2089
|
+
kind: "stop",
|
|
2090
|
+
status: "failed",
|
|
2091
|
+
stopReason: MISSING_JOURNAL_MESSAGE
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
const journal = parseJournalState(readJournal(journalPath), loop);
|
|
2095
|
+
if (journal.misplacedTerminalMarker) {
|
|
2096
|
+
return {
|
|
2097
|
+
kind: "stop",
|
|
2098
|
+
status: "failed",
|
|
2099
|
+
stopReason: MISPLACED_TERMINAL_MARKER_MESSAGE
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
if (journal.completed) {
|
|
2103
|
+
return { kind: "stop", status: "completed" };
|
|
2104
|
+
}
|
|
2105
|
+
if (journal.needsHuman) {
|
|
2106
|
+
return {
|
|
2107
|
+
kind: "suspend",
|
|
2108
|
+
status: "awaiting_attention",
|
|
2109
|
+
stopReason: journal.needsHumanReason ? `agent declared NEEDS_HUMAN: ${journal.needsHumanReason}` : "agent declared NEEDS_HUMAN",
|
|
2110
|
+
...journal.deprecatedMarker ? { deprecation: buildMarkerDeprecation(journal.deprecatedMarker) } : {}
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
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)
|
|
2139
|
+
return {
|
|
2140
|
+
...base,
|
|
2141
|
+
cause: "tokens",
|
|
2142
|
+
limit: loop.maxRunTokens,
|
|
2143
|
+
used: input.tokens ?? 0
|
|
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
|
+
};
|
|
2164
|
+
}
|
|
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.`;
|
|
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";
|
|
2380
|
+
|
|
2381
|
+
// src/core/runtime/failureTaxonomy.ts
|
|
2382
|
+
var RULES = [
|
|
2383
|
+
// ── hard, specific ──
|
|
2384
|
+
{
|
|
2385
|
+
pattern: /\b401\b|authentication_error|unauthorized|invalid.?api.?key|api key.*(invalid|revoked|expired)|oauth token.*(expired|revoked)|not.?logged.?in|please.*log ?in/i,
|
|
2386
|
+
classification: { kind: "hard", code: "auth" }
|
|
2387
|
+
},
|
|
2388
|
+
{
|
|
2389
|
+
pattern: /\b402\b|billing|credit balance|insufficient.?(funds|credits?|quota)|payment required|plan limit/i,
|
|
2390
|
+
classification: { kind: "hard", code: "billing" }
|
|
2391
|
+
},
|
|
2392
|
+
{
|
|
2393
|
+
pattern: /model.{0,20}not.?(found|supported|available)|no such model/i,
|
|
2394
|
+
classification: { kind: "hard", code: "model_not_found" }
|
|
2395
|
+
},
|
|
2396
|
+
// ── transient ──
|
|
2397
|
+
{
|
|
2398
|
+
pattern: /\b429\b|rate.?limit/i,
|
|
2399
|
+
classification: { kind: "transient", code: "rate_limit" }
|
|
2400
|
+
},
|
|
2401
|
+
{
|
|
2402
|
+
pattern: /\b529\b|overloaded/i,
|
|
2403
|
+
classification: { kind: "transient", code: "overloaded" }
|
|
2404
|
+
},
|
|
2405
|
+
{
|
|
2406
|
+
pattern: /\b(500|502|503|504)\b|internal server error|bad gateway|service unavailable|gateway timeout|server_error|api_error/i,
|
|
2407
|
+
classification: { kind: "transient", code: "server_error" }
|
|
2408
|
+
},
|
|
2409
|
+
{
|
|
2410
|
+
pattern: /econnrefused|econnreset|etimedout|enotfound|eai_again|epipe|socket hang up|fetch failed|network error|connection (error|closed|refused|reset)/i,
|
|
2411
|
+
classification: { kind: "transient", code: "network" }
|
|
2412
|
+
},
|
|
2413
|
+
// ── hard, generic (after the specific hard + transient rules) ──
|
|
2414
|
+
{
|
|
2415
|
+
pattern: /\b400\b|invalid_request_error|invalid request|malformed request/i,
|
|
2416
|
+
classification: { kind: "hard", code: "invalid_request" }
|
|
2417
|
+
}
|
|
2418
|
+
];
|
|
2419
|
+
function classifyTurnFailure(input) {
|
|
2420
|
+
const haystack = [input.errorMessage, input.lastStderr, input.lastMessage].filter((part) => typeof part === "string").join("\n");
|
|
2421
|
+
for (const rule of RULES) {
|
|
2422
|
+
if (rule.pattern.test(haystack)) {
|
|
2423
|
+
return rule.classification;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
return { kind: "hard", code: "unclassified" };
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// src/core/workflows/runMachine.ts
|
|
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)";
|
|
2435
|
+
}
|
|
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
|
+
};
|
|
2444
|
+
}
|
|
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) {
|
|
2468
|
+
return `This workflow run was suspended awaiting a human; it is now resumed. The human replied:
|
|
2469
|
+
|
|
2470
|
+
${reply}
|
|
2471
|
+
|
|
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
|
+
`;
|
|
2480
|
+
}
|
|
2481
|
+
function buildFailureDetail(event) {
|
|
2482
|
+
const parts = [];
|
|
2483
|
+
if (event.errorMessage) {
|
|
2484
|
+
parts.push(event.errorMessage);
|
|
2485
|
+
} else if (event.exitCode !== null) {
|
|
2486
|
+
parts.push(`Process exited with code ${event.exitCode}`);
|
|
2487
|
+
}
|
|
2488
|
+
if (event.lastStderr) {
|
|
2489
|
+
parts.push(event.lastStderr);
|
|
2490
|
+
} else if (event.streamMessage) {
|
|
2491
|
+
parts.push(event.streamMessage.slice(0, 300));
|
|
2492
|
+
}
|
|
2493
|
+
return parts.join(": ") || "Turn failed";
|
|
2494
|
+
}
|
|
2495
|
+
function initialRun(cfg, opts) {
|
|
2496
|
+
const initialSteers = opts.initialSteers ?? [];
|
|
2497
|
+
if (opts.resumedMemory) {
|
|
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
|
+
}
|
|
2521
|
+
};
|
|
2522
|
+
return { phase: phase2, memory, actions: kickoffActionsFor(phase2) };
|
|
2523
|
+
}
|
|
2524
|
+
const continuation = opts.initialContinuation ?? { mode: "fresh" };
|
|
2525
|
+
const iteration = 1;
|
|
2526
|
+
const prepared = prepareWorkflowTurn(cfg.workflowState, {
|
|
2527
|
+
prompt: cfg.initialPrompt,
|
|
2528
|
+
iteration,
|
|
2529
|
+
configOverride: void 0
|
|
2530
|
+
});
|
|
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
|
+
};
|
|
2542
|
+
return {
|
|
2543
|
+
phase,
|
|
2544
|
+
memory: {
|
|
2545
|
+
iteration,
|
|
2546
|
+
nudgeStreak: 0,
|
|
2547
|
+
retryStreak: 0,
|
|
2548
|
+
lastJournalHash: null,
|
|
2549
|
+
lastStopPrompt: prompt,
|
|
2550
|
+
lastStopContinuation: continuation,
|
|
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
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
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) {
|
|
2612
|
+
if (event.cancelled) {
|
|
2613
|
+
return {
|
|
2614
|
+
phase: { kind: "cancelled" },
|
|
2615
|
+
memory: incoming,
|
|
2616
|
+
actions: [{ type: "persist" }]
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
const memory = {
|
|
2620
|
+
...withCumulativeTokens(incoming, event.cumulativeTokens),
|
|
2621
|
+
...event.checkpoint ? { checkpoint: event.checkpoint } : {}
|
|
2622
|
+
};
|
|
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
|
+
});
|
|
2661
|
+
return {
|
|
2662
|
+
phase: {
|
|
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
|
|
2675
|
+
},
|
|
2676
|
+
actions: [
|
|
2677
|
+
{ type: "persist" },
|
|
2678
|
+
{
|
|
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
|
|
2693
|
+
}
|
|
2694
|
+
]
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
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
|
+
}
|
|
2713
|
+
return {
|
|
2714
|
+
phase: {
|
|
2715
|
+
kind: "awaiting_attention",
|
|
2716
|
+
stopReason: describeInterruption(event.interruption)
|
|
2717
|
+
},
|
|
2718
|
+
memory,
|
|
2719
|
+
actions: [{ type: "persist" }]
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
2722
|
+
const failed = event.hasError || event.exitCode !== null && event.exitCode !== 0;
|
|
2723
|
+
if (failed) {
|
|
2724
|
+
const failureDetail = buildFailureDetail(event);
|
|
2725
|
+
if (cfg.loop?.enabled) {
|
|
2726
|
+
const classification = classifyTurnFailure({
|
|
2727
|
+
errorMessage: event.errorMessage,
|
|
2728
|
+
lastStderr: event.stderrTail ?? event.lastStderr,
|
|
2729
|
+
lastMessage: event.streamMessage
|
|
2730
|
+
});
|
|
2731
|
+
if (classification.kind === "transient") {
|
|
2732
|
+
const retryStreak = memory.retryStreak + 1;
|
|
2733
|
+
const retryCap = cfg.loop.retryCap ?? DEFAULT_RETRY_CAP;
|
|
2734
|
+
if (retryStreak > retryCap) {
|
|
2735
|
+
return {
|
|
2736
|
+
phase: {
|
|
2737
|
+
kind: "awaiting_attention",
|
|
2738
|
+
stopReason: `retry cap reached: ${retryCap} transient failure${retryCap === 1 ? "" : "s"} (retryCap); last (${classification.code}): ${failureDetail}`
|
|
2739
|
+
},
|
|
2740
|
+
memory: { ...memory, retryStreak },
|
|
2741
|
+
actions: [{ type: "persist" }]
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
const backoffBase = cfg.loop.retryBackoffMs ?? DEFAULT_RETRY_BACKOFF_MS;
|
|
2745
|
+
const ms = backoffBase * 2 ** (retryStreak - 1);
|
|
2746
|
+
return {
|
|
2747
|
+
phase: {
|
|
2748
|
+
kind: "backing_off",
|
|
2749
|
+
ms,
|
|
2750
|
+
resume: {
|
|
2751
|
+
kind: "turn",
|
|
2752
|
+
prompt: phase.prompt,
|
|
2753
|
+
continuation: phase.continuation
|
|
2754
|
+
}
|
|
2755
|
+
},
|
|
2756
|
+
memory: { ...memory, retryStreak },
|
|
2757
|
+
actions: [{ type: "wait", ms }]
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
if (phase.continuation.mode === "resume") {
|
|
2761
|
+
const continuation2 = { mode: "fresh" };
|
|
2762
|
+
const prepared2 = prepareWorkflowTurn(cfg.workflowState, {
|
|
2763
|
+
prompt: cfg.initialPrompt,
|
|
2764
|
+
iteration: memory.iteration,
|
|
2765
|
+
configOverride: void 0
|
|
2766
|
+
});
|
|
2767
|
+
return {
|
|
2768
|
+
phase: {
|
|
2769
|
+
kind: "turn_in_flight",
|
|
2770
|
+
prompt: prepared2.prompt,
|
|
2771
|
+
continuation: continuation2,
|
|
2772
|
+
configOverride: prepared2.configOverride
|
|
2773
|
+
},
|
|
2774
|
+
memory: {
|
|
2775
|
+
...memory,
|
|
2776
|
+
lastStopPrompt: prepared2.prompt,
|
|
2777
|
+
lastStopContinuation: continuation2
|
|
2778
|
+
},
|
|
2779
|
+
actions: [
|
|
2780
|
+
{ type: "persist" },
|
|
2781
|
+
{
|
|
2782
|
+
type: "start_turn",
|
|
2783
|
+
prompt: prepared2.prompt,
|
|
2784
|
+
continuation: continuation2,
|
|
2785
|
+
configOverride: prepared2.configOverride
|
|
2786
|
+
}
|
|
2787
|
+
]
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
return {
|
|
2791
|
+
phase: {
|
|
2792
|
+
kind: "awaiting_attention",
|
|
2793
|
+
stopReason: `hard failure (${classification.code}): ${failureDetail} \u2014 not retried; needs a human`
|
|
2794
|
+
},
|
|
2795
|
+
memory,
|
|
2796
|
+
actions: [{ type: "persist" }]
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
return {
|
|
2800
|
+
phase: { kind: "failed", stopReason: failureDetail },
|
|
2801
|
+
memory,
|
|
2802
|
+
actions: [{ type: "persist" }]
|
|
2803
|
+
};
|
|
2804
|
+
}
|
|
2805
|
+
const memoryAfterSuccess = { ...memory, retryStreak: 0 };
|
|
2806
|
+
if (event.transportBroken) {
|
|
2807
|
+
return {
|
|
2808
|
+
phase: {
|
|
2809
|
+
kind: "failed",
|
|
2810
|
+
stopReason: `Hook transport broken: observed a tool use in the Claude stream but received no PreToolUse events.`
|
|
2811
|
+
},
|
|
2812
|
+
memory: memoryAfterSuccess,
|
|
2813
|
+
actions: [{ type: "persist" }]
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
if (!cfg.loop?.enabled) {
|
|
2817
|
+
return {
|
|
2818
|
+
phase: { kind: "completed" },
|
|
2819
|
+
memory: memoryAfterSuccess,
|
|
2820
|
+
actions: [{ type: "persist" }]
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
const loop = cfg.loop;
|
|
2824
|
+
if (event.outcome && (event.outcome.kind === "stop" || event.outcome.kind === "suspend")) {
|
|
2825
|
+
const outcome = event.outcome;
|
|
2826
|
+
const nextPhase = outcome.status === "completed" ? { kind: "completed" } : outcome.status === "failed" ? { kind: "failed", stopReason: outcome.stopReason } : {
|
|
2827
|
+
kind: "awaiting_attention",
|
|
2828
|
+
stopReason: outcome.stopReason ?? "stopped"
|
|
2829
|
+
};
|
|
2830
|
+
const deprecation = outcome.kind === "suspend" ? outcome.deprecation : void 0;
|
|
2831
|
+
return {
|
|
2832
|
+
phase: nextPhase,
|
|
2833
|
+
memory: memoryAfterSuccess,
|
|
2834
|
+
actions: [
|
|
2835
|
+
...deprecation ? [{ type: "warn", message: deprecation }] : [],
|
|
2836
|
+
{ type: "persist" }
|
|
2837
|
+
]
|
|
2838
|
+
};
|
|
2839
|
+
}
|
|
2840
|
+
const journalHash = hashJournalContent(event.journalContent);
|
|
2841
|
+
let nudgeStreak = memoryAfterSuccess.nudgeStreak;
|
|
2842
|
+
if (journalHash !== memoryAfterSuccess.lastJournalHash) {
|
|
2843
|
+
nudgeStreak = 0;
|
|
2844
|
+
}
|
|
2845
|
+
const memoryWithHash = {
|
|
2846
|
+
...memoryAfterSuccess,
|
|
2847
|
+
lastJournalHash: journalHash,
|
|
2848
|
+
nudgeStreak
|
|
2849
|
+
};
|
|
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) : "");
|
|
2853
|
+
if (event.adapterSessionId) {
|
|
2854
|
+
const nextNudgeStreak = nudgeStreak + 1;
|
|
2855
|
+
const nudgeCap = loop.nudgeCap ?? DEFAULT_NUDGE_CAP;
|
|
2856
|
+
if (nextNudgeStreak > nudgeCap) {
|
|
2857
|
+
return {
|
|
2858
|
+
phase: {
|
|
2859
|
+
kind: "awaiting_attention",
|
|
2860
|
+
stopReason: `nudge cap reached: ${nudgeCap} nudge${nudgeCap === 1 ? "" : "s"} (nudgeCap) without journal progress or a terminal marker`
|
|
2861
|
+
},
|
|
2862
|
+
memory: { ...memoryWithHash, nudgeStreak: nextNudgeStreak },
|
|
2863
|
+
actions: [{ type: "persist" }]
|
|
2864
|
+
};
|
|
2865
|
+
}
|
|
2866
|
+
const promptOverride = buildNudgePrompt(
|
|
2867
|
+
{ ...loop, journalPath: cfg.journalPromptPath ?? loop.journalPath },
|
|
2868
|
+
{
|
|
2869
|
+
skeletonNotReplaced: hasSkeletonMarker(event.journalContent)
|
|
2870
|
+
}
|
|
2871
|
+
) + sizeNudgeSuffix;
|
|
2872
|
+
const nextIteration2 = memoryWithHash.iteration + 1;
|
|
2873
|
+
const continuation2 = {
|
|
2874
|
+
mode: "resume",
|
|
2875
|
+
handle: event.adapterSessionId
|
|
2876
|
+
};
|
|
2877
|
+
const prepared2 = prepareWorkflowTurn(cfg.workflowState, {
|
|
2878
|
+
prompt: cfg.initialPrompt,
|
|
2879
|
+
iteration: nextIteration2,
|
|
2880
|
+
configOverride: void 0
|
|
2881
|
+
});
|
|
2882
|
+
return {
|
|
2883
|
+
phase: {
|
|
2884
|
+
kind: "turn_in_flight",
|
|
2885
|
+
prompt: promptOverride,
|
|
2886
|
+
continuation: continuation2,
|
|
2887
|
+
configOverride: prepared2.configOverride
|
|
2888
|
+
},
|
|
2889
|
+
memory: {
|
|
2890
|
+
...memoryWithHash,
|
|
2891
|
+
nudgeStreak: nextNudgeStreak,
|
|
2892
|
+
iteration: nextIteration2,
|
|
2893
|
+
lastStopPrompt: promptOverride,
|
|
2894
|
+
lastStopContinuation: continuation2
|
|
2895
|
+
},
|
|
2896
|
+
actions: [
|
|
2897
|
+
{ type: "persist" },
|
|
2898
|
+
{ type: "notify_iteration_complete" },
|
|
2899
|
+
{
|
|
2900
|
+
type: "start_turn",
|
|
2901
|
+
prompt: promptOverride,
|
|
2902
|
+
continuation: continuation2,
|
|
2903
|
+
configOverride: prepared2.configOverride
|
|
2904
|
+
}
|
|
2905
|
+
]
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
const nextIteration = memoryWithHash.iteration + 1;
|
|
2909
|
+
const continuation = { mode: "fresh" };
|
|
2910
|
+
const prepared = prepareWorkflowTurn(cfg.workflowState, {
|
|
2911
|
+
prompt: cfg.initialPrompt,
|
|
2912
|
+
iteration: nextIteration,
|
|
2913
|
+
configOverride: void 0
|
|
2914
|
+
});
|
|
2915
|
+
const promptWithSizeNudge = prepared.prompt + sizeNudgeSuffix;
|
|
2916
|
+
return {
|
|
2917
|
+
phase: {
|
|
2918
|
+
kind: "turn_in_flight",
|
|
2919
|
+
prompt: promptWithSizeNudge,
|
|
2920
|
+
continuation,
|
|
2921
|
+
configOverride: prepared.configOverride
|
|
2922
|
+
},
|
|
2923
|
+
memory: {
|
|
2924
|
+
...memoryWithHash,
|
|
2925
|
+
iteration: nextIteration,
|
|
2926
|
+
lastStopPrompt: promptWithSizeNudge,
|
|
2927
|
+
lastStopContinuation: continuation
|
|
2928
|
+
},
|
|
2929
|
+
actions: [
|
|
2930
|
+
{ type: "persist" },
|
|
2931
|
+
{ type: "notify_iteration_complete" },
|
|
2932
|
+
{
|
|
2933
|
+
type: "start_turn",
|
|
2934
|
+
prompt: promptWithSizeNudge,
|
|
2935
|
+
continuation,
|
|
2936
|
+
configOverride: prepared.configOverride
|
|
2937
|
+
}
|
|
2938
|
+
]
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
function handleBackingOff(phase, memory, event, cfg) {
|
|
2942
|
+
if (event.cancelled) {
|
|
2943
|
+
return { phase: { kind: "cancelled" }, memory, actions: [{ type: "persist" }] };
|
|
2944
|
+
}
|
|
2945
|
+
const continuation = event.adapterSessionId ? { mode: "resume", handle: event.adapterSessionId } : phase.resume.continuation;
|
|
2946
|
+
const prompt = continuation.mode === "fresh" ? phase.resume.prompt : cfg.loop ? buildContinuePrompt(cfg.loop) : "Continue.";
|
|
2947
|
+
const prepared = prepareWorkflowTurn(cfg.workflowState, {
|
|
2948
|
+
prompt: cfg.initialPrompt,
|
|
2949
|
+
iteration: memory.iteration,
|
|
2950
|
+
configOverride: void 0
|
|
2951
|
+
});
|
|
2952
|
+
return {
|
|
2953
|
+
phase: {
|
|
2954
|
+
kind: "turn_in_flight",
|
|
2955
|
+
prompt,
|
|
2956
|
+
continuation,
|
|
2957
|
+
configOverride: prepared.configOverride
|
|
2958
|
+
},
|
|
2959
|
+
memory: {
|
|
2960
|
+
...memory,
|
|
2961
|
+
lastStopPrompt: prompt,
|
|
2962
|
+
lastStopContinuation: continuation
|
|
2963
|
+
},
|
|
2964
|
+
actions: [
|
|
2965
|
+
{ type: "persist" },
|
|
2966
|
+
{
|
|
2967
|
+
type: "start_turn",
|
|
2968
|
+
prompt,
|
|
2969
|
+
continuation,
|
|
2970
|
+
configOverride: prepared.configOverride
|
|
2971
|
+
}
|
|
2972
|
+
]
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2975
|
+
function handleAwaitingAttention(phase, memory, event, cfg) {
|
|
2976
|
+
if (event.checkpoint) memory = { ...memory, checkpoint: event.checkpoint };
|
|
2977
|
+
const nextIteration = memory.iteration + 1;
|
|
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
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
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
|
+
);
|
|
3002
|
+
const prepared = prepareWorkflowTurn(cfg.workflowState, {
|
|
3003
|
+
prompt: cfg.initialPrompt,
|
|
3004
|
+
iteration: nextIteration,
|
|
3005
|
+
configOverride: void 0
|
|
3006
|
+
});
|
|
3007
|
+
return {
|
|
3008
|
+
phase: {
|
|
3009
|
+
kind: "turn_in_flight",
|
|
3010
|
+
prompt,
|
|
3011
|
+
continuation,
|
|
3012
|
+
configOverride: prepared.configOverride
|
|
3013
|
+
},
|
|
3014
|
+
memory: {
|
|
3015
|
+
...memory,
|
|
3016
|
+
iteration: nextIteration,
|
|
3017
|
+
lastStopPrompt: prompt,
|
|
3018
|
+
lastStopContinuation: continuation,
|
|
3019
|
+
parkedAfterHandover: false,
|
|
3020
|
+
lastRestartHash: wakesFresh && memory.checkpoint ? hashJournalContent(memory.checkpoint.contract.text) : memory.lastRestartHash
|
|
3021
|
+
},
|
|
3022
|
+
actions: [
|
|
3023
|
+
{ type: "persist" },
|
|
3024
|
+
{
|
|
3025
|
+
type: "start_turn",
|
|
3026
|
+
restartTokens: wakesFresh ? memory.checkpoint?.contract.tokens : void 0,
|
|
3027
|
+
prompt,
|
|
3028
|
+
continuation,
|
|
3029
|
+
configOverride: prepared.configOverride
|
|
3030
|
+
}
|
|
3031
|
+
]
|
|
3032
|
+
};
|
|
3033
|
+
}
|
|
3034
|
+
function transition(phase, memory, event, cfg) {
|
|
3035
|
+
switch (phase.kind) {
|
|
3036
|
+
case "turn_in_flight": {
|
|
3037
|
+
if (event.type === "steer")
|
|
3038
|
+
return handleSteer(phase, memory, event.steer);
|
|
3039
|
+
if (event.type !== "turn_finished") {
|
|
3040
|
+
throw new Error(
|
|
3041
|
+
`runMachine: phase 'turn_in_flight' received unexpected event '${event.type}'`
|
|
3042
|
+
);
|
|
3043
|
+
}
|
|
3044
|
+
return handleTurnInFlight(phase, memory, event, cfg);
|
|
3045
|
+
}
|
|
3046
|
+
case "backing_off": {
|
|
3047
|
+
if (event.type === "steer")
|
|
3048
|
+
return handleSteer(phase, memory, event.steer);
|
|
3049
|
+
if (event.type !== "backoff_elapsed") {
|
|
3050
|
+
throw new Error(
|
|
3051
|
+
`runMachine: phase 'backing_off' received unexpected event '${event.type}'`
|
|
3052
|
+
);
|
|
3053
|
+
}
|
|
3054
|
+
return handleBackingOff(phase, memory, event, cfg);
|
|
3055
|
+
}
|
|
3056
|
+
case "awaiting_attention": {
|
|
3057
|
+
if (event.type !== "woken") {
|
|
3058
|
+
throw new Error(
|
|
3059
|
+
`runMachine: phase 'awaiting_attention' received unexpected event '${event.type}'`
|
|
3060
|
+
);
|
|
3061
|
+
}
|
|
3062
|
+
return handleAwaitingAttention(phase, memory, event, cfg);
|
|
3063
|
+
}
|
|
3064
|
+
case "completed":
|
|
3065
|
+
case "failed":
|
|
3066
|
+
case "cancelled": {
|
|
3067
|
+
throw new Error(
|
|
3068
|
+
`runMachine: step() called on terminal phase '${phase.kind}'`
|
|
3069
|
+
);
|
|
3070
|
+
}
|
|
3071
|
+
default: {
|
|
3072
|
+
const _exhaustive = phase;
|
|
3073
|
+
throw new Error(
|
|
3074
|
+
`runMachine: step() called on unrecognized phase kind '${_exhaustive.kind}'`
|
|
3075
|
+
);
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
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
|
+
}
|
|
3158
|
+
function serializeRunMemory(memory) {
|
|
3159
|
+
return JSON.stringify(memory);
|
|
3160
|
+
}
|
|
3161
|
+
function deserializeRunMemory(json) {
|
|
3162
|
+
if (!json) return null;
|
|
3163
|
+
let parsed;
|
|
3164
|
+
try {
|
|
3165
|
+
parsed = JSON.parse(json);
|
|
3166
|
+
} catch {
|
|
3167
|
+
return null;
|
|
3168
|
+
}
|
|
3169
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
3170
|
+
const candidate = parsed;
|
|
3171
|
+
if (!("lastJournalHash" in candidate) && "lastTrackerHash" in candidate) {
|
|
3172
|
+
candidate.lastJournalHash = candidate.lastTrackerHash;
|
|
3173
|
+
delete candidate.lastTrackerHash;
|
|
3174
|
+
}
|
|
3175
|
+
const continuation = candidate.lastStopContinuation;
|
|
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)) {
|
|
3193
|
+
return null;
|
|
3194
|
+
}
|
|
3195
|
+
return candidate;
|
|
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
|
+
}
|
|
3212
|
+
|
|
3213
|
+
// src/core/workflows/workflowRunner.ts
|
|
3214
|
+
var NULL_TOKENS = {
|
|
3215
|
+
input: null,
|
|
3216
|
+
output: null,
|
|
3217
|
+
cacheRead: null,
|
|
3218
|
+
cacheWrite: null,
|
|
3219
|
+
total: null,
|
|
3220
|
+
contextSize: null,
|
|
3221
|
+
contextWindowSize: null
|
|
3222
|
+
};
|
|
3223
|
+
var JOURNAL_SKELETON_TEMPLATE = `${JOURNAL_SKELETON_MARKER}
|
|
3224
|
+
# Workflow Journal
|
|
3225
|
+
|
|
3226
|
+
**Session**: {sessionId}
|
|
3227
|
+
**Journal**: {journalPath}
|
|
3228
|
+
**Goal**: {input}
|
|
3229
|
+
|
|
3230
|
+
---
|
|
3231
|
+
|
|
3232
|
+
> This journal was created by the runner. Update it as you work.
|
|
3233
|
+
> See the Turn Protocol for journal conventions.
|
|
3234
|
+
|
|
3235
|
+
## Status
|
|
3236
|
+
|
|
3237
|
+
Orientation in progress.
|
|
3238
|
+
|
|
3239
|
+
## Plan
|
|
3240
|
+
|
|
3241
|
+
_To be created during orientation._
|
|
3242
|
+
|
|
3243
|
+
## Progress
|
|
3244
|
+
|
|
3245
|
+
_No progress yet._
|
|
3246
|
+
`;
|
|
3247
|
+
function openRunSection(journalPath, opts) {
|
|
3248
|
+
let existing;
|
|
3249
|
+
try {
|
|
3250
|
+
existing = fs12.readFileSync(journalPath, "utf-8");
|
|
3251
|
+
} catch {
|
|
3252
|
+
return;
|
|
3253
|
+
}
|
|
3254
|
+
const banner = `
|
|
3255
|
+
|
|
3256
|
+
---
|
|
3257
|
+
|
|
3258
|
+
## New Workflow Run
|
|
3259
|
+
|
|
3260
|
+
**Run**: ${opts.runId}
|
|
3261
|
+
**Goal**: ${opts.goal}
|
|
3262
|
+
|
|
3263
|
+
_Sections above belong to earlier Workflow Runs in this Athena Session._
|
|
3264
|
+
`;
|
|
3265
|
+
try {
|
|
3266
|
+
fs12.writeFileSync(
|
|
3267
|
+
journalPath,
|
|
3268
|
+
demoteTerminalMarkers(existing.trimEnd(), opts.markers) + banner,
|
|
3269
|
+
"utf-8"
|
|
3270
|
+
);
|
|
3271
|
+
} catch {
|
|
3272
|
+
}
|
|
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
|
+
}
|
|
3320
|
+
function mergeTokens(base, next) {
|
|
3321
|
+
const input = (base.input ?? 0) + (next.input ?? 0);
|
|
3322
|
+
const output = (base.output ?? 0) + (next.output ?? 0);
|
|
3323
|
+
const cacheRead = (base.cacheRead ?? 0) + (next.cacheRead ?? 0);
|
|
3324
|
+
const cacheWrite = (base.cacheWrite ?? 0) + (next.cacheWrite ?? 0);
|
|
3325
|
+
const hasAny = base.input !== null || next.input !== null || base.output !== null || next.output !== null || base.cacheRead !== null || next.cacheRead !== null || base.cacheWrite !== null || next.cacheWrite !== null;
|
|
3326
|
+
if (!hasAny)
|
|
3327
|
+
return {
|
|
3328
|
+
...NULL_TOKENS,
|
|
3329
|
+
contextSize: next.contextSize,
|
|
3330
|
+
contextWindowSize: next.contextWindowSize
|
|
3331
|
+
};
|
|
3332
|
+
return {
|
|
3333
|
+
input,
|
|
3334
|
+
output,
|
|
3335
|
+
cacheRead,
|
|
3336
|
+
cacheWrite,
|
|
3337
|
+
total: input + output + cacheRead + cacheWrite,
|
|
3338
|
+
contextSize: next.contextSize ?? base.contextSize,
|
|
3339
|
+
contextWindowSize: next.contextWindowSize ?? base.contextWindowSize
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
async function delayWithCancel(ms, isCancelled) {
|
|
3343
|
+
const slice = 250;
|
|
3344
|
+
for (let waited = 0; waited < ms && !isCancelled(); waited += slice) {
|
|
3345
|
+
await new Promise(
|
|
3346
|
+
(resolve) => setTimeout(resolve, Math.min(slice, ms - waited))
|
|
3347
|
+
);
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
function readUnitRecords(journalAbsPath) {
|
|
3351
|
+
const unitsDir = path10.join(path10.dirname(journalAbsPath), "units");
|
|
3352
|
+
let names;
|
|
3353
|
+
try {
|
|
3354
|
+
names = fs12.readdirSync(unitsDir).filter((name) => name.endsWith(".md"));
|
|
3355
|
+
} catch {
|
|
3356
|
+
return [];
|
|
3357
|
+
}
|
|
3358
|
+
const records = [];
|
|
3359
|
+
for (const name of names.sort()) {
|
|
3360
|
+
try {
|
|
3361
|
+
records.push({
|
|
3362
|
+
recordPath: `units/${name}`,
|
|
3363
|
+
content: fs12.readFileSync(path10.join(unitsDir, name), "utf-8")
|
|
3364
|
+
});
|
|
3365
|
+
} catch {
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
return records;
|
|
3369
|
+
}
|
|
3370
|
+
function defaultCreateJournal(journalPath, content) {
|
|
3371
|
+
fs12.mkdirSync(path10.dirname(journalPath), { recursive: true });
|
|
3372
|
+
try {
|
|
3373
|
+
fs12.writeFileSync(journalPath, content, { encoding: "utf-8", flag: "wx" });
|
|
3374
|
+
} catch (e) {
|
|
3375
|
+
if (e.code !== "EEXIST") throw e;
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
function isTerminalPhase(phase) {
|
|
3379
|
+
return phase.kind === "awaiting_attention" || phase.kind === "completed" || phase.kind === "failed" || phase.kind === "cancelled";
|
|
3380
|
+
}
|
|
3381
|
+
function terminalPhaseToStatus(phase) {
|
|
3382
|
+
switch (phase.kind) {
|
|
3383
|
+
case "awaiting_attention":
|
|
3384
|
+
return { status: "awaiting_attention", stopReason: phase.stopReason };
|
|
3385
|
+
case "completed":
|
|
3386
|
+
return { status: "completed" };
|
|
3387
|
+
case "failed":
|
|
3388
|
+
return { status: "failed", stopReason: phase.stopReason };
|
|
3389
|
+
case "cancelled":
|
|
3390
|
+
return { status: "cancelled" };
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
function createWorkflowRunner(input) {
|
|
3394
|
+
const runId = input.resumeRunId ?? crypto3.randomUUID();
|
|
3395
|
+
let cancelled = false;
|
|
3396
|
+
let status = "running";
|
|
3397
|
+
let cumulativeTokens = {
|
|
3398
|
+
...input.resumedRunMemory?.usage ?? NULL_TOKENS,
|
|
3399
|
+
total: input.resumedRunMemory?.cumulativeTokens ?? null
|
|
3400
|
+
};
|
|
3401
|
+
let stopReason;
|
|
3402
|
+
let interruption;
|
|
3403
|
+
let memory;
|
|
3404
|
+
const preStartSteers = [];
|
|
3405
|
+
let applySteer = null;
|
|
3406
|
+
const journalResolved = resolveJournalPath({
|
|
3407
|
+
projectDir: input.projectDir,
|
|
3408
|
+
sessionId: input.sessionId,
|
|
3409
|
+
workflow: input.workflow
|
|
3410
|
+
});
|
|
3411
|
+
const journalAbsPath = journalResolved?.absolutePath ?? null;
|
|
3412
|
+
const journalPromptPath = journalResolved?.promptPath;
|
|
3413
|
+
function snapshot() {
|
|
3414
|
+
const adapterSessionId = input.currentAdapterSessionId?.() ?? void 0;
|
|
3415
|
+
return {
|
|
3416
|
+
runId,
|
|
3417
|
+
sessionId: input.sessionId,
|
|
3418
|
+
workflowName: input.workflow?.name,
|
|
3419
|
+
iteration: memory?.iteration ?? 0,
|
|
3420
|
+
maxIterations: input.workflow?.loop?.maxIterations ?? 1,
|
|
3421
|
+
status,
|
|
3422
|
+
stopReason,
|
|
3423
|
+
journalPath: journalPromptPath,
|
|
3424
|
+
...adapterSessionId ? { adapterSessionId } : {},
|
|
3425
|
+
...memory ? { runMemoryJson: serializeRunMemory(memory) } : {},
|
|
3426
|
+
...interruption ? { interruption } : {}
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
3429
|
+
let persistenceFailure;
|
|
3430
|
+
const hasPersistenceFailure = () => persistenceFailure !== void 0;
|
|
3431
|
+
function persist() {
|
|
3432
|
+
if (persistenceFailure) throw persistenceFailure;
|
|
3433
|
+
try {
|
|
3434
|
+
input.persistRunState(snapshot());
|
|
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;
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
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
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
const result = (async () => {
|
|
3462
|
+
await Promise.resolve();
|
|
3463
|
+
if (journalAbsPath && input.workflow?.loop?.enabled) {
|
|
3464
|
+
const content = substituteVariables(JOURNAL_SKELETON_TEMPLATE, {
|
|
3465
|
+
sessionId: input.sessionId,
|
|
3466
|
+
journalPath: journalPromptPath,
|
|
3467
|
+
input: input.prompt
|
|
3468
|
+
});
|
|
3469
|
+
const write = input.createJournal ?? defaultCreateJournal;
|
|
3470
|
+
const journalExisted = fs12.existsSync(journalAbsPath);
|
|
3471
|
+
write(journalAbsPath, content);
|
|
3472
|
+
if (journalExisted && !input.resumeRunId) {
|
|
3473
|
+
openRunSection(journalAbsPath, {
|
|
3474
|
+
runId,
|
|
3475
|
+
goal: input.prompt,
|
|
3476
|
+
markers: {
|
|
3477
|
+
completionMarker: input.workflow.loop.completionMarker,
|
|
3478
|
+
needsHumanMarker: input.workflow.loop.needsHumanMarker,
|
|
3479
|
+
blockedMarker: input.workflow.loop.blockedMarker
|
|
3480
|
+
}
|
|
3481
|
+
});
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
const workflowState = createWorkflowRunState({
|
|
3485
|
+
projectDir: input.projectDir,
|
|
3486
|
+
sessionId: input.sessionId,
|
|
3487
|
+
workflow: input.workflow,
|
|
3488
|
+
harness: input.harness
|
|
3489
|
+
});
|
|
3490
|
+
const loop = input.workflow?.loop;
|
|
3491
|
+
const cfg = {
|
|
3492
|
+
runId,
|
|
3493
|
+
contextKey: crypto3.randomUUID(),
|
|
3494
|
+
workflowState,
|
|
3495
|
+
initialPrompt: input.prompt,
|
|
3496
|
+
loop,
|
|
3497
|
+
journalAbsPath,
|
|
3498
|
+
journalPromptPath
|
|
3499
|
+
};
|
|
3500
|
+
const initial = createInitialRun(cfg, {
|
|
3501
|
+
initialContinuation: input.initialContinuation,
|
|
3502
|
+
waking: !!(input.resumeRunId && loop?.enabled),
|
|
3503
|
+
resumedMemory: input.resumedRunMemory,
|
|
3504
|
+
initialSteers: preStartSteers.splice(0),
|
|
3505
|
+
parkedInterruption: input.parkedInterruption,
|
|
3506
|
+
awaitingAttentionStopReason: input.resumedStopReason
|
|
3507
|
+
});
|
|
3508
|
+
let phase = initial.phase;
|
|
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
|
+
}
|
|
3641
|
+
async function performStartTurn(prompt, continuation, configOverride) {
|
|
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,
|
|
3644
|
+
continuation,
|
|
3645
|
+
configOverride,
|
|
3646
|
+
iteration: memory.iteration
|
|
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
|
+
};
|
|
3654
|
+
if (cancelled) {
|
|
3655
|
+
return {
|
|
3656
|
+
type: "turn_finished",
|
|
3657
|
+
cancelled: true,
|
|
3658
|
+
hasError: false,
|
|
3659
|
+
exitCode: null,
|
|
3660
|
+
streamMessage: null,
|
|
3661
|
+
transportBroken: false,
|
|
3662
|
+
handoverRequestHandle: null,
|
|
3663
|
+
interruption: null,
|
|
3664
|
+
adapterSessionId: null,
|
|
3665
|
+
outcome: null,
|
|
3666
|
+
journalContent: "",
|
|
3667
|
+
...measured,
|
|
3668
|
+
shedIntegrity: null,
|
|
3669
|
+
cumulativeTokens: null
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3672
|
+
const runTotal = cumulativeTokens.total;
|
|
3673
|
+
const handoverRequest = input.handover?.takeRequest() ?? null;
|
|
3674
|
+
if (handoverRequest) {
|
|
3675
|
+
const journalContent2 = loop?.enabled && journalAbsPath ? readJournal(journalAbsPath) : "";
|
|
3676
|
+
return {
|
|
3677
|
+
type: "turn_finished",
|
|
3678
|
+
cancelled: false,
|
|
3679
|
+
hasError: false,
|
|
3680
|
+
exitCode: null,
|
|
3681
|
+
streamMessage: null,
|
|
3682
|
+
transportBroken: false,
|
|
3683
|
+
handoverRequestHandle: handoverRequest.handle,
|
|
3684
|
+
interruption: null,
|
|
3685
|
+
adapterSessionId: null,
|
|
3686
|
+
outcome: null,
|
|
3687
|
+
journalContent: journalContent2,
|
|
3688
|
+
checkpoint: journalAbsPath ? checkpointFromJournal(journalContent2) : null,
|
|
3689
|
+
...measured,
|
|
3690
|
+
shedIntegrity: observeShedIntegrity(journalContent2),
|
|
3691
|
+
cumulativeTokens: runTotal
|
|
3692
|
+
};
|
|
3693
|
+
}
|
|
3694
|
+
const interruption2 = input.checkInterruption?.() ?? null;
|
|
3695
|
+
if (interruption2) {
|
|
3696
|
+
return {
|
|
3697
|
+
type: "turn_finished",
|
|
3698
|
+
cancelled: false,
|
|
3699
|
+
hasError: false,
|
|
3700
|
+
exitCode: null,
|
|
3701
|
+
streamMessage: null,
|
|
3702
|
+
transportBroken: false,
|
|
3703
|
+
handoverRequestHandle: null,
|
|
3704
|
+
interruption: interruption2,
|
|
3705
|
+
adapterSessionId: null,
|
|
3706
|
+
outcome: null,
|
|
3707
|
+
journalContent: "",
|
|
3708
|
+
...measured,
|
|
3709
|
+
shedIntegrity: null,
|
|
3710
|
+
cumulativeTokens: runTotal
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
const adapterSessionId = input.currentAdapterSessionId?.() ?? null;
|
|
3714
|
+
const hasError = !!turnResult.error;
|
|
3715
|
+
const failed = hasError || turnResult.exitCode !== null && turnResult.exitCode !== 0;
|
|
3716
|
+
if (failed) {
|
|
3717
|
+
return {
|
|
3718
|
+
type: "turn_finished",
|
|
3719
|
+
cancelled: false,
|
|
3720
|
+
hasError,
|
|
3721
|
+
errorMessage: turnResult.error?.message,
|
|
3722
|
+
exitCode: turnResult.exitCode,
|
|
3723
|
+
lastStderr: turnResult.lastStderr,
|
|
3724
|
+
stderrTail: turnResult.stderrTail,
|
|
3725
|
+
streamMessage: turnResult.streamMessage,
|
|
3726
|
+
transportBroken: false,
|
|
3727
|
+
handoverRequestHandle: null,
|
|
3728
|
+
interruption: null,
|
|
3729
|
+
adapterSessionId,
|
|
3730
|
+
outcome: null,
|
|
3731
|
+
journalContent: "",
|
|
3732
|
+
...measured,
|
|
3733
|
+
shedIntegrity: null,
|
|
3734
|
+
cumulativeTokens: runTotal
|
|
3735
|
+
};
|
|
3736
|
+
}
|
|
3737
|
+
const transport = turnResult.diagnostics?.transport;
|
|
3738
|
+
const transportBroken = !!(transport && transport.streamToolUses > 0 && transport.preToolUseEvents === 0);
|
|
3739
|
+
let journalContent = "";
|
|
3740
|
+
let outcome = null;
|
|
3741
|
+
if (!transportBroken && loop?.enabled && journalAbsPath) {
|
|
3742
|
+
journalContent = readJournal(journalAbsPath);
|
|
3743
|
+
outcome = resolveTurnOutcome({
|
|
3744
|
+
journalPath: journalAbsPath,
|
|
3745
|
+
loop,
|
|
3746
|
+
iteration: memory.iteration
|
|
3747
|
+
});
|
|
3748
|
+
observePhase(journalContent, memory.iteration);
|
|
3749
|
+
}
|
|
3750
|
+
return {
|
|
3751
|
+
type: "turn_finished",
|
|
3752
|
+
cancelled: false,
|
|
3753
|
+
hasError: false,
|
|
3754
|
+
exitCode: turnResult.exitCode,
|
|
3755
|
+
streamMessage: turnResult.streamMessage,
|
|
3756
|
+
transportBroken,
|
|
3757
|
+
handoverRequestHandle: null,
|
|
3758
|
+
interruption: null,
|
|
3759
|
+
adapterSessionId,
|
|
3760
|
+
outcome,
|
|
3761
|
+
journalContent,
|
|
3762
|
+
checkpoint: checkpointFromJournal(journalContent),
|
|
3763
|
+
...measured,
|
|
3764
|
+
shedIntegrity: journalContent === "" ? null : observeShedIntegrity(journalContent),
|
|
3765
|
+
cumulativeTokens: runTotal
|
|
3766
|
+
};
|
|
3767
|
+
}
|
|
3768
|
+
function observeShedIntegrity(journalContent) {
|
|
3769
|
+
if (!journalAbsPath || !loop?.enabled) return null;
|
|
3770
|
+
try {
|
|
3771
|
+
return checkShedIntegrity(
|
|
3772
|
+
journalContent,
|
|
3773
|
+
readUnitRecords(journalAbsPath)
|
|
3774
|
+
);
|
|
3775
|
+
} catch {
|
|
3776
|
+
return null;
|
|
3777
|
+
}
|
|
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;
|
|
3786
|
+
}
|
|
3787
|
+
async function performWait(ms) {
|
|
3788
|
+
await delayWithCancel(ms, () => cancelled);
|
|
3789
|
+
if (cancelled) {
|
|
3790
|
+
return {
|
|
3791
|
+
type: "backoff_elapsed",
|
|
3792
|
+
cancelled: true,
|
|
3793
|
+
adapterSessionId: null
|
|
3794
|
+
};
|
|
3795
|
+
}
|
|
3796
|
+
const adapterSessionId = input.currentAdapterSessionId?.() ?? null;
|
|
3797
|
+
return { type: "backoff_elapsed", cancelled: false, adapterSessionId };
|
|
3798
|
+
}
|
|
3799
|
+
function isKickoffAction(action) {
|
|
3800
|
+
return action.type === "start_turn" || action.type === "wait";
|
|
3801
|
+
}
|
|
3802
|
+
function runSideEffects(actions) {
|
|
3803
|
+
let kickoff = null;
|
|
3804
|
+
for (const action of actions) {
|
|
3805
|
+
if (isKickoffAction(action)) {
|
|
3806
|
+
kickoff = action;
|
|
3807
|
+
continue;
|
|
3808
|
+
}
|
|
3809
|
+
switch (action.type) {
|
|
3810
|
+
case "persist":
|
|
3811
|
+
persist();
|
|
3812
|
+
if (journalAbsPath && input.projectTasks) {
|
|
3813
|
+
try {
|
|
3814
|
+
const tasks = projectJournalTasks(journalAbsPath);
|
|
3815
|
+
if (tasks) input.projectTasks(tasks);
|
|
3816
|
+
} catch {
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
break;
|
|
3820
|
+
case "warn":
|
|
3821
|
+
input.onWarning?.(action.message);
|
|
3822
|
+
break;
|
|
3823
|
+
case "notify_iteration_complete":
|
|
3824
|
+
input.onIterationComplete?.(snapshot(), cumulativeTokens);
|
|
3825
|
+
break;
|
|
3826
|
+
case "notify_handover_completed":
|
|
3827
|
+
input.onHandoverCompleted?.({
|
|
3828
|
+
...action.completion,
|
|
3829
|
+
tokens: cumulativeTokens
|
|
3830
|
+
});
|
|
3831
|
+
break;
|
|
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
|
+
}
|
|
3857
|
+
break;
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
return kickoff;
|
|
3861
|
+
}
|
|
3862
|
+
async function runActions(actions) {
|
|
3863
|
+
const kickoff = runSideEffects(actions);
|
|
3864
|
+
if (!kickoff) return null;
|
|
3865
|
+
switch (kickoff.type) {
|
|
3866
|
+
case "start_turn":
|
|
3867
|
+
return performStartTurn(
|
|
3868
|
+
kickoff.prompt,
|
|
3869
|
+
kickoff.continuation,
|
|
3870
|
+
kickoff.configOverride
|
|
3871
|
+
);
|
|
3872
|
+
case "wait":
|
|
3873
|
+
return performWait(kickoff.ms);
|
|
3874
|
+
default:
|
|
3875
|
+
return null;
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
let bootstrapActions = initial.actions;
|
|
3879
|
+
if (phase.kind === "awaiting_attention" && input.resumeRunId && initial.actions.length === 0) {
|
|
3880
|
+
const wokenEvent = {
|
|
3881
|
+
type: "woken",
|
|
3882
|
+
continuation: input.initialContinuation ?? { mode: "fresh" },
|
|
3883
|
+
// Prefer a repaired current-run checkpoint when waking.
|
|
3884
|
+
checkpoint: journalAbsPath ? checkpointFromJournal(readJournal(journalAbsPath)) : null
|
|
3885
|
+
};
|
|
3886
|
+
const stepResult = step(phase, memory, wokenEvent, cfg);
|
|
3887
|
+
phase = stepResult.phase;
|
|
3888
|
+
memory = stepResult.memory;
|
|
3889
|
+
bootstrapActions = stepResult.actions;
|
|
3890
|
+
}
|
|
3891
|
+
if (isTerminalPhase(phase)) {
|
|
3892
|
+
const terminal = terminalPhaseToStatus(phase);
|
|
3893
|
+
status = terminal.status;
|
|
3894
|
+
stopReason = terminal.stopReason;
|
|
3895
|
+
}
|
|
3896
|
+
let pendingEvent = await runActions(bootstrapActions);
|
|
3897
|
+
while (pendingEvent) {
|
|
3898
|
+
const stepResult = step(phase, memory, pendingEvent, cfg);
|
|
3899
|
+
phase = stepResult.phase;
|
|
3900
|
+
memory = stepResult.memory;
|
|
3901
|
+
if (isTerminalPhase(phase)) {
|
|
3902
|
+
const terminal = terminalPhaseToStatus(phase);
|
|
3903
|
+
status = terminal.status;
|
|
3904
|
+
stopReason = terminal.stopReason;
|
|
3905
|
+
await runActions(stepResult.actions);
|
|
3906
|
+
break;
|
|
3907
|
+
}
|
|
3908
|
+
pendingEvent = await runActions(stepResult.actions);
|
|
3909
|
+
}
|
|
3910
|
+
return {
|
|
3911
|
+
runId,
|
|
3912
|
+
status,
|
|
3913
|
+
iterations: memory.iteration,
|
|
3914
|
+
stopReason,
|
|
3915
|
+
...interruption ? { interruption } : {},
|
|
3916
|
+
tokens: cumulativeTokens
|
|
3917
|
+
};
|
|
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
|
+
});
|
|
3928
|
+
return {
|
|
3929
|
+
runId,
|
|
3930
|
+
result,
|
|
3931
|
+
cancel() {
|
|
3932
|
+
cancelled = true;
|
|
3933
|
+
},
|
|
3934
|
+
kill() {
|
|
3935
|
+
cancelled = true;
|
|
3936
|
+
input.abortCurrentTurn?.();
|
|
3937
|
+
},
|
|
3938
|
+
steer(steer) {
|
|
3939
|
+
if (applySteer) return applySteer(steer);
|
|
3940
|
+
preStartSteers.push(steer);
|
|
3941
|
+
return true;
|
|
3942
|
+
}
|
|
3943
|
+
};
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
// src/infra/plugins/mcpOptions.ts
|
|
3947
|
+
import fs13 from "fs";
|
|
3948
|
+
import path11 from "path";
|
|
3949
|
+
function collectMcpServersWithOptions(pluginDirs) {
|
|
3950
|
+
const result = [];
|
|
3951
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3952
|
+
for (const dir of pluginDirs) {
|
|
3953
|
+
const mcpPath = path11.join(dir, ".mcp.json");
|
|
3954
|
+
if (!fs13.existsSync(mcpPath)) {
|
|
3955
|
+
continue;
|
|
3956
|
+
}
|
|
3957
|
+
const config = JSON.parse(fs13.readFileSync(mcpPath, "utf-8"));
|
|
3958
|
+
for (const [serverName, serverConfig] of Object.entries(
|
|
3959
|
+
config.mcpServers ?? {}
|
|
3960
|
+
)) {
|
|
3961
|
+
if (seen.has(serverName) || !Array.isArray(serverConfig.options) || serverConfig.options.length === 0) {
|
|
3962
|
+
continue;
|
|
3963
|
+
}
|
|
3964
|
+
seen.add(serverName);
|
|
3965
|
+
result.push({ serverName, options: serverConfig.options });
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
return result;
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
export {
|
|
3972
|
+
DEFAULT_MAX_TURN_TOKEN_COUNT,
|
|
3973
|
+
DEFAULT_PERMISSION_GRACE_MS,
|
|
3974
|
+
createSteerQueue,
|
|
3975
|
+
deserializeRunMemory,
|
|
3976
|
+
wakesFreshAfterHandover,
|
|
3977
|
+
createWorkflowRunner,
|
|
3978
|
+
isMarketplaceRef,
|
|
3979
|
+
findMarketplaceRepoDir,
|
|
3980
|
+
resolveWorkflowMarketplaceSource,
|
|
3981
|
+
listMarketplaceWorkflows,
|
|
3982
|
+
listMarketplaceWorkflowsFromRepo,
|
|
3983
|
+
resolveMarketplaceWorkflow,
|
|
3984
|
+
gatherMarketplaceWorkflowSources,
|
|
3985
|
+
resolveWorkflowInstall,
|
|
3986
|
+
pullMarketplaceRepo,
|
|
3987
|
+
resolveMarketplacePlugin,
|
|
3988
|
+
projectConfigPath,
|
|
3989
|
+
readConfig,
|
|
3990
|
+
resolveActiveWorkflow,
|
|
3991
|
+
readGlobalConfig,
|
|
3992
|
+
writeGlobalConfig,
|
|
3993
|
+
writeProjectConfig,
|
|
3994
|
+
hasProjectWorkflow,
|
|
3995
|
+
installWorkflowPlugins,
|
|
3996
|
+
resolveWorkflowPlugins,
|
|
3997
|
+
listBuiltinWorkflows,
|
|
3998
|
+
readWorkflowSourceMetadata,
|
|
3999
|
+
workflowRegistryDir,
|
|
4000
|
+
resolveWorkflow,
|
|
4001
|
+
installWorkflowFromSource,
|
|
4002
|
+
updateWorkflow,
|
|
4003
|
+
updateWorkflows,
|
|
4004
|
+
listWorkflows,
|
|
4005
|
+
removeWorkflow,
|
|
4006
|
+
compileWorkflowPlan,
|
|
4007
|
+
collectMcpServersWithOptions
|
|
4008
|
+
};
|
|
4009
|
+
//# sourceMappingURL=chunk-QVRTU2MM.js.map
|