@drisp/cli 0.4.5 → 0.4.7

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.
@@ -0,0 +1,104 @@
1
+ // src/infra/daemon/logFile.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ var DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
5
+ var DEFAULT_MAX_FILES = 5;
6
+ function openDaemonLog(logPath, options = {}) {
7
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
8
+ const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
9
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
10
+ fs.mkdirSync(path.dirname(logPath), { recursive: true, mode: 448 });
11
+ let fd = fs.openSync(logPath, "a", 384);
12
+ if (process.platform !== "win32") {
13
+ try {
14
+ fs.chmodSync(logPath, 384);
15
+ } catch {
16
+ }
17
+ }
18
+ function rotate() {
19
+ try {
20
+ fs.closeSync(fd);
21
+ } catch {
22
+ }
23
+ for (let i = maxFiles - 1; i >= 1; i -= 1) {
24
+ const src = `${logPath}.${i}`;
25
+ const dst = `${logPath}.${i + 1}`;
26
+ try {
27
+ fs.renameSync(src, dst);
28
+ } catch (err) {
29
+ if (err.code !== "ENOENT") {
30
+ }
31
+ }
32
+ }
33
+ try {
34
+ fs.renameSync(logPath, `${logPath}.1`);
35
+ } catch (err) {
36
+ if (err.code !== "ENOENT") {
37
+ }
38
+ }
39
+ fd = fs.openSync(logPath, "a", 384);
40
+ if (process.platform !== "win32") {
41
+ try {
42
+ fs.chmodSync(logPath, 384);
43
+ } catch {
44
+ }
45
+ }
46
+ }
47
+ function write(level, message) {
48
+ const line = `${now().toISOString()} ${level.toUpperCase()} ${redactSecrets(message)}
49
+ `;
50
+ const buf = Buffer.from(line, "utf-8");
51
+ let stat = null;
52
+ try {
53
+ stat = fs.fstatSync(fd);
54
+ } catch {
55
+ }
56
+ if (stat && stat.size + buf.length > maxBytes) {
57
+ rotate();
58
+ }
59
+ try {
60
+ fs.writeSync(fd, buf);
61
+ } catch {
62
+ try {
63
+ fs.closeSync(fd);
64
+ } catch {
65
+ }
66
+ try {
67
+ fd = fs.openSync(logPath, "a", 384);
68
+ fs.writeSync(fd, buf);
69
+ } catch {
70
+ }
71
+ }
72
+ }
73
+ function close() {
74
+ try {
75
+ fs.closeSync(fd);
76
+ } catch {
77
+ }
78
+ }
79
+ return { write, close, path: logPath };
80
+ }
81
+ var SECRET_PATTERNS = [
82
+ [/Bearer\s+[A-Za-z0-9._\-+/=]+/g, "Bearer ***"],
83
+ [
84
+ /(["']?(?:access|refresh)_?token["']?\s*[:=]\s*)["']?[A-Za-z0-9._\-+/=]+/gi,
85
+ '$1"***"'
86
+ ],
87
+ [/(Sec-WebSocket-Protocol:\s*)\S+/gi, "$1***"],
88
+ [
89
+ /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/g,
90
+ "***.***.***"
91
+ ]
92
+ ];
93
+ function redactSecrets(message) {
94
+ let out = message;
95
+ for (const [pattern, replacement] of SECRET_PATTERNS) {
96
+ out = out.replace(pattern, replacement);
97
+ }
98
+ return out;
99
+ }
100
+
101
+ export {
102
+ openDaemonLog
103
+ };
104
+ //# sourceMappingURL=chunk-2OJ3GGIP.js.map
@@ -934,6 +934,31 @@ function hasProjectWorkflow(projectDir) {
934
934
  return hasActiveWorkflow(projectConfigPath(projectDir));
935
935
  }
936
936
 
937
+ // src/infra/plugins/mcpOptions.ts
938
+ import fs6 from "fs";
939
+ import path5 from "path";
940
+ function collectMcpServersWithOptions(pluginDirs) {
941
+ const result = [];
942
+ const seen = /* @__PURE__ */ new Set();
943
+ for (const dir of pluginDirs) {
944
+ const mcpPath = path5.join(dir, ".mcp.json");
945
+ if (!fs6.existsSync(mcpPath)) {
946
+ continue;
947
+ }
948
+ const config = JSON.parse(fs6.readFileSync(mcpPath, "utf-8"));
949
+ for (const [serverName, serverConfig] of Object.entries(
950
+ config.mcpServers ?? {}
951
+ )) {
952
+ if (seen.has(serverName) || !Array.isArray(serverConfig.options) || serverConfig.options.length === 0) {
953
+ continue;
954
+ }
955
+ seen.add(serverName);
956
+ result.push({ serverName, options: serverConfig.options });
957
+ }
958
+ }
959
+ return result;
960
+ }
961
+
937
962
  // src/core/workflows/types.ts
938
963
  function pluginSpecRef(spec) {
939
964
  return typeof spec === "string" ? spec : spec.ref;
@@ -1027,17 +1052,17 @@ function refreshPinnedWorkflowPlugins(workflow) {
1027
1052
  }
1028
1053
 
1029
1054
  // src/core/workflows/registry.ts
1030
- import fs9 from "fs";
1055
+ import fs10 from "fs";
1031
1056
  import os5 from "os";
1032
- import path7 from "path";
1057
+ import path8 from "path";
1033
1058
 
1034
1059
  // src/core/workflows/builtins/index.ts
1035
- import fs7 from "fs";
1060
+ import fs8 from "fs";
1036
1061
  import os4 from "os";
1037
- import path5 from "path";
1062
+ import path6 from "path";
1038
1063
 
1039
1064
  // src/core/workflows/loopManager.ts
1040
- import fs6 from "fs";
1065
+ import fs7 from "fs";
1041
1066
 
1042
1067
  // src/core/workflows/templateVars.ts
1043
1068
  function substituteVariables(text, ctx) {
@@ -1068,7 +1093,7 @@ function createLoopManager(trackerPath, config) {
1068
1093
  const blockedMarker = config.blockedMarker ?? DEFAULT_BLOCKED_MARKER;
1069
1094
  function readTracker() {
1070
1095
  try {
1071
- return fs6.readFileSync(trackerPath, "utf-8");
1096
+ return fs7.readFileSync(trackerPath, "utf-8");
1072
1097
  } catch {
1073
1098
  return "";
1074
1099
  }
@@ -1180,17 +1205,17 @@ If you are blocked and cannot make further progress:
1180
1205
  3. Explain what needs to happen to unblock the task whenever possible
1181
1206
  `;
1182
1207
  function ensureSystemPromptFile() {
1183
- const dir = path5.join(
1208
+ const dir = path6.join(
1184
1209
  os4.homedir(),
1185
1210
  ".config",
1186
1211
  "athena",
1187
1212
  "builtins",
1188
1213
  "default"
1189
1214
  );
1190
- const filePath = path5.join(dir, "system_prompt.md");
1191
- if (!fs7.existsSync(filePath) || fs7.readFileSync(filePath, "utf-8") !== SYSTEM_PROMPT) {
1192
- fs7.mkdirSync(dir, { recursive: true });
1193
- fs7.writeFileSync(filePath, SYSTEM_PROMPT, "utf-8");
1215
+ const filePath = path6.join(dir, "system_prompt.md");
1216
+ if (!fs8.existsSync(filePath) || fs8.readFileSync(filePath, "utf-8") !== SYSTEM_PROMPT) {
1217
+ fs8.mkdirSync(dir, { recursive: true });
1218
+ fs8.writeFileSync(filePath, SYSTEM_PROMPT, "utf-8");
1194
1219
  }
1195
1220
  return filePath;
1196
1221
  }
@@ -1217,19 +1242,19 @@ function listBuiltinWorkflows() {
1217
1242
  }
1218
1243
 
1219
1244
  // src/core/workflows/sourceMetadata.ts
1220
- import fs8 from "fs";
1221
- import path6 from "path";
1245
+ import fs9 from "fs";
1246
+ import path7 from "path";
1222
1247
  function legacyLocalToNew(legacyPath, legacyRepoDir) {
1223
1248
  const repoDir = legacyRepoDir ?? findMarketplaceRepoDir(legacyPath);
1224
1249
  if (!repoDir) {
1225
1250
  return { kind: "filesystem", path: legacyPath };
1226
1251
  }
1227
1252
  try {
1228
- const canonicalRepoDir = fs8.realpathSync(repoDir);
1253
+ const canonicalRepoDir = fs9.realpathSync(repoDir);
1229
1254
  const listings = listMarketplaceWorkflowsFromRepo(canonicalRepoDir);
1230
- const absolutePath = fs8.realpathSync(legacyPath);
1255
+ const absolutePath = fs9.realpathSync(legacyPath);
1231
1256
  const match = listings.find(
1232
- (l) => fs8.realpathSync(l.workflowPath) === absolutePath
1257
+ (l) => fs9.realpathSync(l.workflowPath) === absolutePath
1233
1258
  );
1234
1259
  if (match) {
1235
1260
  return {
@@ -1244,11 +1269,11 @@ function legacyLocalToNew(legacyPath, legacyRepoDir) {
1244
1269
  return { kind: "filesystem", path: legacyPath };
1245
1270
  }
1246
1271
  function readWorkflowSourceMetadata(workflowDir) {
1247
- const sourceFile = path6.join(workflowDir, "source.json");
1248
- if (!fs8.existsSync(sourceFile)) return void 0;
1272
+ const sourceFile = path7.join(workflowDir, "source.json");
1273
+ if (!fs9.existsSync(sourceFile)) return void 0;
1249
1274
  let raw;
1250
1275
  try {
1251
- raw = JSON.parse(fs8.readFileSync(sourceFile, "utf-8"));
1276
+ raw = JSON.parse(fs9.readFileSync(sourceFile, "utf-8"));
1252
1277
  } catch {
1253
1278
  throw new Error(`Invalid source.json: ${sourceFile} is not valid JSON`);
1254
1279
  }
@@ -1290,9 +1315,9 @@ function readWorkflowSourceMetadata(workflowDir) {
1290
1315
  throw new Error(`Invalid source.json: ${sourceFile} kind is not supported`);
1291
1316
  }
1292
1317
  function writeWorkflowSourceMetadata(workflowDir, metadata) {
1293
- fs8.mkdirSync(workflowDir, { recursive: true });
1294
- fs8.writeFileSync(
1295
- path6.join(workflowDir, "source.json"),
1318
+ fs9.mkdirSync(workflowDir, { recursive: true });
1319
+ fs9.writeFileSync(
1320
+ path7.join(workflowDir, "source.json"),
1296
1321
  JSON.stringify({ v: 2, ...metadata }),
1297
1322
  "utf-8"
1298
1323
  );
@@ -1300,19 +1325,19 @@ function writeWorkflowSourceMetadata(workflowDir, metadata) {
1300
1325
 
1301
1326
  // src/core/workflows/registry.ts
1302
1327
  function registryDir() {
1303
- return path7.join(os5.homedir(), ".config", "athena", "workflows");
1328
+ return path8.join(os5.homedir(), ".config", "athena", "workflows");
1304
1329
  }
1305
1330
  function ensurePathWithinRoot(rootDir, targetPath, label) {
1306
- const relative = path7.relative(rootDir, targetPath);
1307
- if (relative === "" || !relative.startsWith("..") && !path7.isAbsolute(relative)) {
1331
+ const relative = path8.relative(rootDir, targetPath);
1332
+ if (relative === "" || !relative.startsWith("..") && !path8.isAbsolute(relative)) {
1308
1333
  return;
1309
1334
  }
1310
1335
  throw new Error(`${label} resolves outside the workflow root: ${targetPath}`);
1311
1336
  }
1312
1337
  function resolveWorkflow(name) {
1313
- const workflowDir = path7.join(registryDir(), name);
1314
- const workflowPath = path7.join(workflowDir, "workflow.json");
1315
- if (!fs9.existsSync(workflowPath)) {
1338
+ const workflowDir = path8.join(registryDir(), name);
1339
+ const workflowPath = path8.join(workflowDir, "workflow.json");
1340
+ if (!fs10.existsSync(workflowPath)) {
1316
1341
  const builtin = resolveBuiltinWorkflow(name);
1317
1342
  if (builtin) {
1318
1343
  return builtin;
@@ -1322,7 +1347,7 @@ function resolveWorkflow(name) {
1322
1347
  );
1323
1348
  }
1324
1349
  const source = readWorkflowSourceMetadata(workflowDir);
1325
- const raw = JSON.parse(fs9.readFileSync(workflowPath, "utf-8"));
1350
+ const raw = JSON.parse(fs10.readFileSync(workflowPath, "utf-8"));
1326
1351
  if (!Array.isArray(raw["plugins"])) {
1327
1352
  throw new Error(
1328
1353
  `Invalid workflow.json: "plugins" must be an array (got ${typeof raw["plugins"]})`
@@ -1362,15 +1387,15 @@ function resolveWorkflow(name) {
1362
1387
  );
1363
1388
  }
1364
1389
  const workflowFile = raw["workflowFile"];
1365
- if (typeof workflowFile === "string" && !path7.isAbsolute(workflowFile)) {
1366
- const workflowFilePath = path7.resolve(workflowDir, workflowFile);
1367
- if (!fs9.existsSync(workflowFilePath)) {
1390
+ if (typeof workflowFile === "string" && !path8.isAbsolute(workflowFile)) {
1391
+ const workflowFilePath = path8.resolve(workflowDir, workflowFile);
1392
+ if (!fs10.existsSync(workflowFilePath)) {
1368
1393
  throw new Error(
1369
1394
  `Invalid workflow.json: workflowFile "${workflowFile}" not found at ${workflowFilePath}`
1370
1395
  );
1371
1396
  }
1372
1397
  raw["workflowFile"] = workflowFilePath;
1373
- } else if (typeof workflowFile === "string" && !fs9.existsSync(workflowFile)) {
1398
+ } else if (typeof workflowFile === "string" && !fs10.existsSync(workflowFile)) {
1374
1399
  throw new Error(
1375
1400
  `Invalid workflow.json: workflowFile "${workflowFile}" not found`
1376
1401
  );
@@ -1381,29 +1406,29 @@ function resolveWorkflow(name) {
1381
1406
  };
1382
1407
  }
1383
1408
  function readWorkflowSource(sourcePath) {
1384
- const content = fs9.readFileSync(sourcePath, "utf-8");
1409
+ const content = fs10.readFileSync(sourcePath, "utf-8");
1385
1410
  return { content, workflow: JSON.parse(content) };
1386
1411
  }
1387
1412
  function copyWorkflowFiles(sourcePath, destDir) {
1388
1413
  const { content, workflow } = readWorkflowSource(sourcePath);
1389
- const absoluteSourcePath = path7.resolve(sourcePath);
1390
- const sourceDir = path7.dirname(absoluteSourcePath);
1391
- const absoluteDestDir = path7.resolve(destDir);
1392
- fs9.mkdirSync(absoluteDestDir, { recursive: true });
1393
- fs9.writeFileSync(
1394
- path7.join(absoluteDestDir, "workflow.json"),
1414
+ const absoluteSourcePath = path8.resolve(sourcePath);
1415
+ const sourceDir = path8.dirname(absoluteSourcePath);
1416
+ const absoluteDestDir = path8.resolve(destDir);
1417
+ fs10.mkdirSync(absoluteDestDir, { recursive: true });
1418
+ fs10.writeFileSync(
1419
+ path8.join(absoluteDestDir, "workflow.json"),
1395
1420
  content,
1396
1421
  "utf-8"
1397
1422
  );
1398
1423
  const copyRelativeAsset = (assetPath) => {
1399
- if (!assetPath || path7.isAbsolute(assetPath)) return;
1400
- const sourceAssetPath = path7.resolve(sourceDir, assetPath);
1424
+ if (!assetPath || path8.isAbsolute(assetPath)) return;
1425
+ const sourceAssetPath = path8.resolve(sourceDir, assetPath);
1401
1426
  ensurePathWithinRoot(sourceDir, sourceAssetPath, "Workflow asset");
1402
- if (!fs9.existsSync(sourceAssetPath)) return;
1403
- const destAssetPath = path7.resolve(absoluteDestDir, assetPath);
1427
+ if (!fs10.existsSync(sourceAssetPath)) return;
1428
+ const destAssetPath = path8.resolve(absoluteDestDir, assetPath);
1404
1429
  ensurePathWithinRoot(absoluteDestDir, destAssetPath, "Workflow asset");
1405
- fs9.mkdirSync(path7.dirname(destAssetPath), { recursive: true });
1406
- fs9.copyFileSync(sourceAssetPath, destAssetPath);
1430
+ fs10.mkdirSync(path8.dirname(destAssetPath), { recursive: true });
1431
+ fs10.copyFileSync(sourceAssetPath, destAssetPath);
1407
1432
  };
1408
1433
  const rawWorkflow = workflow;
1409
1434
  const promptAsset = rawWorkflow["workflowFile"];
@@ -1440,7 +1465,7 @@ function installWorkflowFromSource(source, name) {
1440
1465
  'Workflow has no "name" field. Provide --name to specify one.'
1441
1466
  );
1442
1467
  }
1443
- const destDir = path7.join(registryDir(), workflowName);
1468
+ const destDir = path8.join(registryDir(), workflowName);
1444
1469
  copyWorkflowFiles(source.workflowPath, destDir);
1445
1470
  const metadata = toStoredMetadata(source);
1446
1471
  writeWorkflowSourceMetadata(destDir, metadata);
@@ -1482,7 +1507,7 @@ function reResolveFromMetadata(metadata) {
1482
1507
  return { kind: "filesystem", workflowPath: metadata.path };
1483
1508
  }
1484
1509
  function updateWorkflow(name) {
1485
- const workflowDir = path7.join(registryDir(), name);
1510
+ const workflowDir = path8.join(registryDir(), name);
1486
1511
  const metadata = readWorkflowSourceMetadata(workflowDir);
1487
1512
  if (!metadata) {
1488
1513
  throw new Error(
@@ -1496,8 +1521,8 @@ function updateWorkflow(name) {
1496
1521
  }
1497
1522
  function listWorkflows() {
1498
1523
  const dir = registryDir();
1499
- const installed = fs9.existsSync(dir) ? fs9.readdirSync(dir, { withFileTypes: true }).filter(
1500
- (entry) => entry.isDirectory() && fs9.existsSync(path7.join(dir, entry.name, "workflow.json"))
1524
+ const installed = fs10.existsSync(dir) ? fs10.readdirSync(dir, { withFileTypes: true }).filter(
1525
+ (entry) => entry.isDirectory() && fs10.existsSync(path8.join(dir, entry.name, "workflow.json"))
1501
1526
  ).map((entry) => entry.name) : [];
1502
1527
  const builtins = listBuiltinWorkflows().filter(
1503
1528
  (name) => !installed.includes(name)
@@ -1505,11 +1530,11 @@ function listWorkflows() {
1505
1530
  return [...builtins, ...installed];
1506
1531
  }
1507
1532
  function removeWorkflow(name) {
1508
- const dir = path7.join(registryDir(), name);
1509
- if (!fs9.existsSync(dir)) {
1533
+ const dir = path8.join(registryDir(), name);
1534
+ if (!fs10.existsSync(dir)) {
1510
1535
  throw new Error(`Workflow "${name}" not found.`);
1511
1536
  }
1512
- fs9.rmSync(dir, { recursive: true, force: true });
1537
+ fs10.rmSync(dir, { recursive: true, force: true });
1513
1538
  }
1514
1539
 
1515
1540
  // src/core/workflows/applyWorkflow.ts
@@ -1543,8 +1568,8 @@ function compileWorkflowPlan(input) {
1543
1568
  }
1544
1569
 
1545
1570
  // src/core/workflows/sessionPlan.ts
1546
- import fs10 from "fs";
1547
- import path8 from "path";
1571
+ import fs11 from "fs";
1572
+ import path9 from "path";
1548
1573
 
1549
1574
  // src/core/workflows/stateMachine.md
1550
1575
  var stateMachine_default = "# Stateless Session Protocol\n\nYou run in a stateless loop. Each session is a fresh process with no memory of prior sessions. **The tracker file is your only continuity** \u2014 read it, work, write it. Assume interruption: the runner may kill a long session, 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 session\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 session 1, run [**Orient**](#orient-session-1).\n3. Otherwise \u2192 this is a continuation, run [**Execute**](#execute-session-2) from where the tracker says, not from the start of the flow.\n\nReading first prevents two failure modes that waste whole sessions: redoing work already done, or contradicting decisions a prior session 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 session do first?\n\nA future session 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- 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 (session 1)\n\n1. **Replace the skeleton immediately**, before any domain work. Even a three-line tracker (goal + \"orienting\") protects you if the session dies during setup.\n2. Run the workflow's orientation steps. 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.\n3. 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 session that has no idea what they mean here.\n4. Record concrete observations \u2014 what you actually saw, not what you assumed. Wrong assumptions burn entire future sessions on rework.\n5. **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 session as a failure.\n\n### Execute (session 2+)\n\n- Work from where the tracker says, in the workflow's prescribed sequence. Not every session covers every step.\n- If the workflow defines a skill table, **load the relevant skill before each activity**. Skills carry the implementation detail (scaffolding steps, locator rules, anti-patterns, code templates) that this protocol intentionally doesn't repeat.\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 session should do first.\n3. If all work is verified: append the completion marker.\n4. If an unrecoverable blocker prevents progress: append the blocked marker, 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-session (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 session 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 session, only what's on disk survives.\n- **Plan changed** \u2014 task resequenced, new task surfaced, planned task no longer needed. Stale plans poison continuation sessions.\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 session 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 session. Bias toward writing.\n\n## Task UI projection\n\nThe tracker is the durable source of truth. Your harness's task tools are a session-scoped UI projection of the same plan, shown to the user in their CLI widget. They do not survive process exit.\n\n{{TASK_TOOL_INSTRUCTIONS}}\n\n- **Session 1, after orientation:** project the tracker's task plan into the task tools.\n- **Session 2+, after reading the tracker:** recreate the projection from the tracker; do not assume task IDs from prior sessions still exist.\n- **During work:** update both \u2014 the task tools for immediate UI feedback, the tracker for persistence \u2014 in the same working phase.\n\n## Session bounding\n\nEach fresh session 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 session. Ending early and letting the next session 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 session start; keep both in sync as work lands\n- [ ] Load the workflow's skill before each activity\n- [ ] Run quality gates in order; respect delegation constraints and retry limits\n- [ ] Write the completion marker only when all work is verified\n- [ ] Checkpoint and end before context goes stale\n";
@@ -1587,10 +1612,10 @@ function readWorkflowOverride(projectDir, workflow, sessionId, trackerPath, harn
1587
1612
  if (!workflow?.workflowFile) {
1588
1613
  return { workflowOverride: void 0, warnings: [] };
1589
1614
  }
1590
- const resolvedPath = path8.isAbsolute(workflow.workflowFile) ? workflow.workflowFile : path8.resolve(projectDir, workflow.workflowFile);
1615
+ const resolvedPath = path9.isAbsolute(workflow.workflowFile) ? workflow.workflowFile : path9.resolve(projectDir, workflow.workflowFile);
1591
1616
  let workflowContent;
1592
1617
  try {
1593
- workflowContent = fs10.readFileSync(resolvedPath, "utf-8");
1618
+ workflowContent = fs11.readFileSync(resolvedPath, "utf-8");
1594
1619
  } catch {
1595
1620
  return {
1596
1621
  workflowOverride: void 0,
@@ -1604,9 +1629,9 @@ function readWorkflowOverride(projectDir, workflow, sessionId, trackerPath, harn
1604
1629
  sessionId,
1605
1630
  trackerPath: trackerPath ?? void 0
1606
1631
  });
1607
- const workflowDir = path8.dirname(resolvedPath);
1608
- const composedPath = path8.join(workflowDir, ".composed-system-prompt.md");
1609
- fs10.writeFileSync(composedPath, composed, "utf-8");
1632
+ const workflowDir = path9.dirname(resolvedPath);
1633
+ const composedPath = path9.join(workflowDir, ".composed-system-prompt.md");
1634
+ fs11.writeFileSync(composedPath, composed, "utf-8");
1610
1635
  return {
1611
1636
  workflowOverride: {
1612
1637
  appendSystemPromptFile: composedPath,
@@ -1633,7 +1658,7 @@ function resolveTrackerPath(input) {
1633
1658
  return null;
1634
1659
  }
1635
1660
  const promptPath = input.sessionId ? rawPath.replaceAll("{sessionId}", input.sessionId) : rawPath;
1636
- const absolutePath = path8.isAbsolute(promptPath) ? promptPath : path8.resolve(input.projectDir, promptPath);
1661
+ const absolutePath = path9.isAbsolute(promptPath) ? promptPath : path9.resolve(input.projectDir, promptPath);
1637
1662
  return {
1638
1663
  absolutePath,
1639
1664
  promptPath
@@ -1679,7 +1704,7 @@ function shouldContinueWorkflowRun(state) {
1679
1704
  return null;
1680
1705
  }
1681
1706
  const loopState = loopManager.getState();
1682
- if (!fs10.existsSync(loopManager.trackerPath)) {
1707
+ if (!fs11.existsSync(loopManager.trackerPath)) {
1683
1708
  cleanupWorkflowRun(state);
1684
1709
  return {
1685
1710
  reason: "missing_tracker",
@@ -1725,8 +1750,8 @@ import { useCallback, useEffect, useRef, useState } from "react";
1725
1750
 
1726
1751
  // src/core/workflows/workflowRunner.ts
1727
1752
  import crypto from "crypto";
1728
- import fs11 from "fs";
1729
- import path9 from "path";
1753
+ import fs12 from "fs";
1754
+ import path10 from "path";
1730
1755
  var NULL_TOKENS = {
1731
1756
  input: null,
1732
1757
  output: null,
@@ -1783,9 +1808,9 @@ function mergeTokens(base, next) {
1783
1808
  };
1784
1809
  }
1785
1810
  function defaultCreateTracker(trackerPath, content) {
1786
- fs11.mkdirSync(path9.dirname(trackerPath), { recursive: true });
1811
+ fs12.mkdirSync(path10.dirname(trackerPath), { recursive: true });
1787
1812
  try {
1788
- fs11.writeFileSync(trackerPath, content, { encoding: "utf-8", flag: "wx" });
1813
+ fs12.writeFileSync(trackerPath, content, { encoding: "utf-8", flag: "wx" });
1789
1814
  } catch (e) {
1790
1815
  if (e.code !== "EEXIST") throw e;
1791
1816
  }
@@ -1898,7 +1923,8 @@ function createWorkflowRunner(input) {
1898
1923
  } else if (loopStop.reason === "max_iterations") {
1899
1924
  status = "exhausted";
1900
1925
  } else if (loopStop.reason === "skeleton_not_replaced") {
1901
- status = "completed";
1926
+ status = "failed";
1927
+ stopReason = "tracker skeleton was never replaced \u2014 Claude did not bootstrap the tracker";
1902
1928
  } else {
1903
1929
  status = "failed";
1904
1930
  stopReason = `Loop stopped: ${loopStop.reason}`;
@@ -2041,31 +2067,6 @@ function useWorkflowSessionController(base, input) {
2041
2067
  };
2042
2068
  }
2043
2069
 
2044
- // src/infra/plugins/mcpOptions.ts
2045
- import fs12 from "fs";
2046
- import path10 from "path";
2047
- function collectMcpServersWithOptions(pluginDirs) {
2048
- const result = [];
2049
- const seen = /* @__PURE__ */ new Set();
2050
- for (const dir of pluginDirs) {
2051
- const mcpPath = path10.join(dir, ".mcp.json");
2052
- if (!fs12.existsSync(mcpPath)) {
2053
- continue;
2054
- }
2055
- const config = JSON.parse(fs12.readFileSync(mcpPath, "utf-8"));
2056
- for (const [serverName, serverConfig] of Object.entries(
2057
- config.mcpServers ?? {}
2058
- )) {
2059
- if (seen.has(serverName) || !Array.isArray(serverConfig.options) || serverConfig.options.length === 0) {
2060
- continue;
2061
- }
2062
- seen.add(serverName);
2063
- result.push({ serverName, options: serverConfig.options });
2064
- }
2065
- }
2066
- return result;
2067
- }
2068
-
2069
2070
  export {
2070
2071
  createWorkflowRunner,
2071
2072
  useWorkflowSessionController,
@@ -2084,6 +2085,7 @@ export {
2084
2085
  writeGlobalConfig,
2085
2086
  writeProjectConfig,
2086
2087
  hasProjectWorkflow,
2088
+ collectMcpServersWithOptions,
2087
2089
  installWorkflowPlugins,
2088
2090
  resolveWorkflowPlugins,
2089
2091
  listBuiltinWorkflows,
@@ -2092,7 +2094,6 @@ export {
2092
2094
  updateWorkflow,
2093
2095
  listWorkflows,
2094
2096
  removeWorkflow,
2095
- compileWorkflowPlan,
2096
- collectMcpServersWithOptions
2097
+ compileWorkflowPlan
2097
2098
  };
2098
- //# sourceMappingURL=chunk-5VK2ZMVV.js.map
2099
+ //# sourceMappingURL=chunk-A54HGVML.js.map
@@ -0,0 +1,76 @@
1
+ import {
2
+ isLoopbackHost
3
+ } from "./chunk-WRHKXH5M.js";
4
+
5
+ // src/gateway/auth.ts
6
+ import crypto from "crypto";
7
+ import fs from "fs";
8
+ import path from "path";
9
+ var TOKEN_BYTES = 32;
10
+ function loadOrCreateToken(tokenPath) {
11
+ try {
12
+ const buf = fs.readFileSync(tokenPath);
13
+ const text = buf.toString("utf-8").trim();
14
+ if (text.length >= 16) return text;
15
+ } catch (err) {
16
+ const code = err.code;
17
+ if (code !== "ENOENT") throw err;
18
+ }
19
+ return writeNewToken(tokenPath);
20
+ }
21
+ function rotateGatewayToken(tokenPath) {
22
+ return writeNewToken(tokenPath);
23
+ }
24
+ function writeNewToken(tokenPath) {
25
+ const dir = path.dirname(tokenPath);
26
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
27
+ const token = crypto.randomBytes(TOKEN_BYTES).toString("base64url");
28
+ const tmpPath = `${tokenPath}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
29
+ fs.writeFileSync(tmpPath, token + "\n", { mode: 384 });
30
+ try {
31
+ fs.renameSync(tmpPath, tokenPath);
32
+ } catch (err) {
33
+ try {
34
+ fs.unlinkSync(tmpPath);
35
+ } catch {
36
+ }
37
+ throw err;
38
+ }
39
+ if (process.platform !== "win32") {
40
+ fs.chmodSync(dir, 448);
41
+ fs.chmodSync(tokenPath, 384);
42
+ }
43
+ return token;
44
+ }
45
+ function timingSafeTokenEqual(a, b) {
46
+ const ab = Buffer.from(a, "utf-8");
47
+ const bb = Buffer.from(b, "utf-8");
48
+ if (ab.length !== bb.length) {
49
+ const filler = Buffer.alloc(Math.max(ab.length, bb.length));
50
+ crypto.timingSafeEqual(filler, filler);
51
+ return false;
52
+ }
53
+ return crypto.timingSafeEqual(ab, bb);
54
+ }
55
+ function requireTokenForBind(spec, token) {
56
+ if (spec.kind === "uds" || isLoopbackHost(spec.host)) return;
57
+ if (!token || token.length < 16) {
58
+ throw new Error(
59
+ `gateway: refusing to bind ${spec.host}:${spec.port} without token configured`
60
+ );
61
+ }
62
+ if (spec.tls) return;
63
+ if (!spec.insecure) {
64
+ throw new Error(
65
+ `gateway: refusing to bind ${spec.host}:${spec.port} without TLS; pass --tls-cert/--tls-key, or --insecure only for trusted reverse-proxy/tunnel deployments`
66
+ );
67
+ }
68
+ }
69
+
70
+ export {
71
+ loadOrCreateToken,
72
+ rotateGatewayToken,
73
+ timingSafeTokenEqual,
74
+ requireTokenForBind
75
+ };
76
+ //# sourceMappingURL=chunk-D4W7RB25.js.map