@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.
@@ -1,2229 +0,0 @@
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
- function projectConfigPath(projectDir) {
888
- return path4.join(projectDir, ".athena", "config.json");
889
- }
890
- function readConfig(projectDir) {
891
- return readConfigFile(projectConfigPath(projectDir), projectDir);
892
- }
893
- function resolveActiveWorkflow(input) {
894
- const { globalConfig, projectConfig, override } = input;
895
- if (override !== void 0) {
896
- return {
897
- name: override,
898
- source: "override",
899
- selectionsLayer: projectConfig.activeWorkflow !== void 0 ? projectConfig : globalConfig
900
- };
901
- }
902
- if (projectConfig.activeWorkflow !== void 0) {
903
- return {
904
- name: projectConfig.activeWorkflow,
905
- source: "project",
906
- selectionsLayer: projectConfig
907
- };
908
- }
909
- if (globalConfig.activeWorkflow !== void 0) {
910
- return {
911
- name: globalConfig.activeWorkflow,
912
- source: "global",
913
- selectionsLayer: globalConfig
914
- };
915
- }
916
- return {
917
- name: "default",
918
- source: "default",
919
- selectionsLayer: globalConfig
920
- };
921
- }
922
- function readGlobalConfig() {
923
- const homeDir = os3.homedir();
924
- const configPath = path4.join(homeDir, ".config", "athena", "config.json");
925
- return readConfigFile(configPath, homeDir);
926
- }
927
- function readConfigFile(configPath, baseDir) {
928
- if (!fs5.existsSync(configPath)) {
929
- return EMPTY_CONFIG;
930
- }
931
- const raw = JSON.parse(fs5.readFileSync(configPath, "utf-8"));
932
- if ("workflowMarketplaceSource" in raw) {
933
- throw new Error(
934
- `Invalid config: "${configPath}" uses deprecated "workflowMarketplaceSource"; use "workflowMarketplaceSources"`
935
- );
936
- }
937
- if (raw.workflowMarketplaceSources !== void 0 && (!Array.isArray(raw.workflowMarketplaceSources) || !raw.workflowMarketplaceSources.every(
938
- (source) => typeof source === "string"
939
- ))) {
940
- throw new Error(
941
- `Invalid config: "${configPath}" field "workflowMarketplaceSources" must be an array of strings`
942
- );
943
- }
944
- if (raw.harness !== void 0 && raw.harness !== "claude-code" && raw.harness !== "openai-codex" && raw.harness !== "opencode") {
945
- throw new Error(
946
- `Invalid config: "${configPath}" field "harness" must be one of claude-code, openai-codex, opencode`
947
- );
948
- }
949
- if (raw.channels !== void 0 && (!Array.isArray(raw.channels) || !raw.channels.every((c) => typeof c === "string"))) {
950
- throw new Error(
951
- `Invalid config: "${configPath}" field "channels" must be an array of strings`
952
- );
953
- }
954
- const plugins = (raw.plugins ?? []).map((p) => {
955
- if (isMarketplaceRef(p)) return p;
956
- return path4.isAbsolute(p) ? p : path4.resolve(baseDir, p);
957
- });
958
- const additionalDirectories = (raw.additionalDirectories ?? []).map(
959
- (dir) => path4.isAbsolute(dir) ? dir : path4.resolve(baseDir, dir)
960
- );
961
- return {
962
- plugins,
963
- additionalDirectories,
964
- model: raw.model,
965
- theme: raw.theme,
966
- activeWorkflow: raw.activeWorkflow,
967
- workflowMarketplaceSources: raw.workflowMarketplaceSources,
968
- workflowSelections: raw.workflowSelections,
969
- setupComplete: raw.setupComplete,
970
- harness: raw.harness,
971
- telemetry: raw.telemetry,
972
- telemetryDiagnostics: raw.telemetryDiagnostics,
973
- deviceId: raw.deviceId,
974
- channels: raw.channels,
975
- // Personal capabilities are stored opaque. In particular skill `path`
976
- // is NOT relative-resolved against baseDir (R1) — Issue 3 resolves it
977
- // to an absolute path at install time.
978
- mcpServers: raw.mcpServers,
979
- skills: raw.skills
980
- };
981
- }
982
- function readExistingConfig(configPath) {
983
- try {
984
- return JSON.parse(fs5.readFileSync(configPath, "utf-8"));
985
- } catch {
986
- return {};
987
- }
988
- }
989
- function writeConfigFile(configDir, configPath, updates, deleteKeys) {
990
- const existing = readExistingConfig(configPath);
991
- const merged = { ...existing, ...updates };
992
- if (updates.workflowSelections) {
993
- const existingSelections = existing["workflowSelections"] ?? {};
994
- merged["workflowSelections"] = {
995
- ...existingSelections,
996
- ...updates.workflowSelections
997
- };
998
- }
999
- if (deleteKeys) {
1000
- for (const key of deleteKeys) {
1001
- delete merged[key];
1002
- }
1003
- }
1004
- fs5.mkdirSync(configDir, { recursive: true });
1005
- fs5.writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
1006
- }
1007
- var GLOBAL_CONFIG_LEGACY_KEYS = [
1008
- "workflow",
1009
- "mcpServerOptions",
1010
- "workflowMarketplaceSource"
1011
- ];
1012
- function writeGlobalConfig(updates) {
1013
- const homeDir = os3.homedir();
1014
- const configDir = path4.join(homeDir, ".config", "athena");
1015
- const configPath = path4.join(configDir, "config.json");
1016
- writeConfigFile(configDir, configPath, updates, GLOBAL_CONFIG_LEGACY_KEYS);
1017
- }
1018
- function writeProjectConfig(projectDir, updates) {
1019
- const configDir = path4.join(projectDir, ".athena");
1020
- const configPath = path4.join(configDir, "config.json");
1021
- writeConfigFile(configDir, configPath, updates);
1022
- }
1023
- function hasActiveWorkflow(configPath) {
1024
- try {
1025
- const raw = JSON.parse(fs5.readFileSync(configPath, "utf-8"));
1026
- return typeof raw.activeWorkflow === "string" && raw.activeWorkflow !== "";
1027
- } catch {
1028
- return false;
1029
- }
1030
- }
1031
- function hasProjectWorkflow(projectDir) {
1032
- return hasActiveWorkflow(projectConfigPath(projectDir));
1033
- }
1034
-
1035
- // src/core/workflows/types.ts
1036
- function pluginSpecRef(spec) {
1037
- return typeof spec === "string" ? spec : spec.ref;
1038
- }
1039
- function pluginSpecVersion(spec) {
1040
- return typeof spec === "string" ? void 0 : spec.version;
1041
- }
1042
-
1043
- // src/core/workflows/installer.ts
1044
- function installWorkflowPlugins(workflow) {
1045
- return resolveWorkflowPlugins(workflow).resolvedPlugins.map(
1046
- (plugin) => plugin.claudeArtifactDir
1047
- );
1048
- }
1049
- function marketplaceNameFromRef(ref) {
1050
- return ref.slice(ref.indexOf("@") + 1);
1051
- }
1052
- function toResolvedWorkflowPlugin(target, ref, version) {
1053
- return {
1054
- ref: target.ref,
1055
- pluginName: target.pluginName,
1056
- marketplaceName: marketplaceNameFromRef(ref),
1057
- ...version !== void 0 && { version },
1058
- pluginDir: target.pluginDir,
1059
- claudeArtifactDir: target.pluginDir,
1060
- codexPluginDir: target.codexPluginDir,
1061
- codexMarketplacePath: target.marketplacePath
1062
- };
1063
- }
1064
- function resolveWorkflowPlugins(workflow) {
1065
- const resolvedPlugins = workflow.plugins.map((spec) => {
1066
- const ref = pluginSpecRef(spec);
1067
- const version = pluginSpecVersion(spec);
1068
- try {
1069
- const source = "__source" in workflow ? workflow.__source : void 0;
1070
- const repoDir = source?.kind === "marketplace-local" ? source.repoDir : void 0;
1071
- if (version) {
1072
- const target2 = resolveVersionedMarketplacePluginTarget(
1073
- ref,
1074
- version,
1075
- repoDir
1076
- );
1077
- return toResolvedWorkflowPlugin(target2, ref, version);
1078
- }
1079
- if (repoDir) {
1080
- const target2 = resolveMarketplacePluginTargetFromRepo(ref, repoDir);
1081
- return toResolvedWorkflowPlugin(target2, ref);
1082
- }
1083
- const target = resolveMarketplacePluginTarget(ref);
1084
- return toResolvedWorkflowPlugin(target, ref);
1085
- } catch (error) {
1086
- throw new Error(
1087
- `Workflow "${workflow.name}": failed to install plugin "${ref}": ${error.message}`
1088
- );
1089
- }
1090
- });
1091
- return {
1092
- resolvedPlugins,
1093
- localPlugins: resolvedPlugins.map((p) => ({
1094
- ref: p.ref,
1095
- pluginDir: p.claudeArtifactDir
1096
- })),
1097
- codexPlugins: resolvedPlugins.map((p) => ({
1098
- ref: p.ref,
1099
- pluginName: p.pluginName,
1100
- marketplacePath: p.codexMarketplacePath,
1101
- ...p.version !== void 0 && { version: p.version }
1102
- }))
1103
- };
1104
- }
1105
- function refreshPinnedWorkflowPlugins(workflow) {
1106
- const source = "__source" in workflow ? workflow.__source : void 0;
1107
- for (const spec of workflow.plugins) {
1108
- const ref = pluginSpecRef(spec);
1109
- const version = pluginSpecVersion(spec);
1110
- if (!version) {
1111
- continue;
1112
- }
1113
- try {
1114
- refreshVersionedMarketplacePluginTarget(
1115
- ref,
1116
- version,
1117
- source?.kind === "marketplace-local" ? source.repoDir : void 0
1118
- );
1119
- } catch (error) {
1120
- throw new Error(
1121
- `Workflow "${workflow.name}": failed to refresh pinned plugin "${ref}" at version ${version}: ${error.message}`
1122
- );
1123
- }
1124
- }
1125
- }
1126
-
1127
- // src/core/workflows/registry.ts
1128
- import fs9 from "fs";
1129
- import os5 from "os";
1130
- import path7 from "path";
1131
-
1132
- // src/core/workflows/builtins/index.ts
1133
- import fs7 from "fs";
1134
- import os4 from "os";
1135
- import path5 from "path";
1136
-
1137
- // src/core/workflows/trackerReader.ts
1138
- import fs6 from "fs";
1139
-
1140
- // src/core/workflows/templateVars.ts
1141
- function substituteVariables(text, ctx) {
1142
- let result = text;
1143
- if (ctx.input !== void 0) {
1144
- result = result.replaceAll("{input}", ctx.input);
1145
- }
1146
- if (ctx.sessionId !== void 0) {
1147
- result = result.replaceAll("{sessionId}", ctx.sessionId);
1148
- result = result.replaceAll("<session_id>", ctx.sessionId);
1149
- }
1150
- if (ctx.trackerPath !== void 0) {
1151
- result = result.replaceAll("{trackerPath}", ctx.trackerPath);
1152
- }
1153
- return result;
1154
- }
1155
-
1156
- // src/core/workflows/trackerReader.ts
1157
- var DEFAULT_COMPLETION_MARKER = "<!-- WORKFLOW_COMPLETE -->";
1158
- var DEFAULT_BLOCKED_MARKER = "<!-- WORKFLOW_BLOCKED";
1159
- var DEFAULT_TRACKER_PATH = ".athena/{sessionId}/tracker.md";
1160
- var TRACKER_SKELETON_MARKER = "<!-- TRACKER_SKELETON -->";
1161
- var DEFAULT_CONTINUE_PROMPT = "Continue the task. Read the tracker at {trackerPath} for current progress. If the work is complete or blocked, the terminal marker must be the final non-empty line of the tracker; do not write any prose after it.";
1162
- function readTracker(trackerPath) {
1163
- try {
1164
- return fs6.readFileSync(trackerPath, "utf-8");
1165
- } catch {
1166
- return "";
1167
- }
1168
- }
1169
- function getNonEmptyLines(content) {
1170
- return content.trimEnd().split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
1171
- }
1172
- function isBlockedLine(line, blockedMarker) {
1173
- return line === `${blockedMarker} -->` || line.startsWith(`${blockedMarker}:`);
1174
- }
1175
- function isTerminalMarkerLine(line, completionMarker, blockedMarker) {
1176
- return line === completionMarker || isBlockedLine(line, blockedMarker);
1177
- }
1178
- function getMisplacedTerminalMarker(lines, completionMarker, blockedMarker) {
1179
- if (lines.length < 2) return void 0;
1180
- const terminalLine = lines.at(-1);
1181
- if (terminalLine && isTerminalMarkerLine(terminalLine, completionMarker, blockedMarker)) {
1182
- return void 0;
1183
- }
1184
- return lines.slice(0, -1).find((line) => isTerminalMarkerLine(line, completionMarker, blockedMarker));
1185
- }
1186
- function extractBlockedReason(line, blockedMarker) {
1187
- if (!line.startsWith(blockedMarker)) return void 0;
1188
- const afterMarker = line.slice(blockedMarker.length);
1189
- const match = afterMarker.match(/^:\s*(.+?)(?:\s*-->|$)/);
1190
- return match?.[1]?.trim();
1191
- }
1192
- function parseTrackerState(content, markers = {}) {
1193
- const completionMarker = markers.completionMarker ?? DEFAULT_COMPLETION_MARKER;
1194
- const blockedMarker = markers.blockedMarker ?? DEFAULT_BLOCKED_MARKER;
1195
- const lines = getNonEmptyLines(content);
1196
- const terminalLine = lines.at(-1);
1197
- const completed = terminalLine === completionMarker;
1198
- const blocked = terminalLine !== void 0 && isBlockedLine(terminalLine, blockedMarker);
1199
- const blockedReason = blocked && terminalLine ? extractBlockedReason(terminalLine, blockedMarker) : void 0;
1200
- return {
1201
- completed,
1202
- blocked,
1203
- blockedReason,
1204
- misplacedTerminalMarker: getMisplacedTerminalMarker(
1205
- lines,
1206
- completionMarker,
1207
- blockedMarker
1208
- ),
1209
- skeletonNotReplaced: content.includes(TRACKER_SKELETON_MARKER)
1210
- };
1211
- }
1212
- function buildContinuePrompt(loop) {
1213
- const template = loop.continuePrompt ?? DEFAULT_CONTINUE_PROMPT;
1214
- return substituteVariables(template, {
1215
- trackerPath: loop.trackerPath ?? DEFAULT_TRACKER_PATH
1216
- });
1217
- }
1218
-
1219
- // src/core/workflows/builtins/index.ts
1220
- var DEFAULT_BLOCKED_CLOSED_MARKER = `${DEFAULT_BLOCKED_MARKER} -->`;
1221
- var DEFAULT_BLOCKED_REASON_MARKER = `${DEFAULT_BLOCKED_MARKER}: reason -->`;
1222
- var SYSTEM_PROMPT = `You are working on a long-horizon task managed by Athena. A tracker file is used to persist progress across sessions.
1223
-
1224
- ## Tracker File
1225
-
1226
- At the start of each session, read the tracker file if it exists. It contains the task plan, completed steps, and current status from prior sessions.
1227
-
1228
- If no tracker file exists, create one by:
1229
- 1. Analyzing the user's request to understand the full scope
1230
- 2. Breaking the task into concrete, actionable steps
1231
- 3. Writing the plan to the tracker file
1232
-
1233
- ### Tracker Format
1234
-
1235
- Use this markdown format for the tracker:
1236
-
1237
- \`\`\`
1238
- # Task: <one-line summary>
1239
-
1240
- ## Plan
1241
- - [x] Step 1 description
1242
- - [x] Step 2 description
1243
- - [ ] Step 3 description (current)
1244
- - [ ] Step 4 description
1245
-
1246
- ## Current Status
1247
- <brief description of where things stand and what to do next>
1248
-
1249
- ## Notes
1250
- <any important context, decisions, or blockers discovered along the way>
1251
- \`\`\`
1252
-
1253
- ### Updating the Tracker
1254
-
1255
- After completing meaningful work, update the tracker:
1256
- - Check off completed steps
1257
- - Update the current status section
1258
- - Add any important notes or decisions
1259
-
1260
- ### Completion
1261
-
1262
- When all steps are complete:
1263
- 1. Update the tracker with all steps checked off
1264
- 2. Put any final summary or outcome notes above the terminal marker
1265
- 3. Add \`${DEFAULT_COMPLETION_MARKER}\` as the final non-empty line of the tracker file
1266
- 4. Do not write any tracker content after the terminal marker
1267
-
1268
- ### Blocked
1269
-
1270
- If you are blocked and cannot make further progress:
1271
- 1. Document what is blocking you in the Notes section
1272
- 2. Explain what needs to happen to unblock the task whenever possible above the terminal marker
1273
- 3. Add \`${DEFAULT_BLOCKED_CLOSED_MARKER}\` or \`${DEFAULT_BLOCKED_REASON_MARKER}\` as the final non-empty line of the tracker file
1274
- 4. Do not write any tracker content after the terminal marker
1275
- `;
1276
- function ensureSystemPromptFile() {
1277
- const dir = path5.join(
1278
- os4.homedir(),
1279
- ".config",
1280
- "athena",
1281
- "builtins",
1282
- "default"
1283
- );
1284
- const filePath = path5.join(dir, "system_prompt.md");
1285
- if (!fs7.existsSync(filePath) || fs7.readFileSync(filePath, "utf-8") !== SYSTEM_PROMPT) {
1286
- fs7.mkdirSync(dir, { recursive: true });
1287
- fs7.writeFileSync(filePath, SYSTEM_PROMPT, "utf-8");
1288
- }
1289
- return filePath;
1290
- }
1291
- function resolveBuiltinWorkflow(name) {
1292
- if (name !== "default") {
1293
- return void 0;
1294
- }
1295
- return {
1296
- name: "default",
1297
- description: "General-purpose workflow for long-horizon tasks \u2014 breaks work into steps, tracks progress across sessions, and loops until complete",
1298
- promptTemplate: "{input}",
1299
- loop: {
1300
- enabled: true,
1301
- completionMarker: DEFAULT_COMPLETION_MARKER,
1302
- blockedMarker: DEFAULT_BLOCKED_MARKER,
1303
- maxIterations: 20
1304
- },
1305
- plugins: [],
1306
- workflowFile: ensureSystemPromptFile()
1307
- };
1308
- }
1309
- function listBuiltinWorkflows() {
1310
- return ["default"];
1311
- }
1312
-
1313
- // src/core/workflows/sourceMetadata.ts
1314
- import fs8 from "fs";
1315
- import path6 from "path";
1316
- function legacyLocalToNew(legacyPath, legacyRepoDir) {
1317
- const repoDir = legacyRepoDir ?? findMarketplaceRepoDir(legacyPath);
1318
- if (!repoDir) {
1319
- return { kind: "filesystem", path: legacyPath };
1320
- }
1321
- try {
1322
- const canonicalRepoDir = fs8.realpathSync(repoDir);
1323
- const listings = listMarketplaceWorkflowsFromRepo(canonicalRepoDir);
1324
- const absolutePath = fs8.realpathSync(legacyPath);
1325
- const match = listings.find(
1326
- (l) => fs8.realpathSync(l.workflowPath) === absolutePath
1327
- );
1328
- if (match) {
1329
- return {
1330
- kind: "marketplace-local",
1331
- repoDir: canonicalRepoDir,
1332
- workflowName: match.name,
1333
- ...match.version ? { version: match.version } : {}
1334
- };
1335
- }
1336
- } catch {
1337
- }
1338
- return { kind: "filesystem", path: legacyPath };
1339
- }
1340
- function readWorkflowSourceMetadata(workflowDir) {
1341
- const sourceFile = path6.join(workflowDir, "source.json");
1342
- if (!fs8.existsSync(sourceFile)) return void 0;
1343
- let raw;
1344
- try {
1345
- raw = JSON.parse(fs8.readFileSync(sourceFile, "utf-8"));
1346
- } catch {
1347
- throw new Error(`Invalid source.json: ${sourceFile} is not valid JSON`);
1348
- }
1349
- if (!raw || typeof raw !== "object") {
1350
- throw new Error(
1351
- `Invalid source.json: ${sourceFile} must contain an object`
1352
- );
1353
- }
1354
- const r = raw;
1355
- if (r["v"] === 2) {
1356
- if (r["kind"] === "marketplace-remote" && typeof r["ref"] === "string") {
1357
- return {
1358
- kind: "marketplace-remote",
1359
- ref: r["ref"],
1360
- ...typeof r["version"] === "string" ? { version: r["version"] } : {}
1361
- };
1362
- }
1363
- if (r["kind"] === "marketplace-local" && typeof r["repoDir"] === "string" && typeof r["workflowName"] === "string") {
1364
- return {
1365
- kind: "marketplace-local",
1366
- repoDir: r["repoDir"],
1367
- workflowName: r["workflowName"],
1368
- ...typeof r["version"] === "string" ? { version: r["version"] } : {}
1369
- };
1370
- }
1371
- if (r["kind"] === "filesystem" && typeof r["path"] === "string") {
1372
- return { kind: "filesystem", path: r["path"] };
1373
- }
1374
- }
1375
- if (r["kind"] === "marketplace" && typeof r["ref"] === "string") {
1376
- return { kind: "marketplace-remote", ref: r["ref"] };
1377
- }
1378
- if (r["kind"] === "local" && typeof r["path"] === "string") {
1379
- return legacyLocalToNew(
1380
- r["path"],
1381
- typeof r["repoDir"] === "string" ? r["repoDir"] : void 0
1382
- );
1383
- }
1384
- throw new Error(`Invalid source.json: ${sourceFile} kind is not supported`);
1385
- }
1386
- function writeWorkflowSourceMetadata(workflowDir, metadata) {
1387
- fs8.mkdirSync(workflowDir, { recursive: true });
1388
- fs8.writeFileSync(
1389
- path6.join(workflowDir, "source.json"),
1390
- JSON.stringify({ v: 2, ...metadata }),
1391
- "utf-8"
1392
- );
1393
- }
1394
-
1395
- // src/core/workflows/registry.ts
1396
- function registryDir() {
1397
- return path7.join(os5.homedir(), ".config", "athena", "workflows");
1398
- }
1399
- function ensurePathWithinRoot(rootDir, targetPath, label) {
1400
- const relative = path7.relative(rootDir, targetPath);
1401
- if (relative === "" || !relative.startsWith("..") && !path7.isAbsolute(relative)) {
1402
- return;
1403
- }
1404
- throw new Error(`${label} resolves outside the workflow root: ${targetPath}`);
1405
- }
1406
- function resolveWorkflow(name) {
1407
- const workflowDir = path7.join(registryDir(), name);
1408
- const workflowPath = path7.join(workflowDir, "workflow.json");
1409
- if (!fs9.existsSync(workflowPath)) {
1410
- const builtin = resolveBuiltinWorkflow(name);
1411
- if (builtin) {
1412
- return builtin;
1413
- }
1414
- throw new Error(
1415
- `Workflow "${name}" not found. Install with: athena workflow install <source> --name ${name}`
1416
- );
1417
- }
1418
- const source = readWorkflowSourceMetadata(workflowDir);
1419
- const raw = JSON.parse(fs9.readFileSync(workflowPath, "utf-8"));
1420
- if (!Array.isArray(raw["plugins"])) {
1421
- throw new Error(
1422
- `Invalid workflow.json: "plugins" must be an array (got ${typeof raw["plugins"]})`
1423
- );
1424
- }
1425
- for (const entry of raw["plugins"]) {
1426
- if (typeof entry === "string") {
1427
- if (isMarketplaceRef(entry.trim())) {
1428
- continue;
1429
- }
1430
- throw new Error(
1431
- `Invalid workflow.json: each plugin must be a valid marketplace ref string or {ref, version} object`
1432
- );
1433
- }
1434
- if (typeof entry === "object" && entry !== null) {
1435
- const r = entry["ref"];
1436
- const v = entry["version"];
1437
- if (typeof r === "string" && isMarketplaceRef(r.trim()) && r.trim().length > 0 && typeof v === "string" && v.trim().length > 0) {
1438
- continue;
1439
- }
1440
- }
1441
- throw new Error(
1442
- `Invalid workflow.json: each plugin must be a valid marketplace ref string or {ref, version} object`
1443
- );
1444
- }
1445
- if (typeof raw["promptTemplate"] !== "string") {
1446
- throw new Error(`Invalid workflow.json: "promptTemplate" must be a string`);
1447
- }
1448
- if (raw["examplePrompts"] !== void 0 && (!Array.isArray(raw["examplePrompts"]) || !raw["examplePrompts"].every((e) => typeof e === "string"))) {
1449
- throw new Error(
1450
- `Invalid workflow.json: "examplePrompts" must be an array of strings`
1451
- );
1452
- }
1453
- if (typeof raw["workflowFile"] !== "string" || raw["workflowFile"].length === 0) {
1454
- throw new Error(
1455
- 'Invalid workflow.json: "workflowFile" is required and must point to the workflow instructions file'
1456
- );
1457
- }
1458
- const workflowFile = raw["workflowFile"];
1459
- if (typeof workflowFile === "string" && !path7.isAbsolute(workflowFile)) {
1460
- const workflowFilePath = path7.resolve(workflowDir, workflowFile);
1461
- if (!fs9.existsSync(workflowFilePath)) {
1462
- throw new Error(
1463
- `Invalid workflow.json: workflowFile "${workflowFile}" not found at ${workflowFilePath}`
1464
- );
1465
- }
1466
- raw["workflowFile"] = workflowFilePath;
1467
- } else if (typeof workflowFile === "string" && !fs9.existsSync(workflowFile)) {
1468
- throw new Error(
1469
- `Invalid workflow.json: workflowFile "${workflowFile}" not found`
1470
- );
1471
- }
1472
- return {
1473
- ...raw,
1474
- ...source ? { __source: source } : {}
1475
- };
1476
- }
1477
- function readWorkflowSource(sourcePath) {
1478
- const content = fs9.readFileSync(sourcePath, "utf-8");
1479
- return { content, workflow: JSON.parse(content) };
1480
- }
1481
- function copyWorkflowFiles(sourcePath, destDir) {
1482
- const { content, workflow } = readWorkflowSource(sourcePath);
1483
- const absoluteSourcePath = path7.resolve(sourcePath);
1484
- const sourceDir = path7.dirname(absoluteSourcePath);
1485
- const absoluteDestDir = path7.resolve(destDir);
1486
- fs9.mkdirSync(absoluteDestDir, { recursive: true });
1487
- fs9.writeFileSync(
1488
- path7.join(absoluteDestDir, "workflow.json"),
1489
- content,
1490
- "utf-8"
1491
- );
1492
- const copyRelativeAsset = (assetPath) => {
1493
- if (!assetPath || path7.isAbsolute(assetPath)) return;
1494
- const sourceAssetPath = path7.resolve(sourceDir, assetPath);
1495
- ensurePathWithinRoot(sourceDir, sourceAssetPath, "Workflow asset");
1496
- if (!fs9.existsSync(sourceAssetPath)) return;
1497
- const destAssetPath = path7.resolve(absoluteDestDir, assetPath);
1498
- ensurePathWithinRoot(absoluteDestDir, destAssetPath, "Workflow asset");
1499
- fs9.mkdirSync(path7.dirname(destAssetPath), { recursive: true });
1500
- fs9.copyFileSync(sourceAssetPath, destAssetPath);
1501
- };
1502
- const rawWorkflow = workflow;
1503
- const promptAsset = rawWorkflow["workflowFile"];
1504
- if (typeof promptAsset !== "string" || promptAsset.length === 0) {
1505
- throw new Error(
1506
- 'Invalid workflow.json: "workflowFile" is required and must point to the workflow instructions file'
1507
- );
1508
- }
1509
- copyRelativeAsset(promptAsset);
1510
- }
1511
- function toStoredMetadata(source) {
1512
- if (source.kind === "marketplace-remote") {
1513
- return {
1514
- kind: "marketplace-remote",
1515
- ref: source.ref,
1516
- ...source.version ? { version: source.version } : {}
1517
- };
1518
- }
1519
- if (source.kind === "marketplace-local") {
1520
- return {
1521
- kind: "marketplace-local",
1522
- repoDir: source.repoDir,
1523
- workflowName: source.workflowName,
1524
- ...source.version ? { version: source.version } : {}
1525
- };
1526
- }
1527
- return { kind: "filesystem", path: source.workflowPath };
1528
- }
1529
- function installWorkflowFromSource(source, name) {
1530
- const { workflow } = readWorkflowSource(source.workflowPath);
1531
- const workflowName = name ?? workflow.name;
1532
- if (!workflowName) {
1533
- throw new Error(
1534
- 'Workflow has no "name" field. Provide --name to specify one.'
1535
- );
1536
- }
1537
- const destDir = path7.join(registryDir(), workflowName);
1538
- copyWorkflowFiles(source.workflowPath, destDir);
1539
- const metadata = toStoredMetadata(source);
1540
- writeWorkflowSourceMetadata(destDir, metadata);
1541
- return workflowName;
1542
- }
1543
- function reResolveFromMetadata(metadata, refresh = refreshMarketplaceRepo, onSelfHeal) {
1544
- if (metadata.kind === "marketplace-remote") {
1545
- const slug = metadata.ref.slice(metadata.ref.indexOf("@") + 1);
1546
- const slashIdx = slug.indexOf("/");
1547
- const owner = slug.slice(0, slashIdx);
1548
- const repo = slug.slice(slashIdx + 1);
1549
- const refreshed = refresh(owner, repo);
1550
- if (!refreshed.ok) {
1551
- throw new MarketplaceRefreshError(refreshed);
1552
- }
1553
- if (refreshed.selfHealed) {
1554
- onSelfHeal?.({
1555
- marketplace: `${owner}/${repo}`,
1556
- backupDir: refreshed.backupDir
1557
- });
1558
- }
1559
- return resolveWorkflowInstall(metadata.ref, []);
1560
- }
1561
- if (metadata.kind === "marketplace-local") {
1562
- const manifestPath = resolveWorkflowManifestPath(metadata.repoDir);
1563
- const listings = listMarketplaceWorkflowsFromRepo(metadata.repoDir);
1564
- const entry = listings.find((l) => l.name === metadata.workflowName);
1565
- if (!entry) {
1566
- throw new Error(
1567
- `Workflow "${metadata.workflowName}" is no longer in the local marketplace at ${metadata.repoDir}.`
1568
- );
1569
- }
1570
- return {
1571
- kind: "marketplace-local",
1572
- repoDir: metadata.repoDir,
1573
- workflowName: metadata.workflowName,
1574
- ...entry.version ? { version: entry.version } : {},
1575
- manifestPath,
1576
- workflowPath: entry.workflowPath
1577
- };
1578
- }
1579
- return { kind: "filesystem", workflowPath: metadata.path };
1580
- }
1581
- function updateWorkflow(name, refresh = refreshMarketplaceRepo, onSelfHeal) {
1582
- const workflowDir = path7.join(registryDir(), name);
1583
- const metadata = readWorkflowSourceMetadata(workflowDir);
1584
- if (!metadata) {
1585
- throw new Error(
1586
- `Workflow "${name}" has no recorded source. Reinstall it with: athena-flow workflow install <source>`
1587
- );
1588
- }
1589
- const source = reResolveFromMetadata(metadata, refresh, onSelfHeal);
1590
- const installedName = installWorkflowFromSource(source, name);
1591
- refreshPinnedWorkflowPlugins(resolveWorkflow(installedName));
1592
- return installedName;
1593
- }
1594
- function updateWorkflows(names) {
1595
- const refreshCache = /* @__PURE__ */ new Map();
1596
- const refresh = (owner, repo) => {
1597
- const key = `${owner}/${repo}`;
1598
- const cached = refreshCache.get(key);
1599
- if (cached) return cached;
1600
- const outcome = refreshMarketplaceRepo(owner, repo);
1601
- refreshCache.set(key, outcome);
1602
- return outcome;
1603
- };
1604
- const upgraded = [];
1605
- const marketplaceFailures = [];
1606
- const reportedMarketplaces = /* @__PURE__ */ new Set();
1607
- const workflowFailures = [];
1608
- const selfHealed = [];
1609
- const selfHealedMarketplaces = /* @__PURE__ */ new Set();
1610
- const onSelfHeal = (info) => {
1611
- if (!selfHealedMarketplaces.has(info.marketplace)) {
1612
- selfHealedMarketplaces.add(info.marketplace);
1613
- selfHealed.push(info);
1614
- }
1615
- };
1616
- for (const name of names) {
1617
- try {
1618
- upgraded.push(updateWorkflow(name, refresh, onSelfHeal));
1619
- } catch (error) {
1620
- if (error instanceof MarketplaceRefreshError) {
1621
- if (!reportedMarketplaces.has(error.marketplace)) {
1622
- reportedMarketplaces.add(error.marketplace);
1623
- marketplaceFailures.push({ marketplace: error.marketplace, error });
1624
- }
1625
- } else {
1626
- workflowFailures.push({
1627
- name,
1628
- error: error instanceof Error ? error : new Error(String(error))
1629
- });
1630
- }
1631
- }
1632
- }
1633
- return { upgraded, marketplaceFailures, workflowFailures, selfHealed };
1634
- }
1635
- function listWorkflows() {
1636
- const dir = registryDir();
1637
- const installed = fs9.existsSync(dir) ? fs9.readdirSync(dir, { withFileTypes: true }).filter(
1638
- (entry) => entry.isDirectory() && fs9.existsSync(path7.join(dir, entry.name, "workflow.json"))
1639
- ).map((entry) => entry.name) : [];
1640
- const builtins = listBuiltinWorkflows().filter(
1641
- (name) => !installed.includes(name)
1642
- );
1643
- return [...builtins, ...installed];
1644
- }
1645
- function removeWorkflow(name) {
1646
- const dir = path7.join(registryDir(), name);
1647
- if (!fs9.existsSync(dir)) {
1648
- throw new Error(`Workflow "${name}" not found.`);
1649
- }
1650
- fs9.rmSync(dir, { recursive: true, force: true });
1651
- }
1652
-
1653
- // src/core/workflows/applyWorkflow.ts
1654
- function applyPromptTemplate(template, input, ctx) {
1655
- return substituteVariables(template, { input, ...ctx });
1656
- }
1657
-
1658
- // src/core/workflows/plan.ts
1659
- function compileWorkflowPlan(input) {
1660
- if (!input.workflow) {
1661
- return void 0;
1662
- }
1663
- const resolved = !input.resolvedPlugins && (!input.localPlugins || !input.codexPlugins) ? resolveWorkflowPlugins(input.workflow) : void 0;
1664
- const resolvedPlugins = input.resolvedPlugins ?? resolved?.resolvedPlugins ?? [];
1665
- const localPlugins = input.localPlugins ?? resolved?.localPlugins ?? [];
1666
- const codexPlugins = input.codexPlugins ?? resolved?.codexPlugins ?? [];
1667
- return {
1668
- workflow: input.workflow,
1669
- resolvedPlugins: resolvedPlugins.filter(
1670
- (plugin, index, array) => array.findIndex((candidate) => candidate.ref === plugin.ref) === index
1671
- ),
1672
- localPlugins: localPlugins.filter(
1673
- (plugin, index, array) => array.findIndex((candidate) => candidate.ref === plugin.ref) === index
1674
- ),
1675
- agentRoots: resolvedPlugins.map((plugin) => `${plugin.claudeArtifactDir}/agents`).filter((root, index, array) => array.indexOf(root) === index),
1676
- codexPlugins: codexPlugins.filter(
1677
- (target, index, array) => array.findIndex((candidate) => candidate.ref === target.ref) === index
1678
- ),
1679
- pluginMcpConfig: input.pluginMcpConfig
1680
- };
1681
- }
1682
-
1683
- // src/core/workflows/sessionPlan.ts
1684
- import fs10 from "fs";
1685
- import path8 from "path";
1686
-
1687
- // src/core/workflows/stateMachine.md
1688
- var stateMachine_default = "# Stateless Turn Protocol\n\nYou run in a stateless loop. Each Turn is a fresh process with no memory of prior Turns. **The tracker file is your only continuity** \u2014 read it, work, write it. Assume interruption: the runner may kill a long Turn, your context may collapse under tool output, you may hit token limits mid-task. Anything not in the tracker is gone.\n\n## First action, every Turn\n\n1. Read the tracker at the configured path (default: `.athena/<session_id>/tracker.md`). The runner provides the session ID \u2014 do not invent one.\n2. If the tracker contains `<!-- TRACKER_SKELETON -->` \u2192 this is Turn 1, run [**Orient**](#orient-turn-1).\n3. Otherwise \u2192 this is a continuation, run [**Execute**](#execute-turn-2) from where the tracker says, not from the start of the flow.\n\nReading first prevents two failure modes that waste whole Turns: redoing work already done, or contradicting decisions a prior Turn made.\n\n## Tracker contract\n\nThe tracker must always answer four 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?\n\nA future Turn has no other context. If something isn't here, it doesn't exist. Section headings may vary by workflow, but these four answers must be explicit and easy to find.\n\n### Terminal markers\n\nDefault markers (workflows may override \u2014 use the markers configured for the active workflow):\n\n- `<!-- WORKFLOW_COMPLETE -->` \u2014 all work done and verified\n- `<!-- WORKFLOW_BLOCKED -->` or `<!-- WORKFLOW_BLOCKED: reason -->` \u2014 cannot proceed without external intervention\n\nRules:\n\n- Only the last non-empty line of the tracker is authoritative. Marker-like text in notes, examples, or quoted instructions earlier in the file is ignored.\n- When you write a terminal marker, it must be the final non-empty line of the tracker. Put every summary, status note, and next-step sentence before the marker. Never append prose after it.\n- The runner trusts markers unconditionally. A premature marker ends the loop with no automatic recovery \u2014 write one only when its criteria are fully met.\n- Include a concrete reason after `WORKFLOW_BLOCKED:` whenever possible; the bare form is still valid.\n\n## Phases\n\n### Orient (Turn 1)\n\n1. **Replace the skeleton immediately**, before any domain work. Even a three-line tracker (goal + \"orienting\") protects you if the Turn dies during setup.\n2. Identify and load the applicable workflow skills before doing domain work. If a workflow, plugin, or local skill table names a relevant skill, read it fully and follow it. Do not assume you already know the workflow's conventions, tool sequence, quality gates, or implementation details.\n3. Use a dedicated git worktree for repository-changing work. If you are not already inside a task-specific worktree, create or enter one before editing files, record its branch/path in the tracker, and continue there. Skip this only when the workflow explicitly forbids it or the task is read-only.\n4. Run the workflow's orientation steps exactly as written. These vary by domain \u2014 a test-writing workflow explores the product in a browser; a migration workflow audits the schema. The workflow defines what orientation means. Do not skip, reorder, reinterpret, or replace workflow steps with a generic approach unless the workflow explicitly allows it or the tracker records a concrete blocker that makes the written step impossible.\n5. Refine the tracker into a granular plan. Each task a concrete, verifiable unit of work, including verification steps (running checks, reviewing output) \u2014 not just implementation. Vague tasks (\"write tests\") cannot be meaningfully resumed by a future Turn that has no idea what they mean here.\n6. Record concrete observations \u2014 what you actually saw, not what you assumed. Wrong assumptions burn entire future Turns on rework.\n7. **Single-Turn requests still go through this phase.** If the entire request is satisfied in one Turn, write a minimal tracker (what was asked, what was done, the outcome) and append `<!-- WORKFLOW_COMPLETE -->`. Leaving the skeleton in place causes the runner to classify the Turn as a failure.\n\n### Execute (Turn 2+)\n\n- Work from where the tracker says, in the workflow's prescribed sequence. Not every Turn covers every step.\n- Be strict with workflow steps. Before starting each unit, identify the next required workflow step from the workflow document and tracker, follow it as written, and record completion or blockers against that step. Do not substitute your own process, collapse separate gates into one, or advance past an unchecked step.\n- Be strict with skills. Before each new activity, check the workflow, plugin metadata, local skill table, and tracker for relevant skills. Load the appropriate skill first, read it completely, and follow its instructions. If no skill applies, record that explicitly in the tracker before proceeding. Skills carry the implementation detail (scaffolding steps, locator rules, anti-patterns, code templates) that this protocol intentionally doesn't repeat.\n- Keep repository work inside the recorded git worktree. If a continuation Turn 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\n1. Tracker reflects all progress, discoveries, and blockers.\n2. Tracker says clearly what the next Turn should do first.\n3. If all work is verified: append the completion marker as the final non-empty line.\n4. If an unrecoverable blocker prevents progress: append the blocked marker as the final non-empty line, with a reason if you have one.\n\n## When to write the tracker\n\nWrite on **concrete triggers**, not on a vague sense of \"meaningful progress.\" The right cadence sits between every-tool-call (noisy log, wastes tokens) and end-of-Turn (everything lost if you die mid-task).\n\n- **Discrete unit done** \u2014 file written, fix applied, test run, gate passed. Reflect the new reality before starting the next unit.\n- **Insight learned** \u2014 API quirk, config field that turned out to matter, dead end ruled out, decision between two approaches. Insights are tracker-worthy even when no code changed; rediscovering them costs the next Turn a full re-exploration. The tracker is a knowledge ledger, not just a task log.\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- **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 the next Turn needs. Don't transcribe tool calls \u2014 the tracker is a contract with your future self, not a replay log.\n\nThe cost of one extra tracker update is a few tokens. The cost of dying without one is a whole wasted Turn. Bias toward writing.\n\n## Task UI projection\n\nThe tracker is the durable source of truth. Your harness's task tools are a Turn-scoped UI projection of the same plan, shown to the user in their CLI widget. They do not survive process exit.\n\n{{TASK_TOOL_INSTRUCTIONS}}\n\n- **Turn 1, after orientation:** project the tracker's task plan into the task tools.\n- **Turn 2+, after reading the tracker:** recreate the projection from the tracker; do not assume task IDs from prior Turns still exist.\n- **During work:** update both \u2014 the task tools for immediate UI feedback, the tracker for persistence \u2014 in the same working phase.\n\n## Turn bounding\n\nEach fresh Turn starts with a clean context window and a compact tracker \u2014 effectively self-compaction. As you work, context fills with tool outputs and intermediate state. The longer you run, the more attention is spread across tokens that are no longer relevant, degrading precision on the work that matters now.\n\nWork a bounded chunk per Turn. Ending early and letting the next Turn pick up from a clean tracker is almost always better than pushing through with a heavy context. Natural checkpoints:\n\n- After a quality gate\n- After crossing multiple phases (explored \u2192 planned \u2192 wrote specs) \u2014 stop before pushing into the next\n- When your context is visibly heavy with tool output from earlier work\n\n## Quick reference\n\n- [ ] Read the tracker before doing anything else\n- [ ] Replace the skeleton immediately, even for single-Turn requests\n- [ ] Update on concrete triggers \u2014 unit done, insight learned, risky op pending, plan changed\n- [ ] Project the tracker plan into task tools at Turn start; keep both in sync as work lands\n- [ ] Follow the workflow steps as written; do not skip, reorder, or substitute your own process\n- [ ] Load the appropriate skill before each activity; do not rely on assumed knowledge\n- [ ] Use and record a dedicated git worktree for repository-changing work\n- [ ] Run quality gates in order; respect delegation constraints and retry limits\n- [ ] Write the completion marker only when all work is verified, and make it the final non-empty line\n- [ ] Checkpoint and end before context goes stale\n";
1689
-
1690
- // src/core/workflows/stateMachine.ts
1691
- function buildTaskToolInstructions(harness) {
1692
- switch (harness) {
1693
- case "openai-codex":
1694
- return `Use the \`update_plan\` tool to create and maintain the task list shown to the user for the full harness session.
1695
-
1696
- - As soon as orientation yields a credible plan, create a detailed task list from the tracker; each item should be a concrete, verifiable unit of work, not a vague phase
1697
- - At the start of every fresh harness session, recreate the full task list from the tracker before resuming execution
1698
- - Do not carry forward prior session task IDs or assume prior session plan items still exist in the tool
1699
- - 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 tracker
1700
- - Keep task state consistent and non-stale: reconcile the task list against the tracker before moving on, and before ending the session
1701
- - Maintain exactly one \`in_progress\` task unless the tracker explicitly records parallel active work`;
1702
- case "claude-code":
1703
- case "opencode":
1704
- default:
1705
- return `Use \`TaskCreate\` and \`TaskUpdate\` to create and maintain the task list shown to the user for the full harness session.
1706
-
1707
- - As soon as orientation yields a credible plan, create a detailed task list from the tracker; each item should be a concrete, verifiable unit of work, not a vague phase
1708
- - At the start of every fresh harness session, recreate the full task list from the tracker instead of trying to resume prior session task IDs
1709
- - Do not refer to task IDs created in earlier sessions; they are session-scoped UI artifacts, not durable workflow state
1710
- - 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 tracker
1711
- - Keep task state consistent and non-stale: reconcile the task list against the tracker before moving on, and before ending the session
1712
- - Maintain exactly one active task unless the tracker explicitly records parallel active work`;
1713
- }
1714
- }
1715
- function buildStateMachineContent(harness = "claude-code") {
1716
- return stateMachine_default.replace(
1717
- "{{TASK_TOOL_INSTRUCTIONS}}",
1718
- buildTaskToolInstructions(harness)
1719
- );
1720
- }
1721
- var STATE_MACHINE_CONTENT = buildStateMachineContent();
1722
-
1723
- // src/core/workflows/sessionPlan.ts
1724
- function readWorkflowOverride(projectDir, workflow, sessionId, trackerPath, harness = "claude-code") {
1725
- if (!workflow?.workflowFile) {
1726
- return { workflowOverride: void 0, warnings: [] };
1727
- }
1728
- const resolvedPath = path8.isAbsolute(workflow.workflowFile) ? workflow.workflowFile : path8.resolve(projectDir, workflow.workflowFile);
1729
- let workflowContent;
1730
- try {
1731
- workflowContent = fs10.readFileSync(resolvedPath, "utf-8");
1732
- } catch {
1733
- return {
1734
- workflowOverride: void 0,
1735
- warnings: [
1736
- `Workflow file not found: ${workflow.workflowFile}. Continuing without workflow system instructions.`
1737
- ]
1738
- };
1739
- }
1740
- let composed = workflow.loop?.enabled ? buildStateMachineContent(harness) + "\n\n" + workflowContent : workflowContent;
1741
- composed = substituteVariables(composed, {
1742
- sessionId,
1743
- trackerPath: trackerPath ?? void 0
1744
- });
1745
- const workflowDir = path8.dirname(resolvedPath);
1746
- const composedPath = path8.join(workflowDir, ".composed-system-prompt.md");
1747
- fs10.writeFileSync(composedPath, composed, "utf-8");
1748
- return {
1749
- workflowOverride: {
1750
- appendSystemPromptFile: composedPath,
1751
- developerInstructions: composed
1752
- },
1753
- warnings: []
1754
- };
1755
- }
1756
- function mergeOverrides(base, workflowOverride) {
1757
- if (!base) return workflowOverride;
1758
- if (!workflowOverride) return base;
1759
- return {
1760
- ...base,
1761
- ...workflowOverride
1762
- };
1763
- }
1764
- function resolveTrackerPath(input) {
1765
- const loop = input.workflow?.loop;
1766
- if (!loop?.enabled) {
1767
- return null;
1768
- }
1769
- const rawPath = loop.trackerPath ?? DEFAULT_TRACKER_PATH;
1770
- if (!input.sessionId && rawPath.includes("{sessionId}")) {
1771
- return null;
1772
- }
1773
- const promptPath = input.sessionId ? rawPath.replaceAll("{sessionId}", input.sessionId) : rawPath;
1774
- const absolutePath = path8.isAbsolute(promptPath) ? promptPath : path8.resolve(input.projectDir, promptPath);
1775
- return {
1776
- absolutePath,
1777
- promptPath
1778
- };
1779
- }
1780
- function createWorkflowRunState(input) {
1781
- const { projectDir, sessionId, workflow, harness } = input;
1782
- const trackerResolved = resolveTrackerPath({ projectDir, sessionId, workflow });
1783
- const { workflowOverride, warnings } = readWorkflowOverride(
1784
- projectDir,
1785
- workflow,
1786
- sessionId,
1787
- trackerResolved?.promptPath,
1788
- harness
1789
- );
1790
- return {
1791
- workflow,
1792
- trackerPathForPrompt: trackerResolved?.promptPath,
1793
- workflowOverride,
1794
- warnings
1795
- };
1796
- }
1797
- function prepareWorkflowTurn(state, input) {
1798
- const { workflow } = state;
1799
- const iteration = input.iteration ?? 1;
1800
- const isContinuation = workflow?.loop?.enabled === true && iteration > 1;
1801
- const prompt = isContinuation ? buildContinuePrompt({
1802
- ...workflow.loop,
1803
- trackerPath: state.trackerPathForPrompt ?? workflow.loop?.trackerPath
1804
- }) : workflow ? applyPromptTemplate(workflow.promptTemplate, input.prompt) : input.prompt;
1805
- return {
1806
- prompt,
1807
- configOverride: mergeOverrides(
1808
- input.configOverride,
1809
- state.workflowOverride
1810
- ),
1811
- warnings: state.warnings
1812
- };
1813
- }
1814
-
1815
- // src/core/workflows/useWorkflowSessionController.ts
1816
- import { useCallback, useEffect, useRef, useState } from "react";
1817
-
1818
- // src/core/workflows/workflowRunner.ts
1819
- import crypto from "crypto";
1820
- import fs12 from "fs";
1821
- import path9 from "path";
1822
-
1823
- // src/core/workflows/terminalOutcome.ts
1824
- import fs11 from "fs";
1825
- var MISSING_TRACKER_MESSAGE = "the tracker file went missing during the run \u2014 the workflow can no longer verify progress";
1826
- var SKELETON_NOT_REPLACED_MESSAGE = "tracker skeleton was never replaced \u2014 Claude did not bootstrap the tracker";
1827
- var MISPLACED_TERMINAL_MARKER_MESSAGE = "terminal workflow marker is not the final non-empty line of the tracker; move all summary text above the marker";
1828
- function resolveTurnOutcome(input) {
1829
- const { trackerPath, loop, iteration } = input;
1830
- if (!fs11.existsSync(trackerPath)) {
1831
- return {
1832
- kind: "stop",
1833
- status: "failed",
1834
- stopReason: MISSING_TRACKER_MESSAGE
1835
- };
1836
- }
1837
- const tracker = parseTrackerState(readTracker(trackerPath), loop);
1838
- if (tracker.skeletonNotReplaced) {
1839
- return {
1840
- kind: "stop",
1841
- status: "failed",
1842
- stopReason: SKELETON_NOT_REPLACED_MESSAGE
1843
- };
1844
- }
1845
- if (tracker.misplacedTerminalMarker) {
1846
- return {
1847
- kind: "stop",
1848
- status: "failed",
1849
- stopReason: MISPLACED_TERMINAL_MARKER_MESSAGE
1850
- };
1851
- }
1852
- if (tracker.completed) {
1853
- return { kind: "stop", status: "completed" };
1854
- }
1855
- if (tracker.blocked) {
1856
- return { kind: "stop", status: "blocked", stopReason: tracker.blockedReason };
1857
- }
1858
- if (iteration >= loop.maxIterations) {
1859
- return { kind: "stop", status: "exhausted" };
1860
- }
1861
- return { kind: "continue" };
1862
- }
1863
-
1864
- // src/core/workflows/workflowRunner.ts
1865
- var NULL_TOKENS = {
1866
- input: null,
1867
- output: null,
1868
- cacheRead: null,
1869
- cacheWrite: null,
1870
- total: null,
1871
- contextSize: null,
1872
- contextWindowSize: null
1873
- };
1874
- var TRACKER_SKELETON_TEMPLATE = `${TRACKER_SKELETON_MARKER}
1875
- # Workflow Tracker
1876
-
1877
- **Session**: {sessionId}
1878
- **Tracker**: {trackerPath}
1879
- **Goal**: {input}
1880
-
1881
- ---
1882
-
1883
- > This tracker was created by the runner. Update it as you work.
1884
- > See the Stateless Turn Protocol for tracker conventions.
1885
-
1886
- ## Status
1887
-
1888
- Orientation in progress.
1889
-
1890
- ## Plan
1891
-
1892
- _To be created during orientation._
1893
-
1894
- ## Progress
1895
-
1896
- _No progress yet._
1897
- `;
1898
- function mergeTokens(base, next) {
1899
- const input = (base.input ?? 0) + (next.input ?? 0);
1900
- const output = (base.output ?? 0) + (next.output ?? 0);
1901
- const cacheRead = (base.cacheRead ?? 0) + (next.cacheRead ?? 0);
1902
- const cacheWrite = (base.cacheWrite ?? 0) + (next.cacheWrite ?? 0);
1903
- 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;
1904
- if (!hasAny)
1905
- return {
1906
- ...NULL_TOKENS,
1907
- contextSize: next.contextSize,
1908
- contextWindowSize: next.contextWindowSize
1909
- };
1910
- return {
1911
- input,
1912
- output,
1913
- cacheRead,
1914
- cacheWrite,
1915
- total: input + output + cacheRead + cacheWrite,
1916
- contextSize: next.contextSize ?? base.contextSize,
1917
- contextWindowSize: next.contextWindowSize ?? base.contextWindowSize
1918
- };
1919
- }
1920
- function defaultCreateTracker(trackerPath, content) {
1921
- fs12.mkdirSync(path9.dirname(trackerPath), { recursive: true });
1922
- try {
1923
- fs12.writeFileSync(trackerPath, content, { encoding: "utf-8", flag: "wx" });
1924
- } catch (e) {
1925
- if (e.code !== "EEXIST") throw e;
1926
- }
1927
- }
1928
- function createWorkflowRunner(input) {
1929
- const runId = crypto.randomUUID();
1930
- let cancelled = false;
1931
- let status = "running";
1932
- let iterations = 0;
1933
- let cumulativeTokens = { ...NULL_TOKENS };
1934
- let stopReason;
1935
- const trackerResolved = resolveTrackerPath({
1936
- projectDir: input.projectDir,
1937
- sessionId: input.sessionId,
1938
- workflow: input.workflow
1939
- });
1940
- const trackerAbsPath = trackerResolved?.absolutePath ?? null;
1941
- const trackerPromptPath = trackerResolved?.promptPath;
1942
- function snapshot() {
1943
- return {
1944
- runId,
1945
- sessionId: input.sessionId,
1946
- workflowName: input.workflow?.name,
1947
- iteration: iterations,
1948
- maxIterations: input.workflow?.loop?.maxIterations ?? 1,
1949
- status,
1950
- stopReason,
1951
- trackerPath: trackerPromptPath
1952
- };
1953
- }
1954
- function persist() {
1955
- try {
1956
- input.persistRunState(snapshot());
1957
- } catch {
1958
- }
1959
- }
1960
- const result = (async () => {
1961
- await Promise.resolve();
1962
- if (trackerAbsPath && input.workflow?.loop?.enabled) {
1963
- const content = substituteVariables(TRACKER_SKELETON_TEMPLATE, {
1964
- sessionId: input.sessionId,
1965
- trackerPath: trackerPromptPath,
1966
- input: input.prompt
1967
- });
1968
- const write = input.createTracker ?? defaultCreateTracker;
1969
- write(trackerAbsPath, content);
1970
- }
1971
- persist();
1972
- const workflowState = createWorkflowRunState({
1973
- projectDir: input.projectDir,
1974
- sessionId: input.sessionId,
1975
- workflow: input.workflow,
1976
- harness: input.harness
1977
- });
1978
- let nextContinuation = input.initialContinuation ?? {
1979
- mode: "fresh"
1980
- };
1981
- const loop = input.workflow?.loop;
1982
- while (!cancelled) {
1983
- iterations++;
1984
- const prepared = prepareWorkflowTurn(workflowState, {
1985
- prompt: input.prompt,
1986
- iteration: iterations,
1987
- configOverride: void 0
1988
- });
1989
- const turnResult = await input.startTurn({
1990
- prompt: prepared.prompt,
1991
- continuation: nextContinuation,
1992
- configOverride: prepared.configOverride
1993
- });
1994
- if (cancelled) {
1995
- status = "cancelled";
1996
- persist();
1997
- break;
1998
- }
1999
- cumulativeTokens = mergeTokens(cumulativeTokens, turnResult.tokens);
2000
- if (turnResult.error || turnResult.exitCode !== null && turnResult.exitCode !== 0) {
2001
- status = "failed";
2002
- const parts = [];
2003
- if (turnResult.error?.message) {
2004
- parts.push(turnResult.error.message);
2005
- } else if (turnResult.exitCode !== null) {
2006
- parts.push(`Process exited with code ${turnResult.exitCode}`);
2007
- }
2008
- if (turnResult.lastStderr) {
2009
- parts.push(turnResult.lastStderr);
2010
- }
2011
- stopReason = parts.join(": ") || "Turn failed";
2012
- persist();
2013
- break;
2014
- }
2015
- const transport = turnResult.diagnostics?.transport;
2016
- if (transport && transport.streamToolUses > 0 && transport.preToolUseEvents === 0) {
2017
- status = "failed";
2018
- stopReason = `Hook transport broken: observed ${transport.streamToolUses} tool use(s) in Claude stream but received no PreToolUse events.`;
2019
- persist();
2020
- break;
2021
- }
2022
- if (!loop?.enabled) {
2023
- status = "completed";
2024
- persist();
2025
- break;
2026
- }
2027
- if (trackerAbsPath) {
2028
- const outcome = resolveTurnOutcome({
2029
- trackerPath: trackerAbsPath,
2030
- loop,
2031
- iteration: iterations
2032
- });
2033
- if (outcome.kind === "stop") {
2034
- status = outcome.status;
2035
- stopReason = outcome.stopReason;
2036
- persist();
2037
- break;
2038
- }
2039
- }
2040
- persist();
2041
- input.onIterationComplete?.(snapshot());
2042
- nextContinuation = { mode: "fresh" };
2043
- }
2044
- if (cancelled && status === "running") {
2045
- status = "cancelled";
2046
- persist();
2047
- }
2048
- return {
2049
- runId,
2050
- status,
2051
- iterations,
2052
- stopReason,
2053
- tokens: cumulativeTokens
2054
- };
2055
- })();
2056
- return {
2057
- runId,
2058
- result,
2059
- cancel() {
2060
- cancelled = true;
2061
- },
2062
- kill() {
2063
- cancelled = true;
2064
- input.abortCurrentTurn?.();
2065
- }
2066
- };
2067
- }
2068
-
2069
- // src/core/workflows/useWorkflowSessionController.ts
2070
- function useWorkflowSessionController(base, input) {
2071
- const [isRunning, setIsRunning] = useState(false);
2072
- const runnerRef = useRef(null);
2073
- const activeRunIdRef = useRef(null);
2074
- const cancelCurrentRun = useCallback(async () => {
2075
- const runner = runnerRef.current;
2076
- if (runner) {
2077
- runner.kill();
2078
- await runner.result.catch(() => {
2079
- });
2080
- runnerRef.current = null;
2081
- activeRunIdRef.current = null;
2082
- }
2083
- }, []);
2084
- const interrupt = useCallback(() => {
2085
- const runner = runnerRef.current;
2086
- if (runner) {
2087
- runner.kill();
2088
- runnerRef.current = null;
2089
- activeRunIdRef.current = null;
2090
- } else {
2091
- void base.kill().catch(() => {
2092
- });
2093
- }
2094
- setIsRunning(false);
2095
- }, [base]);
2096
- const kill = useCallback(async () => {
2097
- if (runnerRef.current) {
2098
- await cancelCurrentRun();
2099
- } else {
2100
- await base.kill();
2101
- }
2102
- setIsRunning(false);
2103
- }, [base, cancelCurrentRun]);
2104
- const spawn = useCallback(
2105
- async (prompt, continuation, _configOverride) => {
2106
- await cancelCurrentRun();
2107
- setIsRunning(true);
2108
- const handle = createWorkflowRunner({
2109
- sessionId: input.sessionId ?? "",
2110
- projectDir: input.projectDir,
2111
- harness: input.harness,
2112
- workflow: input.workflow,
2113
- prompt,
2114
- initialContinuation: continuation,
2115
- startTurn: (turnInput) => base.startTurn(
2116
- turnInput.prompt,
2117
- turnInput.continuation,
2118
- turnInput.configOverride
2119
- ),
2120
- persistRunState: input.persistRunState ?? (() => {
2121
- }),
2122
- abortCurrentTurn: () => void base.kill().catch(() => {
2123
- })
2124
- });
2125
- runnerRef.current = handle;
2126
- activeRunIdRef.current = handle.runId;
2127
- try {
2128
- const runResult = await handle.result;
2129
- return {
2130
- exitCode: runResult.status === "failed" ? 1 : 0,
2131
- error: runResult.status === "failed" ? new Error(runResult.stopReason ?? "Run failed") : null,
2132
- tokens: runResult.tokens,
2133
- streamMessage: null
2134
- };
2135
- } finally {
2136
- if (runnerRef.current === handle) {
2137
- runnerRef.current = null;
2138
- activeRunIdRef.current = null;
2139
- setIsRunning(false);
2140
- }
2141
- }
2142
- },
2143
- [
2144
- base,
2145
- cancelCurrentRun,
2146
- input.projectDir,
2147
- input.sessionId,
2148
- input.harness,
2149
- input.workflow,
2150
- input.persistRunState
2151
- ]
2152
- );
2153
- useEffect(() => {
2154
- return () => {
2155
- runnerRef.current?.kill();
2156
- runnerRef.current = null;
2157
- activeRunIdRef.current = null;
2158
- };
2159
- }, []);
2160
- return {
2161
- ...base,
2162
- startTurn: spawn,
2163
- isRunning,
2164
- interrupt,
2165
- kill,
2166
- get activeRunId() {
2167
- return activeRunIdRef.current;
2168
- }
2169
- };
2170
- }
2171
-
2172
- // src/infra/plugins/mcpOptions.ts
2173
- import fs13 from "fs";
2174
- import path10 from "path";
2175
- function collectMcpServersWithOptions(pluginDirs) {
2176
- const result = [];
2177
- const seen = /* @__PURE__ */ new Set();
2178
- for (const dir of pluginDirs) {
2179
- const mcpPath = path10.join(dir, ".mcp.json");
2180
- if (!fs13.existsSync(mcpPath)) {
2181
- continue;
2182
- }
2183
- const config = JSON.parse(fs13.readFileSync(mcpPath, "utf-8"));
2184
- for (const [serverName, serverConfig] of Object.entries(
2185
- config.mcpServers ?? {}
2186
- )) {
2187
- if (seen.has(serverName) || !Array.isArray(serverConfig.options) || serverConfig.options.length === 0) {
2188
- continue;
2189
- }
2190
- seen.add(serverName);
2191
- result.push({ serverName, options: serverConfig.options });
2192
- }
2193
- }
2194
- return result;
2195
- }
2196
-
2197
- export {
2198
- createWorkflowRunner,
2199
- useWorkflowSessionController,
2200
- isMarketplaceRef,
2201
- findMarketplaceRepoDir,
2202
- resolveWorkflowMarketplaceSource,
2203
- listMarketplaceWorkflows,
2204
- listMarketplaceWorkflowsFromRepo,
2205
- resolveMarketplaceWorkflow,
2206
- gatherMarketplaceWorkflowSources,
2207
- resolveWorkflowInstall,
2208
- pullMarketplaceRepo,
2209
- resolveMarketplacePlugin,
2210
- projectConfigPath,
2211
- readConfig,
2212
- resolveActiveWorkflow,
2213
- readGlobalConfig,
2214
- writeGlobalConfig,
2215
- writeProjectConfig,
2216
- hasProjectWorkflow,
2217
- installWorkflowPlugins,
2218
- resolveWorkflowPlugins,
2219
- listBuiltinWorkflows,
2220
- resolveWorkflow,
2221
- installWorkflowFromSource,
2222
- updateWorkflow,
2223
- updateWorkflows,
2224
- listWorkflows,
2225
- removeWorkflow,
2226
- compileWorkflowPlan,
2227
- collectMcpServersWithOptions
2228
- };
2229
- //# sourceMappingURL=chunk-73V7GXV6.js.map