@ametyst/cli 0.3.0 → 0.3.4

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.
Files changed (2) hide show
  1. package/dist/index.js +1217 -894
  2. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -112016,12 +112016,17 @@ var init_core = __esm({
112016
112016
  // src/commands/init-claim.ts
112017
112017
  var init_claim_exports = {};
112018
112018
  __export(init_claim_exports, {
112019
+ CLAIM_HOT_RELOAD_NOTICE: () => CLAIM_HOT_RELOAD_NOTICE,
112019
112020
  WRAP_KEY_BYTES: () => WRAP_KEY_BYTES,
112021
+ backupVaultFile: () => backupVaultFile,
112020
112022
  claimErrorMessage: () => claimErrorMessage,
112021
112023
  decryptBootstrapBlob: () => decryptBootstrapBlob,
112022
112024
  exchangeClaimToken: () => exchangeClaimToken,
112023
- parseClaimArgument: () => parseClaimArgument
112025
+ parseClaimArgument: () => parseClaimArgument,
112026
+ vaultBackupPath: () => vaultBackupPath,
112027
+ vaultBackupSuffix: () => vaultBackupSuffix
112024
112028
  });
112029
+ import { chmodSync as chmodSync3, constants as fsConstants, copyFileSync as copyFileSync3, existsSync as existsSync7 } from "fs";
112025
112030
  import { createDecipheriv as createDecipheriv2 } from "crypto";
112026
112031
  function claimErrorMessage(failure) {
112027
112032
  switch (failure.kind) {
@@ -112157,13 +112162,32 @@ function decryptBootstrapBlob(wrapKey, blob) {
112157
112162
  return null;
112158
112163
  }
112159
112164
  }
112160
- var WRAP_KEY_BYTES, BASE64URL;
112165
+ function vaultBackupSuffix(at) {
112166
+ return `${at.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z")}`;
112167
+ }
112168
+ function vaultBackupPath(vaultPath, at) {
112169
+ return `${vaultPath}.bak-${vaultBackupSuffix(at)}`;
112170
+ }
112171
+ function backupVaultFile(vaultPath, at = /* @__PURE__ */ new Date()) {
112172
+ if (!existsSync7(vaultPath)) return null;
112173
+ const target = vaultBackupPath(vaultPath, at);
112174
+ try {
112175
+ copyFileSync3(vaultPath, target, fsConstants.COPYFILE_EXCL);
112176
+ } catch (e) {
112177
+ if (e?.code === "EEXIST") return target;
112178
+ throw e;
112179
+ }
112180
+ chmodSync3(target, 384);
112181
+ return target;
112182
+ }
112183
+ var WRAP_KEY_BYTES, BASE64URL, CLAIM_HOT_RELOAD_NOTICE;
112161
112184
  var init_init_claim = __esm({
112162
112185
  "src/commands/init-claim.ts"() {
112163
112186
  "use strict";
112164
112187
  init_esm_shims();
112165
112188
  WRAP_KEY_BYTES = 32;
112166
112189
  BASE64URL = /^[A-Za-z0-9_-]+$/;
112190
+ CLAIM_HOT_RELOAD_NOTICE = "Running MCP servers pick up the new identity on their next call. If an agent host still shows the old workspace, restart it fully (Claude desktop: Cmd+Q).";
112167
112191
  }
112168
112192
  });
112169
112193
 
@@ -112173,12 +112197,12 @@ var init_version5 = __esm({
112173
112197
  "src/version.ts"() {
112174
112198
  "use strict";
112175
112199
  init_esm_shims();
112176
- CLI_VERSION = true ? "0.3.0" : "0.0.0-dev";
112200
+ CLI_VERSION = true ? "0.3.4" : "0.0.0-dev";
112177
112201
  }
112178
112202
  });
112179
112203
 
112180
112204
  // src/connections/catalog.ts
112181
- import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
112205
+ import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
112182
112206
  import { join as join6 } from "path";
112183
112207
  function declaredVerbsHeader() {
112184
112208
  return SUPPORTED_HTTP_METHODS.join(",");
@@ -112435,7 +112459,7 @@ function parseConnectorsResponseDetailed(body, onWarn = warnToStderr) {
112435
112459
  function readCachedCatalogStatus() {
112436
112460
  let raw;
112437
112461
  try {
112438
- if (!existsSync7(CONNECTOR_CACHE_PATH)) return { state: "absent" };
112462
+ if (!existsSync8(CONNECTOR_CACHE_PATH)) return { state: "absent" };
112439
112463
  raw = JSON.parse(readFileSync7(CONNECTOR_CACHE_PATH, "utf-8"));
112440
112464
  } catch (e) {
112441
112465
  return { state: "unreadable", reason: e instanceof Error ? e.message : String(e) };
@@ -112472,7 +112496,7 @@ function readMirrorRowsFromDisk() {
112472
112496
  }
112473
112497
  function readMirrorContainer() {
112474
112498
  try {
112475
- if (!existsSync7(CONNECTOR_CACHE_PATH)) return { state: "absent" };
112499
+ if (!existsSync8(CONNECTOR_CACHE_PATH)) return { state: "absent" };
112476
112500
  const raw = JSON.parse(readFileSync7(CONNECTOR_CACHE_PATH, "utf-8"));
112477
112501
  if (!isRecord(raw)) {
112478
112502
  return { state: "unreadable", reason: "the mirror's top level is not an object" };
@@ -112487,7 +112511,7 @@ function readMirrorContainer() {
112487
112511
  }
112488
112512
  function readMirrorFetchedAt() {
112489
112513
  try {
112490
- if (!existsSync7(CONNECTOR_CACHE_PATH)) return "";
112514
+ if (!existsSync8(CONNECTOR_CACHE_PATH)) return "";
112491
112515
  const raw = JSON.parse(readFileSync7(CONNECTOR_CACHE_PATH, "utf-8"));
112492
112516
  if (!isRecord(raw)) return "";
112493
112517
  return typeof raw.fetchedAt === "string" ? raw.fetchedAt : "";
@@ -113477,7 +113501,7 @@ var init_call2 = __esm({
113477
113501
  });
113478
113502
 
113479
113503
  // src/connections/engine-store.ts
113480
- import { chmodSync as chmodSync3, closeSync as closeSync2, existsSync as existsSync10, mkdirSync as mkdirSync7, openSync as openSync2 } from "fs";
113504
+ import { chmodSync as chmodSync4, closeSync as closeSync2, existsSync as existsSync11, mkdirSync as mkdirSync7, openSync as openSync2 } from "fs";
113481
113505
  import { dirname as dirname7, join as join16 } from "path";
113482
113506
  function connectionsDbPath() {
113483
113507
  return join16(AMETYST_DIR, CONNECTIONS_DB_FILENAME);
@@ -113485,8 +113509,8 @@ function connectionsDbPath() {
113485
113509
  function ensureOwnerOnlyFile(path2) {
113486
113510
  if (path2 === IN_MEMORY_DB) return;
113487
113511
  mkdirSync7(dirname7(path2), { recursive: true, mode: DIR_MODE });
113488
- if (!existsSync10(path2)) closeSync2(openSync2(path2, "a", CONNECTIONS_DB_FILE_MODE));
113489
- chmodSync3(path2, CONNECTIONS_DB_FILE_MODE);
113512
+ if (!existsSync11(path2)) closeSync2(openSync2(path2, "a", CONNECTIONS_DB_FILE_MODE));
113513
+ chmodSync4(path2, CONNECTIONS_DB_FILE_MODE);
113490
113514
  }
113491
113515
  function sqliteWasmDriver(db, raw) {
113492
113516
  const connection = {
@@ -114884,6 +114908,7 @@ walletCommand.command("import").description("Import a pasted session private key
114884
114908
 
114885
114909
  // src/commands/init.ts
114886
114910
  init_config();
114911
+ init_paths();
114887
114912
  init_core();
114888
114913
 
114889
114914
  // src/mcp-server/policy-access.ts
@@ -115199,7 +115224,14 @@ confirm. No need to re-run init.`);
115199
115224
  return false;
115200
115225
  }
115201
115226
  async function claimInit(claim, options) {
115202
- const { parseClaimArgument: parseClaimArgument2, exchangeClaimToken: exchangeClaimToken2, decryptBootstrapBlob: decryptBootstrapBlob2, claimErrorMessage: claimErrorMessage2 } = await Promise.resolve().then(() => (init_init_claim(), init_claim_exports));
115227
+ const {
115228
+ parseClaimArgument: parseClaimArgument2,
115229
+ exchangeClaimToken: exchangeClaimToken2,
115230
+ decryptBootstrapBlob: decryptBootstrapBlob2,
115231
+ claimErrorMessage: claimErrorMessage2,
115232
+ backupVaultFile: backupVaultFile2,
115233
+ CLAIM_HOT_RELOAD_NOTICE: CLAIM_HOT_RELOAD_NOTICE2
115234
+ } = await Promise.resolve().then(() => (init_init_claim(), init_claim_exports));
115203
115235
  const parsed = parseClaimArgument2(claim);
115204
115236
  if (!parsed.ok) {
115205
115237
  console.error(claimErrorMessage2(parsed.failure));
@@ -115226,11 +115258,21 @@ ${claimErrorMessage2({ kind: "decrypt" })}`);
115226
115258
  }
115227
115259
  await loginCommand({ apiKey: exchanged.result.apiKey, preseedAll: options.preseedAll });
115228
115260
  const effective = applyWorkspacePassphraseStance(options);
115261
+ try {
115262
+ const backup = backupVaultFile2(VAULT_PATH);
115263
+ if (backup) console.log(`Previous wallet backed up to ${backup}`);
115264
+ } catch (e) {
115265
+ console.error(
115266
+ `Could not back up the existing wallet (${e instanceof Error ? e.message : String(e)}) \u2014 continuing with the claim.`
115267
+ );
115268
+ }
115229
115269
  await walletImportCommand({
115230
115270
  walletKey: privateKey,
115231
115271
  force: effective.force,
115232
115272
  noPassphrase: effective.noPassphrase
115233
115273
  });
115274
+ console.log(`
115275
+ ${CLAIM_HOT_RELOAD_NOTICE2}`);
115234
115276
  return exchanged.result.apiKey;
115235
115277
  }
115236
115278
  async function initCommand(options = {}) {
@@ -116848,569 +116890,63 @@ init_local_state();
116848
116890
  init_esm_shims();
116849
116891
  init_paths();
116850
116892
  import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync8 } from "fs";
116851
- import { join as join7 } from "path";
116852
- var TASK_DEFINITION_FILES = [
116853
- ["skill", "SKILL.md", "markdownBody"],
116854
- ["vision", "VISION.md", "visionMd"],
116855
- ["constraints", "CONSTRAINTS.md", "constraintsMd"],
116856
- ["readme", "README.md", "readmeMd"],
116857
- ["dashboardHtml", "dashboard.html", "dashboardHtml"],
116858
- ["dashboardManifest", "dashboard.manifest.json", "dashboardManifest"]
116859
- ];
116860
- function materializeTask(task, runId) {
116861
- const dir = loopFireDir(task.slug, runId);
116862
- mkdirSync6(dir, { recursive: true, mode: 448 });
116863
- const files = {};
116864
- const skipped = [];
116865
- for (const [label2, filename, field] of TASK_DEFINITION_FILES) {
116866
- const body = task[field];
116867
- if (typeof body === "string" && body.length > 0) {
116868
- writeFileSync8(join7(dir, filename), body, { mode: 384 });
116869
- files[label2] = filename;
116870
- } else {
116871
- skipped.push(label2);
116872
- }
116873
- }
116874
- writeFileSync8(
116875
- join7(dir, "STATUS.md"),
116876
- `# STATUS \u2014 ${task.slug}
116893
+ import { join as join8 } from "path";
116877
116894
 
116878
- task_id: ${task.id}
116879
- run_id: ${runId}
116880
- started: pending
116881
- queue: not started
116882
- `,
116883
- { mode: 384 }
116884
- );
116885
- files.status = "STATUS.md";
116886
- return { dir, files, skipped };
116887
- }
116895
+ // src/loops/state-docs.ts
116896
+ init_esm_shims();
116897
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
116898
+ import { join as join7 } from "path";
116888
116899
 
116889
- // src/loops/dashboard.ts
116900
+ // src/loops/shipback.ts
116890
116901
  init_esm_shims();
116891
- import { createServer as createServer2 } from "http";
116892
- import * as realFs from "fs";
116893
- import { spawn as realSpawn } from "child_process";
116894
- import { join as join8 } from "path";
116895
- var DEFAULT_PORT = 4477;
116896
- var DASHBOARD_PORT_ENV = "AMETYST_LOOP_DASHBOARD_PORT";
116897
- var DASHBOARD_NO_OPEN_ENV = "AMETYST_DASHBOARD_NO_OPEN";
116898
- var NO_DASHBOARD_MESSAGE = "no dashboard on this task \u2014 ask your agent to create one";
116899
- var MAX_PORT_RETRIES = 20;
116900
- var SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
116901
- function parseManifestFiles(manifest) {
116902
- if (typeof manifest !== "string" || !manifest.trim()) return [];
116903
- let parsed;
116904
- try {
116905
- parsed = JSON.parse(manifest);
116906
- } catch {
116907
- return [];
116908
- }
116909
- const list2 = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.files) ? parsed.files : [];
116910
- return list2.filter((f) => typeof f === "string" && SAFE_NAME.test(f));
116902
+ function scanHeadings(text) {
116903
+ return scanDoc(text).items;
116911
116904
  }
116912
- function dashboardPort(env = process.env) {
116913
- const raw = Number(env[DASHBOARD_PORT_ENV]);
116914
- return Number.isInteger(raw) && raw > 0 && raw < 65536 ? raw : DEFAULT_PORT;
116905
+ function scanDoc(text) {
116906
+ const out = [];
116907
+ let offset = 0;
116908
+ let inFence = false;
116909
+ for (const line of text.split("\n")) {
116910
+ const bare = line.endsWith("\r") ? line.slice(0, -1) : line;
116911
+ const trimmed = bare.trimStart();
116912
+ if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) inFence = !inFence;
116913
+ else if (!inFence) {
116914
+ const m = /^###\s+(.*)$/.exec(bare);
116915
+ if (m) out.push({ heading: m[1].trim(), start: offset });
116916
+ }
116917
+ offset += line.length + 1;
116918
+ }
116919
+ return { items: out, unbalanced: inFence };
116915
116920
  }
116916
- function openDashboardInBrowser(url2, deps = {}) {
116917
- const env = deps.env ?? process.env;
116918
- if (env[DASHBOARD_NO_OPEN_ENV] === "1") return false;
116919
- const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
116920
- if (!isTTY) return false;
116921
- const platform = deps.platform ?? process.platform;
116922
- const cmd = platform === "darwin" ? "open" : platform === "linux" ? "xdg-open" : null;
116923
- if (!cmd) return false;
116924
- const spawn4 = deps.spawn ?? realSpawn;
116925
- try {
116926
- const child = spawn4(cmd, [url2], { stdio: "ignore", detached: true });
116927
- child.once?.("error", () => {
116928
- });
116929
- child.unref?.();
116930
- return true;
116931
- } catch {
116932
- return false;
116921
+ function headingsIgnoringFences(text) {
116922
+ const out = [];
116923
+ for (const line of text.split("\n")) {
116924
+ const bare = line.endsWith("\r") ? line.slice(0, -1) : line;
116925
+ const m = /^ {0,3}###\s+(.*)$/.exec(bare);
116926
+ if (m) out.push(m[1].trim());
116933
116927
  }
116928
+ return out;
116934
116929
  }
116935
- function startDashboardServer(args) {
116936
- const html = args.loop.dashboardHtml;
116937
- if (typeof html !== "string" || !html) return Promise.resolve(null);
116938
- const fs = args.deps?.fs ?? realFs;
116939
- const log = args.deps?.log ?? ((line) => console.error(line));
116940
- const make = args.deps?.createServer ?? createServer2;
116941
- const basePort = args.port ?? dashboardPort();
116942
- const files = parseManifestFiles(args.loop.dashboardManifest);
116943
- const handler = (req, res) => {
116944
- try {
116945
- const url2 = (req.url ?? "/").split("?")[0];
116946
- if (req.method === "GET" && url2 === "/") {
116947
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
116948
- res.end(html);
116949
- return;
116950
- }
116951
- if (req.method === "GET" && url2 === "/data") {
116952
- const data = {};
116953
- for (const name of files) {
116954
- try {
116955
- const p = join8(args.loopDir, name);
116956
- if (fs.existsSync(p)) data[name] = fs.readFileSync(p, "utf-8");
116957
- } catch {
116958
- }
116959
- }
116960
- const state = {};
116961
- try {
116962
- const p = join8(args.loopDir, ".state", "fires.jsonl");
116963
- if (fs.existsSync(p)) state["fires.jsonl"] = fs.readFileSync(p, "utf-8");
116964
- } catch {
116965
- }
116966
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
116967
- res.end(
116968
- JSON.stringify({
116969
- loop: args.loop.slug,
116970
- files: data,
116971
- state,
116972
- mode: "live",
116973
- asOf: (/* @__PURE__ */ new Date()).toISOString()
116974
- })
116975
- );
116976
- return;
116977
- }
116978
- res.writeHead(404, { "content-type": "text/plain" });
116979
- res.end("not found");
116980
- } catch {
116981
- try {
116982
- res.writeHead(500);
116983
- res.end();
116984
- } catch {
116985
- }
116986
- }
116987
- };
116988
- const bind = (port) => new Promise((resolve) => {
116989
- const server2 = make(handler);
116990
- server2.once("error", (err) => {
116991
- try {
116992
- server2.close();
116993
- } catch {
116994
- }
116995
- resolve({ ok: false, err });
116996
- });
116997
- server2.listen(port, "127.0.0.1", () => {
116998
- server2.unref();
116999
- resolve({
117000
- ok: true,
117001
- handle: {
117002
- server: server2,
117003
- port,
117004
- close() {
117005
- try {
117006
- server2.close();
117007
- server2.closeAllConnections?.();
117008
- } catch {
117009
- }
117010
- }
117011
- }
117012
- });
117013
- });
117014
- });
117015
- return (async () => {
117016
- const lastPort = Math.min(basePort + MAX_PORT_RETRIES, 65535);
117017
- for (let port = basePort; port <= lastPort; port++) {
117018
- const attempt = await bind(port);
117019
- if (attempt.ok) {
117020
- log(` loop dashboard: http://localhost:${attempt.handle.port}`);
117021
- return attempt.handle;
117022
- }
117023
- if (attempt.err.code !== "EADDRINUSE") {
117024
- log(
117025
- ` (loop dashboard not started on :${port} \u2014 ${attempt.err.code ?? attempt.err.message}; run continues)`
117026
- );
117027
- return null;
117028
- }
117029
- }
117030
- log(` (loop dashboard not started \u2014 :${basePort}-${lastPort} all in use; run continues)`);
117031
- return null;
117032
- })();
116930
+ function headings(text) {
116931
+ return scanHeadings(text).map((h) => h.heading);
117033
116932
  }
117034
-
117035
- // src/mcp-server/task-run-mode.ts
117036
- init_esm_shims();
117037
- var ACCEPTED_RUN_MODES = ["in-chat", "headless"];
117038
- var FRONTMATTER_SCAN_LIMIT = 8192;
117039
- function normalizeRunMode(raw) {
117040
- if (typeof raw !== "string") return void 0;
117041
- const v = raw.trim().toLowerCase();
117042
- if (v === "headless") return "headless";
117043
- if (v === "in-chat" || v === "inchat" || v === "in_chat") return "in-chat";
117044
- return void 0;
116933
+ function countByHeading(list2) {
116934
+ const m = /* @__PURE__ */ new Map();
116935
+ for (const h of list2) m.set(h, (m.get(h) ?? 0) + 1);
116936
+ return m;
117045
116937
  }
117046
- function describeProvided(raw) {
117047
- if (typeof raw === "string") return raw.trim();
117048
- try {
117049
- return JSON.stringify(raw) ?? String(raw);
117050
- } catch {
117051
- return String(raw);
117052
- }
116938
+ function sections(fragment) {
116939
+ const found = scanHeadings(fragment);
116940
+ if (found.length === 0) return { preamble: fragment, blocks: [] };
116941
+ const preamble = fragment.slice(0, found[0].start);
116942
+ const blocks = found.map((h, i) => ({
116943
+ heading: h.heading,
116944
+ text: fragment.slice(h.start, i + 1 < found.length ? found[i + 1].start : fragment.length)
116945
+ }));
116946
+ return { preamble, blocks };
117053
116947
  }
117054
- function classifyRunModeArgument(raw) {
117055
- if (raw === void 0 || raw === null) return { kind: "omitted" };
117056
- if (typeof raw === "string" && raw.trim() === "") return { kind: "omitted" };
117057
- const mode2 = normalizeRunMode(raw);
117058
- if (mode2) return { kind: "valid", mode: mode2 };
117059
- return { kind: "invalid", provided: describeProvided(raw) };
117060
- }
117061
- function unquoteScalar(raw) {
117062
- let v = raw.trim();
117063
- const comment = v.match(/(?:^|\s)#.*$/);
117064
- if (comment) v = v.slice(0, comment.index === 0 ? 0 : comment.index).trim();
117065
- if (v.length >= 2 && (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'"))) {
117066
- v = v.slice(1, -1).trim();
117067
- }
117068
- return v;
117069
- }
117070
- function parseDefaultRunMode(body) {
117071
- if (typeof body !== "string" || !body) return void 0;
117072
- const head = body.replace(/^\uFEFF/, "").slice(0, FRONTMATTER_SCAN_LIMIT);
117073
- const opener = head.match(/^---[ \t]*\r?\n/);
117074
- if (!opener) return void 0;
117075
- const rest = head.slice(opener[0].length);
117076
- const closer = rest.search(/^(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/m);
117077
- if (closer < 0) return void 0;
117078
- const block = rest.slice(0, closer);
117079
- const hit = block.match(/^defaultRunMode[ \t]*:[ \t]*(.*)$/m);
117080
- if (!hit) return void 0;
117081
- return normalizeRunMode(unquoteScalar(hit[1] ?? ""));
117082
- }
117083
- function resolveRunMode(explicit, body) {
117084
- const arg = classifyRunModeArgument(explicit);
117085
- if (arg.kind === "invalid") {
117086
- return { ok: false, error: "invalid_mode", provided: arg.provided, accepted: ACCEPTED_RUN_MODES };
117087
- }
117088
- if (arg.kind === "valid") return { ok: true, mode: arg.mode, source: "explicit" };
117089
- const fromBody = parseDefaultRunMode(body);
117090
- if (fromBody) return { ok: true, mode: fromBody, source: "frontmatter" };
117091
- return { ok: true, mode: "in-chat", source: "default" };
117092
- }
117093
-
117094
- // src/loops/estimate.ts
117095
- init_esm_shims();
117096
- function estimateBlastRadius(loop2) {
117097
- const g = loop2.graphJson ?? {};
117098
- const nodes = Array.isArray(g.nodes) ? g.nodes : [];
117099
- const steps = nodes.length;
117100
- const paidSteps = nodes.filter(
117101
- (n) => n?.type === "spend" || n?.data?.paid === true || n?.paid === true
117102
- ).length;
117103
- const costHints = nodes.map((n) => Number(n?.data?.estCostEur ?? n?.estCostEur)).filter((x) => !Number.isNaN(x));
117104
- const estCostEur = costHints.length ? costHints.reduce((a, b) => a + b, 0) : null;
117105
- return { steps, paidSteps, estCostEur };
117106
- }
117107
-
117108
- // src/loops/dashboard-template.ts
117109
- init_esm_shims();
117110
- var DEFAULT_DASHBOARD_FILES = [
117111
- "VISION.md",
117112
- "CONSTRAINTS.md",
117113
- "QUEUE.md",
117114
- "STATUS.md",
117115
- "README.md",
117116
- "rounds.jsonl"
117117
- ];
117118
- function parseProcessSpec(manifest) {
117119
- if (typeof manifest !== "string" || !manifest.trim()) return null;
117120
- let parsed;
117121
- try {
117122
- parsed = JSON.parse(manifest);
117123
- } catch {
117124
- return null;
117125
- }
117126
- const raw = parsed?.processSpec;
117127
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
117128
- const spec = raw;
117129
- const stages = (Array.isArray(spec.stages) ? spec.stages : []).map((s) => {
117130
- if (typeof s === "string" && s.trim()) return { label: s.trim() };
117131
- if (s && typeof s === "object" && !Array.isArray(s)) {
117132
- const o = s;
117133
- if (typeof o.label === "string" && o.label.trim()) {
117134
- return {
117135
- label: o.label.trim(),
117136
- ...typeof o.id === "string" && o.id.trim() ? { id: o.id.trim() } : {},
117137
- ...typeof o.detail === "string" && o.detail.trim() ? { detail: o.detail.trim() } : {}
117138
- };
117139
- }
117140
- }
117141
- return null;
117142
- }).filter((s) => s !== null);
117143
- const strings = (v) => (Array.isArray(v) ? v : []).filter((x) => typeof x === "string" && x.trim() !== "");
117144
- const out = {
117145
- stages,
117146
- inputs: strings(spec.inputs),
117147
- outputs: strings(spec.outputs),
117148
- ...typeof spec.title === "string" && spec.title.trim() ? { title: spec.title.trim() } : {}
117149
- };
117150
- return stages.length || out.inputs.length || out.outputs.length ? out : null;
117151
- }
117152
- function defaultDashboardManifest() {
117153
- return JSON.stringify({ files: [...DEFAULT_DASHBOARD_FILES] });
117154
- }
117155
- function embedJson(value2) {
117156
- return JSON.stringify(value2).replace(/</g, "\\u003c");
117157
- }
117158
- function escapeHtml(s) {
117159
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
117160
- }
117161
- function renderLoopDashboardTemplate(args) {
117162
- const title = escapeHtml(args.processSpec?.title ?? args.slug);
117163
- const seed = embedJson({
117164
- slug: args.slug,
117165
- descriptionShort: args.descriptionShort ?? "",
117166
- processSpec: args.processSpec ?? null
117167
- });
117168
- return `<!doctype html>
117169
- <html lang="en">
117170
- <head>
117171
- <meta charset="utf-8">
117172
- <meta name="viewport" content="width=device-width, initial-scale=1">
117173
- <title>${title} \u2014 loop dashboard</title>
117174
- <style>
117175
- :root { --bg:#f7f7fb; --panel:#fff; --border:#e3e3ee; --muted:#6b6b80; --accent:#5b5bd6; --ok:#1a7f37; --bad:#b42318; }
117176
- * { box-sizing: border-box; }
117177
- body { margin:0; font:14px/1.5 -apple-system, "Segoe UI", Roboto, sans-serif; background:var(--bg); color:#1d1d2b; padding:24px; }
117178
- h1 { font-size:20px; margin:0 0 4px; }
117179
- h2 { font-size:14px; margin:24px 0 8px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
117180
- .sub { color:var(--muted); margin:0 0 16px; }
117181
- .panel { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px; }
117182
- .map { display:flex; flex-wrap:wrap; align-items:stretch; gap:8px; }
117183
- .stage { background:var(--panel); border:1px solid var(--border); border-left:3px solid var(--accent); border-radius:10px; padding:10px 14px; min-width:140px; flex:1; }
117184
- .stage .label { font-weight:600; }
117185
- .stage .detail { color:var(--muted); font-size:12px; margin-top:2px; }
117186
- .arrow { align-self:center; color:var(--muted); }
117187
- .io { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
117188
- ul { margin:6px 0 0; padding-left:18px; }
117189
- table { width:100%; border-collapse:collapse; background:var(--panel); border:1px solid var(--border); border-radius:12px; overflow:hidden; }
117190
- th, td { text-align:left; padding:8px 12px; border-top:1px solid var(--border); font-size:13px; vertical-align:top; }
117191
- thead th { border-top:none; background:var(--bg); color:var(--muted); font-weight:600; }
117192
- .ok { color:var(--ok); } .bad { color:var(--bad); }
117193
- details { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:10px 14px; margin-bottom:8px; }
117194
- summary { cursor:pointer; font-weight:600; }
117195
- pre { overflow-x:auto; font-size:12px; background:var(--bg); border-radius:8px; padding:10px; }
117196
- .empty { color:var(--muted); font-style:italic; }
117197
- #live { font-size:12px; color:var(--muted); float:right; }
117198
- </style>
117199
- </head>
117200
- <body>
117201
- <span id="live">loading\u2026</span>
117202
- <h1 id="title"></h1>
117203
- <p class="sub" id="desc"></p>
117204
-
117205
- <h2>Process</h2>
117206
- <div id="map" class="map panel"></div>
117207
-
117208
- <h2>Inputs &amp; outputs</h2>
117209
- <div class="io">
117210
- <div class="panel"><strong>Inputs</strong><ul id="inputs"></ul></div>
117211
- <div class="panel"><strong>Outputs</strong><ul id="outputs"></ul></div>
117212
- </div>
117213
-
117214
- <h2>Rounds</h2>
117215
- <div id="rounds"></div>
117216
-
117217
- <h2>Files</h2>
117218
- <div id="files"></div>
117219
-
117220
- <script type="application/json" id="loop-seed">${seed}</script>
117221
- <script>
117222
- (function () {
117223
- "use strict";
117224
- var seed = JSON.parse(document.getElementById("loop-seed").textContent);
117225
- document.getElementById("title").textContent = (seed.processSpec && seed.processSpec.title) || seed.slug;
117226
- document.getElementById("desc").textContent = seed.descriptionShort || "";
117227
- document.title = seed.slug + " \u2014 loop dashboard";
117228
-
117229
- function el(tag, cls, text) {
117230
- var e = document.createElement(tag);
117231
- if (cls) e.className = cls;
117232
- if (text !== undefined) e.textContent = text;
117233
- return e;
117234
- }
117235
-
117236
- // \u2500\u2500 Process map + IO from the embedded spec (static \u2014 rendered once). \u2500\u2500
117237
- var map = document.getElementById("map");
117238
- var spec = seed.processSpec;
117239
- if (spec && spec.stages && spec.stages.length) {
117240
- spec.stages.forEach(function (s, i) {
117241
- if (i > 0) map.appendChild(el("div", "arrow", "\\u2192"));
117242
- var box = el("div", "stage");
117243
- box.appendChild(el("div", "label", s.label));
117244
- if (s.detail) box.appendChild(el("div", "detail", s.detail));
117245
- map.appendChild(box);
117246
- });
117247
- } else {
117248
- map.appendChild(el("span", "empty", "No process spec on this loop \\u2014 live files and rounds below."));
117249
- }
117250
- function fillList(id, items) {
117251
- var ul = document.getElementById(id);
117252
- ul.textContent = "";
117253
- if (!items || !items.length) { ul.appendChild(el("li", "empty", "\\u2014")); return; }
117254
- items.forEach(function (x) { ul.appendChild(el("li", null, x)); });
117255
- }
117256
- fillList("inputs", spec && spec.inputs);
117257
- fillList("outputs", spec && spec.outputs);
117258
-
117259
- // \u2500\u2500 Live data: rounds + files, refreshed from GET /data. \u2500\u2500
117260
- function parseJsonl(text) {
117261
- var rows = [];
117262
- (text || "").split("\\n").forEach(function (line) {
117263
- line = line.trim();
117264
- if (!line) return;
117265
- try { rows.push(JSON.parse(line)); } catch (e) { /* skip malformed line */ }
117266
- });
117267
- return rows;
117268
- }
117269
-
117270
- function renderRounds(fires, rounds) {
117271
- var host = document.getElementById("rounds");
117272
- host.textContent = "";
117273
- if (!fires.length && !rounds.length) {
117274
- var p = el("div", "panel"); p.appendChild(el("span", "empty", "No rounds yet \\u2014 this fills in as the loop runs."));
117275
- host.appendChild(p);
117276
- return;
117277
- }
117278
- var table = document.createElement("table");
117279
- var thead = document.createElement("thead");
117280
- var hr = document.createElement("tr");
117281
- ["when", "duration", "turns", "tokens", "exit", "round detail (rounds.jsonl)"].forEach(function (h) {
117282
- hr.appendChild(el("th", null, h));
117283
- });
117284
- thead.appendChild(hr); table.appendChild(thead);
117285
- var tbody = document.createElement("tbody");
117286
- var n = Math.max(fires.length, rounds.length);
117287
- for (var i = n - 1; i >= 0; i--) { // newest first
117288
- var f = fires[i] || {};
117289
- var r = rounds[i];
117290
- var tr = document.createElement("tr");
117291
- tr.appendChild(el("td", null, f.ts || (r && r.ts) || "\\u2014"));
117292
- tr.appendChild(el("td", null, f.duration_s != null ? f.duration_s + "s" : "\\u2014"));
117293
- tr.appendChild(el("td", null, f.turns != null ? String(f.turns) : "\\u2014"));
117294
- tr.appendChild(el("td", null, f.tokens_total != null ? Number(f.tokens_total).toLocaleString() : "\\u2014"));
117295
- tr.appendChild(el("td", f.exit === 0 ? "ok" : f.exit != null ? "bad" : null, f.exit != null ? String(f.exit) : "\\u2014"));
117296
- var detail = el("td");
117297
- if (f.launch_failed) { detail.textContent = "launch failed \\u2014 " + (f.reason || "unknown reason"); }
117298
- else if (r) { var pre = document.createElement("pre"); pre.textContent = JSON.stringify(r, null, 1); detail.appendChild(pre); }
117299
- else detail.textContent = "\\u2014";
117300
- tr.appendChild(detail);
117301
- tbody.appendChild(tr);
117302
- }
117303
- table.appendChild(tbody);
117304
- host.appendChild(table);
117305
- }
117306
-
117307
- function renderFiles(files) {
117308
- var host = document.getElementById("files");
117309
- host.textContent = "";
117310
- var names = Object.keys(files || {}).filter(function (n) { return n !== "rounds.jsonl"; });
117311
- if (!names.length) { var p = el("div", "panel"); p.appendChild(el("span", "empty", "No files surfaced by the manifest.")); host.appendChild(p); return; }
117312
- names.forEach(function (name) {
117313
- var d = document.createElement("details");
117314
- d.appendChild(el("summary", null, name));
117315
- var pre = document.createElement("pre");
117316
- pre.textContent = files[name];
117317
- d.appendChild(pre);
117318
- host.appendChild(d);
117319
- });
117320
- }
117321
-
117322
- function refresh() {
117323
- fetch("/data").then(function (res) { return res.json(); }).then(function (data) {
117324
- var fires = parseJsonl(data.state && data.state["fires.jsonl"]);
117325
- var rounds = parseJsonl(data.files && data.files["rounds.jsonl"]);
117326
- renderRounds(fires, rounds);
117327
- renderFiles(data.files);
117328
- document.getElementById("live").textContent = "updated " + new Date().toLocaleTimeString();
117329
- }).catch(function () {
117330
- document.getElementById("live").textContent = "server stopped";
117331
- });
117332
- }
117333
- refresh();
117334
- setInterval(refresh, 5000);
117335
- })();
117336
- </script>
117337
- </body>
117338
- </html>
117339
- `;
117340
- }
117341
- function injectDefaultDashboard(loop2) {
117342
- const hasHtml = typeof loop2.dashboardHtml === "string" && loop2.dashboardHtml.trim() !== "";
117343
- if (hasHtml) return;
117344
- if (loop2.dashboardManifest && typeof loop2.dashboardManifest === "object") {
117345
- try {
117346
- loop2.dashboardManifest = JSON.stringify(loop2.dashboardManifest);
117347
- } catch {
117348
- }
117349
- }
117350
- const manifest = typeof loop2.dashboardManifest === "string" ? loop2.dashboardManifest : void 0;
117351
- loop2.dashboardHtml = renderLoopDashboardTemplate({
117352
- slug: typeof loop2.slug === "string" ? loop2.slug : "loop",
117353
- descriptionShort: typeof loop2.descriptionShort === "string" ? loop2.descriptionShort : void 0,
117354
- processSpec: parseProcessSpec(manifest)
117355
- });
117356
- if (!manifest || !manifest.trim()) loop2.dashboardManifest = defaultDashboardManifest();
117357
- }
117358
-
117359
- // src/loops/state-docs.ts
117360
- init_esm_shims();
117361
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
117362
- import { join as join9 } from "path";
117363
-
117364
- // src/loops/shipback.ts
117365
- init_esm_shims();
117366
- function scanHeadings(text) {
117367
- return scanDoc(text).items;
117368
- }
117369
- function scanDoc(text) {
117370
- const out = [];
117371
- let offset = 0;
117372
- let inFence = false;
117373
- for (const line of text.split("\n")) {
117374
- const bare = line.endsWith("\r") ? line.slice(0, -1) : line;
117375
- const trimmed = bare.trimStart();
117376
- if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) inFence = !inFence;
117377
- else if (!inFence) {
117378
- const m = /^###\s+(.*)$/.exec(bare);
117379
- if (m) out.push({ heading: m[1].trim(), start: offset });
117380
- }
117381
- offset += line.length + 1;
117382
- }
117383
- return { items: out, unbalanced: inFence };
117384
- }
117385
- function headingsIgnoringFences(text) {
117386
- const out = [];
117387
- for (const line of text.split("\n")) {
117388
- const bare = line.endsWith("\r") ? line.slice(0, -1) : line;
117389
- const m = /^ {0,3}###\s+(.*)$/.exec(bare);
117390
- if (m) out.push(m[1].trim());
117391
- }
117392
- return out;
117393
- }
117394
- function headings(text) {
117395
- return scanHeadings(text).map((h) => h.heading);
117396
- }
117397
- function countByHeading(list2) {
117398
- const m = /* @__PURE__ */ new Map();
117399
- for (const h of list2) m.set(h, (m.get(h) ?? 0) + 1);
117400
- return m;
117401
- }
117402
- function sections(fragment) {
117403
- const found = scanHeadings(fragment);
117404
- if (found.length === 0) return { preamble: fragment, blocks: [] };
117405
- const preamble = fragment.slice(0, found[0].start);
117406
- const blocks = found.map((h, i) => ({
117407
- heading: h.heading,
117408
- text: fragment.slice(h.start, i + 1 < found.length ? found[i + 1].start : fragment.length)
117409
- }));
117410
- return { preamble, blocks };
117411
- }
117412
- function norm(s) {
117413
- return s.replace(/\r\n/g, "\n").trimEnd();
116948
+ function norm(s) {
116949
+ return s.replace(/\r\n/g, "\n").trimEnd();
117414
116950
  }
117415
116951
  function mergeConstraints(boot, materialized, fresh) {
117416
116952
  if (materialized === boot) return { next: void 0, added: [], addedBlocks: [], cleanAppend: true };
@@ -117503,357 +117039,898 @@ function isSafeKey(key) {
117503
117039
  if (key.includes("/") || key.includes("\\") || key.includes("\0")) return false;
117504
117040
  return true;
117505
117041
  }
117506
- function normalizeMemoryScope(scope) {
117507
- switch (scope) {
117508
- case "shared":
117509
- case "workspace":
117510
- return "shared";
117511
- case "member":
117512
- case "profile":
117513
- return "member";
117514
- default:
117515
- return void 0;
117042
+ function normalizeMemoryScope(scope) {
117043
+ switch (scope) {
117044
+ case "shared":
117045
+ case "workspace":
117046
+ return "shared";
117047
+ case "member":
117048
+ case "profile":
117049
+ return "member";
117050
+ default:
117051
+ return void 0;
117052
+ }
117053
+ }
117054
+ function validateMemoryManifest(value2, reserved) {
117055
+ if (value2 === null) return { ok: true, manifest: null };
117056
+ if (Array.isArray(value2)) {
117057
+ return {
117058
+ ok: false,
117059
+ error: 'stateDocs is an ARRAY \u2014 that is the retired declaration shape. The memory manifest is an object: {"docs":[{"key":"\u2026","scope":"shared|member"}],"records":[{"kind":"run|decision|error|import|<free kind>","scope":"shared|member"}]}. Send "null" to clear, or the object form.'
117060
+ };
117061
+ }
117062
+ if (typeof value2 !== "object") {
117063
+ return { ok: false, error: "stateDocs must be a JSON object with `docs` and `records` arrays, or null." };
117064
+ }
117065
+ const obj = value2;
117066
+ const extra = Object.keys(obj).filter((k) => k !== "docs" && k !== "records");
117067
+ if (extra.length > 0) {
117068
+ return { ok: false, error: `stateDocs carries unknown propert${extra.length === 1 ? "y" : "ies"} ${extra.join(", ")} \u2014 only \`docs\` and \`records\` are allowed.` };
117069
+ }
117070
+ if (!Array.isArray(obj.docs) || !Array.isArray(obj.records)) {
117071
+ return { ok: false, error: "stateDocs needs BOTH `docs` and `records` arrays (either may be empty)." };
117072
+ }
117073
+ if (obj.docs.length > MEMORY_MANIFEST_MAX_DOCS) {
117074
+ return { ok: false, error: `stateDocs.docs holds ${obj.docs.length} entries \u2014 the maximum is ${MEMORY_MANIFEST_MAX_DOCS}.` };
117075
+ }
117076
+ if (obj.records.length > MEMORY_MANIFEST_MAX_RECORDS) {
117077
+ return { ok: false, error: `stateDocs.records holds ${obj.records.length} entries \u2014 the maximum is ${MEMORY_MANIFEST_MAX_RECORDS}.` };
117078
+ }
117079
+ const docs = [];
117080
+ const seenKeys = /* @__PURE__ */ new Set();
117081
+ for (const [i, raw] of obj.docs.entries()) {
117082
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
117083
+ return { ok: false, error: `stateDocs.docs[${i}] must be an object {key, scope}.` };
117084
+ }
117085
+ const d = raw;
117086
+ const extraDoc = Object.keys(d).filter((k) => k !== "key" && k !== "scope");
117087
+ if (extraDoc.length > 0) {
117088
+ return { ok: false, error: `stateDocs.docs[${i}] carries unknown propert${extraDoc.length === 1 ? "y" : "ies"} ${extraDoc.join(", ")} \u2014 a doc is exactly {key, scope}.` };
117089
+ }
117090
+ if (typeof d.key !== "string" || !isSafeKey(d.key)) {
117091
+ return { ok: false, error: `stateDocs.docs[${i}].key must be a safe basename of 1..${KEY_MAX_LENGTH} chars (no \`/\`, \`\\\`, NUL, no leading dot).` };
117092
+ }
117093
+ if (isReservedManifestKey(d.key, reserved)) {
117094
+ return { ok: false, error: `stateDocs.docs[${i}].key "${d.key}" names a file the runner writes itself (${reserved.join(", ")}) \u2014 pick another key.` };
117095
+ }
117096
+ const lower = d.key.toLowerCase();
117097
+ if (seenKeys.has(lower)) {
117098
+ return { ok: false, error: `stateDocs.docs has "${d.key}" more than once (keys are unique case-insensitively).` };
117099
+ }
117100
+ seenKeys.add(lower);
117101
+ if (d.scope !== "shared" && d.scope !== "member") {
117102
+ return { ok: false, error: `stateDocs.docs[${i}].scope must be "shared" or "member".` };
117103
+ }
117104
+ docs.push({ key: d.key, scope: d.scope });
117105
+ }
117106
+ const records = [];
117107
+ const seenKinds = /* @__PURE__ */ new Set();
117108
+ for (const [i, raw] of obj.records.entries()) {
117109
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
117110
+ return { ok: false, error: `stateDocs.records[${i}] must be an object {kind, scope}.` };
117111
+ }
117112
+ const r = raw;
117113
+ const extraRec = Object.keys(r).filter((k) => k !== "kind" && k !== "scope");
117114
+ if (extraRec.length > 0) {
117115
+ return { ok: false, error: `stateDocs.records[${i}] carries unknown propert${extraRec.length === 1 ? "y" : "ies"} ${extraRec.join(", ")} \u2014 a record is exactly {kind, scope}.` };
117116
+ }
117117
+ if (!isRecordKindToken(r.kind)) {
117118
+ return {
117119
+ ok: false,
117120
+ error: `stateDocs.records[${i}].kind must be a token matching [a-z0-9-]{1,64} \u2014 a reserved kind (${RESERVED_RECORD_KINDS.join(", ")}) or a free kind of the task's own (e.g. "pbi").`
117121
+ };
117122
+ }
117123
+ if (seenKinds.has(r.kind)) {
117124
+ return { ok: false, error: `stateDocs.records declares "${r.kind}" more than once.` };
117125
+ }
117126
+ seenKinds.add(r.kind);
117127
+ if (r.scope !== "shared" && r.scope !== "member") {
117128
+ return { ok: false, error: `stateDocs.records[${i}].scope must be "shared" or "member".` };
117129
+ }
117130
+ records.push({ kind: r.kind, scope: r.scope });
117131
+ }
117132
+ return { ok: true, manifest: { docs, records } };
117133
+ }
117134
+ function normalizeMemoryManifest(manifest) {
117135
+ if (!manifest || typeof manifest !== "object") return null;
117136
+ const docs = [];
117137
+ for (const d of Array.isArray(manifest.docs) ? manifest.docs : []) {
117138
+ const scope = normalizeMemoryScope(d?.scope);
117139
+ if (typeof d?.key !== "string" || d.key.length === 0 || !scope) continue;
117140
+ docs.push({ key: d.key, scope });
117141
+ }
117142
+ const records = [];
117143
+ for (const r of Array.isArray(manifest.records) ? manifest.records : []) {
117144
+ const scope = normalizeMemoryScope(r?.scope);
117145
+ if (!isRecordKindToken(r?.kind) || !scope) continue;
117146
+ records.push({ kind: r.kind, scope });
117147
+ }
117148
+ return { docs, records };
117149
+ }
117150
+ async function readTaskManifest(sdk, apiKey, slug) {
117151
+ try {
117152
+ const got = await sdk.tasks.get(apiKey, slug);
117153
+ if (got?.status !== "ok") return void 0;
117154
+ return normalizeMemoryManifest(got.task?.stateDocs ?? null);
117155
+ } catch {
117156
+ return void 0;
117157
+ }
117158
+ }
117159
+ function declaredDocScope(manifest, key) {
117160
+ return manifest?.docs.find((d) => d.key.toLowerCase() === key.toLowerCase())?.scope;
117161
+ }
117162
+ function declaredRecordScope(manifest, kind) {
117163
+ return manifest?.records.find((r) => r.kind === kind)?.scope;
117164
+ }
117165
+ function declaredRecordKinds(manifest) {
117166
+ if (manifest === void 0) return void 0;
117167
+ return manifest?.records.map((r) => r.kind) ?? [];
117168
+ }
117169
+ function formatMemoryManifest(manifest) {
117170
+ const normalized = normalizeMemoryManifest(manifest);
117171
+ if (!normalized) return "(none)";
117172
+ const lines = [
117173
+ ...normalized.docs.map((d) => `doc ${d.key} \xB7 ${d.scope}`),
117174
+ ...normalized.records.map((r) => `record ${r.kind} \xB7 ${r.scope}`)
117175
+ ];
117176
+ return lines.length ? lines.join("\n") : "(declared empty: no docs, no records)";
117177
+ }
117178
+
117179
+ // src/loops/state-docs.ts
117180
+ var RESERVED_FIRE_FILENAMES = [
117181
+ "SKILL.md",
117182
+ "VISION.md",
117183
+ "CONSTRAINTS.md",
117184
+ "README.md",
117185
+ "STATUS.md",
117186
+ "dashboard.html",
117187
+ "dashboard.manifest.json"
117188
+ ];
117189
+ var KEY_EXTENSION = /\.[A-Za-z0-9]{1,8}$/;
117190
+ function filenameForKey(key) {
117191
+ return KEY_EXTENSION.test(key) ? key : `${key}.md`;
117192
+ }
117193
+ function isSafeBasename(filename) {
117194
+ if (filename.length === 0) return false;
117195
+ if (filename.startsWith(".")) return false;
117196
+ if (filename.includes("/") || filename.includes("\\")) return false;
117197
+ if (filename === ".." || filename.includes("\0")) return false;
117198
+ return true;
117199
+ }
117200
+ function isReservedName(filename) {
117201
+ return RESERVED_FIRE_FILENAMES.some((r) => r.toLowerCase() === filename.toLowerCase());
117202
+ }
117203
+ var DOC_LIST_LIMIT = 500;
117204
+ async function discoverMemoryDocs(sdk, apiKey, slug) {
117205
+ const out = { own: [], shared: [], failed: [], truncated: [] };
117206
+ for (const scope of ["member", "shared"]) {
117207
+ const res = await sdk.loops.memory.listDocs(apiKey, slug, { scope, limit: DOC_LIST_LIMIT });
117208
+ if (res.status !== "ok") {
117209
+ if (scope === "member" && isNotFound(res)) continue;
117210
+ out.failed.push(scope);
117211
+ continue;
117212
+ }
117213
+ if (res.items.length >= DOC_LIST_LIMIT) out.truncated.push(scope);
117214
+ if (scope === "member") out.own = res.items;
117215
+ else out.shared = res.items;
117516
117216
  }
117217
+ return out;
117517
117218
  }
117518
- function validateMemoryManifest(value2, reserved) {
117519
- if (value2 === null) return { ok: true, manifest: null };
117520
- if (Array.isArray(value2)) {
117521
- return {
117522
- ok: false,
117523
- error: 'stateDocs is an ARRAY \u2014 that is the retired declaration shape. The memory manifest is an object: {"docs":[{"key":"\u2026","scope":"shared|member"}],"records":[{"kind":"run|decision|error|import|<free kind>","scope":"shared|member"}]}. Send "null" to clear, or the object form.'
117524
- };
117219
+ function planStateDocs(stateDocs, discovered) {
117220
+ const manifest = normalizeMemoryManifest(stateDocs);
117221
+ const candidates = [];
117222
+ const claimed = /* @__PURE__ */ new Set();
117223
+ for (const d of manifest?.docs ?? []) {
117224
+ claimed.add(d.key);
117225
+ candidates.push({ key: d.key, scope: d.scope, declared: true });
117525
117226
  }
117526
- if (typeof value2 !== "object") {
117527
- return { ok: false, error: "stateDocs must be a JSON object with `docs` and `records` arrays, or null." };
117227
+ for (const [rows, scope] of [
117228
+ [discovered.own, "member"],
117229
+ [discovered.shared, "shared"]
117230
+ ]) {
117231
+ for (const m of rows) {
117232
+ if (claimed.has(m.key)) continue;
117233
+ claimed.add(m.key);
117234
+ candidates.push({ key: m.key, scope, declared: false });
117235
+ }
117528
117236
  }
117529
- const obj = value2;
117530
- const extra = Object.keys(obj).filter((k) => k !== "docs" && k !== "records");
117531
- if (extra.length > 0) {
117532
- return { ok: false, error: `stateDocs carries unknown propert${extra.length === 1 ? "y" : "ies"} ${extra.join(", ")} \u2014 only \`docs\` and \`records\` are allowed.` };
117237
+ const docs = [];
117238
+ const skipped = [];
117239
+ const taken = /* @__PURE__ */ new Map();
117240
+ for (const c of candidates) {
117241
+ const filename = filenameForKey(c.key);
117242
+ if (!isSafeBasename(filename)) {
117243
+ skipped.push({ key: c.key, reason: `'${filename}' is not a safe basename` });
117244
+ continue;
117245
+ }
117246
+ if (isReservedName(filename)) {
117247
+ skipped.push({
117248
+ key: c.key,
117249
+ reason: `'${filename}' is a file the wrapper writes itself \u2014 rename the key`
117250
+ });
117251
+ continue;
117252
+ }
117253
+ const owner = taken.get(filename.toLowerCase());
117254
+ if (owner !== void 0) {
117255
+ skipped.push({ key: c.key, reason: `'${filename}' is already taken by key '${owner}'` });
117256
+ continue;
117257
+ }
117258
+ taken.set(filename.toLowerCase(), c.key);
117259
+ docs.push({ key: c.key, filename, scope: c.scope, declared: c.declared });
117533
117260
  }
117534
- if (!Array.isArray(obj.docs) || !Array.isArray(obj.records)) {
117535
- return { ok: false, error: "stateDocs needs BOTH `docs` and `records` arrays (either may be empty)." };
117261
+ return { docs, skipped, manifest };
117262
+ }
117263
+ function describeSource(doc) {
117264
+ const from14 = doc.scope === "shared" ? "shared" : "own";
117265
+ const notes = [];
117266
+ if (doc.created) notes.push(doc.seeded ? "created, seeded from shared" : "created");
117267
+ if (!doc.declared) notes.push("not in manifest");
117268
+ return `${doc.filename} \u2190 ${from14}${notes.length ? ` (${notes.join(", ")})` : ""}`;
117269
+ }
117270
+ async function readScoped(sdk, apiKey, slug, key, scope) {
117271
+ const res = await sdk.loops.memory.getDoc(apiKey, slug, key, { scope });
117272
+ if (res.status === "ok") return { status: "ok", body: res.doc.content ?? "" };
117273
+ if (isNotFound(res)) return { status: "absent" };
117274
+ return { status: "failed" };
117275
+ }
117276
+ async function fetchStateDocs(sdk, apiKey, slug, docs) {
117277
+ const fetched = [];
117278
+ const failed = [];
117279
+ for (const doc of docs) {
117280
+ const read = await readScoped(sdk, apiKey, slug, doc.key, doc.scope);
117281
+ if (read.status === "ok") {
117282
+ fetched.push({ ...doc, body: read.body, created: false, seeded: false });
117283
+ continue;
117284
+ }
117285
+ if (read.status === "failed") {
117286
+ failed.push({ key: doc.key, reason: `could not read the ${doc.scope} row` });
117287
+ continue;
117288
+ }
117289
+ if (!doc.declared) {
117290
+ failed.push({ key: doc.key, reason: `the ${doc.scope} row vanished after the listing` });
117291
+ continue;
117292
+ }
117293
+ let seed = "";
117294
+ let seeded = false;
117295
+ if (doc.scope === "member") {
117296
+ const shared = await readScoped(sdk, apiKey, slug, doc.key, "shared");
117297
+ if (shared.status === "failed") {
117298
+ failed.push({ key: doc.key, reason: "could not read the shared row to seed the member row" });
117299
+ continue;
117300
+ }
117301
+ if (shared.status === "ok") {
117302
+ seed = shared.body;
117303
+ seeded = true;
117304
+ }
117305
+ }
117306
+ const put = await sdk.loops.memory.putDoc(apiKey, slug, doc.key, { content: seed, scope: doc.scope });
117307
+ if (put.status !== "ok") {
117308
+ failed.push({ key: doc.key, reason: `could not create the ${doc.scope} row (${put.error ?? "write refused"})` });
117309
+ continue;
117310
+ }
117311
+ fetched.push({ ...doc, body: seed, created: true, seeded });
117536
117312
  }
117537
- if (obj.docs.length > MEMORY_MANIFEST_MAX_DOCS) {
117538
- return { ok: false, error: `stateDocs.docs holds ${obj.docs.length} entries \u2014 the maximum is ${MEMORY_MANIFEST_MAX_DOCS}.` };
117313
+ return { fetched, failed };
117314
+ }
117315
+ function isNotFound(res) {
117316
+ if (typeof res !== "object" || res === null) return false;
117317
+ return res.code === 404;
117318
+ }
117319
+ function docMergeRefusal(boot, fresh, next, cleanAppend) {
117320
+ if (next.trim() === "" && fresh.trim() !== "") {
117321
+ return "would TRUNCATE the stored document to empty while the record still holds content \u2014 an emptied file is not an instruction to erase the record";
117539
117322
  }
117540
- if (obj.records.length > MEMORY_MANIFEST_MAX_RECORDS) {
117541
- return { ok: false, error: `stateDocs.records holds ${obj.records.length} entries \u2014 the maximum is ${MEMORY_MANIFEST_MAX_RECORDS}.` };
117323
+ if (!cleanAppend && fresh !== boot && headings(fresh).length === 0) {
117324
+ return "this fire rewrote the document rather than appending to it, the record MOVED under us, and the document has no headings for the shrink guard to count \u2014 so accepting this write would silently discard whatever the concurrent writer added";
117542
117325
  }
117543
- const docs = [];
117544
- const seenKeys = /* @__PURE__ */ new Set();
117545
- for (const [i, raw] of obj.docs.entries()) {
117546
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
117547
- return { ok: false, error: `stateDocs.docs[${i}] must be an object {key, scope}.` };
117326
+ return void 0;
117327
+ }
117328
+ async function shipBackStateDocs(sdk, apiKey, slug, dir, boot) {
117329
+ const outcomes = [];
117330
+ for (const doc of boot) {
117331
+ const path2 = join7(dir, doc.filename);
117332
+ if (!existsSync9(path2)) {
117333
+ outcomes.push({ key: doc.key, outcome: "unchanged" });
117334
+ continue;
117548
117335
  }
117549
- const d = raw;
117550
- const extraDoc = Object.keys(d).filter((k) => k !== "key" && k !== "scope");
117551
- if (extraDoc.length > 0) {
117552
- return { ok: false, error: `stateDocs.docs[${i}] carries unknown propert${extraDoc.length === 1 ? "y" : "ies"} ${extraDoc.join(", ")} \u2014 a doc is exactly {key, scope}.` };
117336
+ const materialized = readFileSync8(path2, "utf-8");
117337
+ const reread = await readScoped(sdk, apiKey, slug, doc.key, doc.scope);
117338
+ const fresh = reread.status === "ok" ? reread.body : reread.status === "absent" && doc.body === "" ? "" : void 0;
117339
+ if (fresh === void 0) {
117340
+ outcomes.push({
117341
+ key: doc.key,
117342
+ outcome: "failed",
117343
+ detail: `could not re-read the ${doc.scope} row; ${path2} kept for recovery`
117344
+ });
117345
+ continue;
117553
117346
  }
117554
- if (typeof d.key !== "string" || !isSafeKey(d.key)) {
117555
- return { ok: false, error: `stateDocs.docs[${i}].key must be a safe basename of 1..${KEY_MAX_LENGTH} chars (no \`/\`, \`\\\`, NUL, no leading dot).` };
117347
+ const merged = mergeConstraints(doc.body, materialized, fresh);
117348
+ if (merged.refusal) {
117349
+ outcomes.push({ key: doc.key, outcome: "refused", detail: merged.refusal });
117350
+ continue;
117556
117351
  }
117557
- if (isReservedManifestKey(d.key, reserved)) {
117558
- return { ok: false, error: `stateDocs.docs[${i}].key "${d.key}" names a file the runner writes itself (${reserved.join(", ")}) \u2014 pick another key.` };
117352
+ if (merged.next === void 0) {
117353
+ outcomes.push({ key: doc.key, outcome: "unchanged" });
117354
+ continue;
117559
117355
  }
117560
- const lower = d.key.toLowerCase();
117561
- if (seenKeys.has(lower)) {
117562
- return { ok: false, error: `stateDocs.docs has "${d.key}" more than once (keys are unique case-insensitively).` };
117356
+ const docRefusal = docMergeRefusal(doc.body, fresh, merged.next, merged.cleanAppend);
117357
+ if (docRefusal) {
117358
+ outcomes.push({
117359
+ key: doc.key,
117360
+ outcome: "refused",
117361
+ detail: `${docRefusal}; ${path2} kept for recovery`
117362
+ });
117363
+ continue;
117563
117364
  }
117564
- seenKeys.add(lower);
117565
- if (d.scope !== "shared" && d.scope !== "member") {
117566
- return { ok: false, error: `stateDocs.docs[${i}].scope must be "shared" or "member".` };
117365
+ const put = await sdk.loops.memory.putDoc(apiKey, slug, doc.key, {
117366
+ content: merged.next,
117367
+ scope: doc.scope
117368
+ });
117369
+ if (put.status !== "ok") {
117370
+ outcomes.push({
117371
+ key: doc.key,
117372
+ outcome: "failed",
117373
+ detail: `write to the ${doc.scope} row rejected; ${path2} kept for recovery`
117374
+ });
117375
+ continue;
117567
117376
  }
117568
- docs.push({ key: d.key, scope: d.scope });
117377
+ const after = await sdk.loops.memory.getDoc(apiKey, slug, doc.key, { scope: doc.scope });
117378
+ if (after.status !== "ok") {
117379
+ outcomes.push({ key: doc.key, outcome: "shipped", detail: "could not verify it landed" });
117380
+ continue;
117381
+ }
117382
+ const afterText = after.doc.content ?? "";
117383
+ const missing = merged.addedBlocks.length > 0 ? verifyLanded(afterText, merged.addedBlocks) : afterText === merged.next ? [] : ["<rewrite did not survive>"];
117384
+ outcomes.push(
117385
+ missing.length === 0 ? { key: doc.key, outcome: "shipped" } : {
117386
+ key: doc.key,
117387
+ outcome: "failed",
117388
+ detail: `${missing.length} section(s) did not survive a concurrent write; ${path2} kept`
117389
+ }
117390
+ );
117569
117391
  }
117570
- const records = [];
117571
- const seenKinds = /* @__PURE__ */ new Set();
117572
- for (const [i, raw] of obj.records.entries()) {
117573
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
117574
- return { ok: false, error: `stateDocs.records[${i}] must be an object {kind, scope}.` };
117392
+ return outcomes;
117393
+ }
117394
+
117395
+ // src/mcp-server/materialize-task.ts
117396
+ var TASK_DEFINITION_FILES = [
117397
+ ["skill", "SKILL.md", "markdownBody"],
117398
+ ["vision", "VISION.md", "visionMd"],
117399
+ ["constraints", "CONSTRAINTS.md", "constraintsMd"],
117400
+ ["readme", "README.md", "readmeMd"],
117401
+ ["dashboardHtml", "dashboard.html", "dashboardHtml"],
117402
+ ["dashboardManifest", "dashboard.manifest.json", "dashboardManifest"]
117403
+ ];
117404
+ function materializeTask(task, runId) {
117405
+ const dir = loopFireDir(task.slug, runId);
117406
+ mkdirSync6(dir, { recursive: true, mode: 448 });
117407
+ const files = {};
117408
+ const skipped = [];
117409
+ for (const [label2, filename, field] of TASK_DEFINITION_FILES) {
117410
+ const body = task[field];
117411
+ if (typeof body === "string" && body.length > 0) {
117412
+ writeFileSync8(join8(dir, filename), body, { mode: 384 });
117413
+ files[label2] = filename;
117414
+ } else {
117415
+ skipped.push(label2);
117575
117416
  }
117576
- const r = raw;
117577
- const extraRec = Object.keys(r).filter((k) => k !== "kind" && k !== "scope");
117578
- if (extraRec.length > 0) {
117579
- return { ok: false, error: `stateDocs.records[${i}] carries unknown propert${extraRec.length === 1 ? "y" : "ies"} ${extraRec.join(", ")} \u2014 a record is exactly {kind, scope}.` };
117417
+ }
117418
+ writeFileSync8(
117419
+ join8(dir, "STATUS.md"),
117420
+ `# STATUS \u2014 ${task.slug}
117421
+
117422
+ task_id: ${task.id}
117423
+ run_id: ${runId}
117424
+ started: pending
117425
+ queue: not started
117426
+ `,
117427
+ { mode: 384 }
117428
+ );
117429
+ files.status = "STATUS.md";
117430
+ return { dir, files, skipped };
117431
+ }
117432
+ async function materializeMemoryDocs(sdk, apiKey, task, dir) {
117433
+ const files = {};
117434
+ const notes = [];
117435
+ try {
117436
+ const discovered = await discoverMemoryDocs(sdk, apiKey, task.slug);
117437
+ for (const scope of discovered.failed) {
117438
+ notes.push(`could not list the ${scope} memory docs \u2014 undeclared docs there are not materialized`);
117580
117439
  }
117581
- if (!isRecordKindToken(r.kind)) {
117582
- return {
117583
- ok: false,
117584
- error: `stateDocs.records[${i}].kind must be a token matching [a-z0-9-]{1,64} \u2014 a reserved kind (${RESERVED_RECORD_KINDS.join(", ")}) or a free kind of the task's own (e.g. "pbi").`
117585
- };
117440
+ const plan = planStateDocs(task.stateDocs ?? null, discovered);
117441
+ for (const s of plan.skipped) {
117442
+ notes.push(`memory doc '${s.key}' not materialized: ${s.reason}`);
117586
117443
  }
117587
- if (seenKinds.has(r.kind)) {
117588
- return { ok: false, error: `stateDocs.records declares "${r.kind}" more than once.` };
117444
+ if (plan.docs.length === 0) return { files, notes };
117445
+ const { fetched, failed } = await fetchStateDocs(sdk, apiKey, task.slug, plan.docs);
117446
+ for (const f of failed) {
117447
+ notes.push(`memory doc '${f.key}' not materialized: ${f.reason}`);
117589
117448
  }
117590
- seenKinds.add(r.kind);
117591
- if (r.scope !== "shared" && r.scope !== "member") {
117592
- return { ok: false, error: `stateDocs.records[${i}].scope must be "shared" or "member".` };
117449
+ for (const doc of fetched) {
117450
+ writeFileSync8(join8(dir, doc.filename), doc.body, { mode: 384 });
117451
+ files[`doc:${doc.key}`] = doc.filename;
117452
+ notes.push(describeSource(doc));
117593
117453
  }
117594
- records.push({ kind: r.kind, scope: r.scope });
117454
+ return { files, notes };
117455
+ } catch (err) {
117456
+ notes.push(
117457
+ `memory docs not materialized (${err instanceof Error ? err.message : String(err)}) \u2014 the run continues; read them with taskMemoryGet instead`
117458
+ );
117459
+ return { files, notes };
117595
117460
  }
117596
- return { ok: true, manifest: { docs, records } };
117597
117461
  }
117598
- function normalizeMemoryManifest(manifest) {
117599
- if (!manifest || typeof manifest !== "object") return null;
117600
- const docs = [];
117601
- for (const d of Array.isArray(manifest.docs) ? manifest.docs : []) {
117602
- const scope = normalizeMemoryScope(d?.scope);
117603
- if (typeof d?.key !== "string" || d.key.length === 0 || !scope) continue;
117604
- docs.push({ key: d.key, scope });
117462
+
117463
+ // src/mcp-server/index.ts
117464
+ import { existsSync as existsSync12 } from "fs";
117465
+
117466
+ // src/loops/dashboard.ts
117467
+ init_esm_shims();
117468
+ import { createServer as createServer2 } from "http";
117469
+ import * as realFs from "fs";
117470
+ import { spawn as realSpawn } from "child_process";
117471
+ import { join as join9 } from "path";
117472
+ var DEFAULT_PORT = 4477;
117473
+ var DASHBOARD_PORT_ENV = "AMETYST_LOOP_DASHBOARD_PORT";
117474
+ var DASHBOARD_NO_OPEN_ENV = "AMETYST_DASHBOARD_NO_OPEN";
117475
+ var NO_DASHBOARD_MESSAGE = "no dashboard on this task \u2014 ask your agent to create one";
117476
+ var MAX_PORT_RETRIES = 20;
117477
+ var SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
117478
+ function parseManifestFiles(manifest) {
117479
+ if (typeof manifest !== "string" || !manifest.trim()) return [];
117480
+ let parsed;
117481
+ try {
117482
+ parsed = JSON.parse(manifest);
117483
+ } catch {
117484
+ return [];
117605
117485
  }
117606
- const records = [];
117607
- for (const r of Array.isArray(manifest.records) ? manifest.records : []) {
117608
- const scope = normalizeMemoryScope(r?.scope);
117609
- if (!isRecordKindToken(r?.kind) || !scope) continue;
117610
- records.push({ kind: r.kind, scope });
117486
+ const list2 = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.files) ? parsed.files : [];
117487
+ return list2.filter((f) => typeof f === "string" && SAFE_NAME.test(f));
117488
+ }
117489
+ function dashboardPort(env = process.env) {
117490
+ const raw = Number(env[DASHBOARD_PORT_ENV]);
117491
+ return Number.isInteger(raw) && raw > 0 && raw < 65536 ? raw : DEFAULT_PORT;
117492
+ }
117493
+ function openDashboardInBrowser(url2, deps = {}) {
117494
+ const env = deps.env ?? process.env;
117495
+ if (env[DASHBOARD_NO_OPEN_ENV] === "1") return false;
117496
+ const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
117497
+ if (!isTTY) return false;
117498
+ const platform = deps.platform ?? process.platform;
117499
+ const cmd = platform === "darwin" ? "open" : platform === "linux" ? "xdg-open" : null;
117500
+ if (!cmd) return false;
117501
+ const spawn4 = deps.spawn ?? realSpawn;
117502
+ try {
117503
+ const child = spawn4(cmd, [url2], { stdio: "ignore", detached: true });
117504
+ child.once?.("error", () => {
117505
+ });
117506
+ child.unref?.();
117507
+ return true;
117508
+ } catch {
117509
+ return false;
117611
117510
  }
117612
- return { docs, records };
117613
117511
  }
117614
- async function readTaskManifest(sdk, apiKey, slug) {
117512
+ function startDashboardServer(args) {
117513
+ const html = args.loop.dashboardHtml;
117514
+ if (typeof html !== "string" || !html) return Promise.resolve(null);
117515
+ const fs = args.deps?.fs ?? realFs;
117516
+ const log = args.deps?.log ?? ((line) => console.error(line));
117517
+ const make = args.deps?.createServer ?? createServer2;
117518
+ const basePort = args.port ?? dashboardPort();
117519
+ const files = parseManifestFiles(args.loop.dashboardManifest);
117520
+ const handler = (req, res) => {
117521
+ try {
117522
+ const url2 = (req.url ?? "/").split("?")[0];
117523
+ if (req.method === "GET" && url2 === "/") {
117524
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
117525
+ res.end(html);
117526
+ return;
117527
+ }
117528
+ if (req.method === "GET" && url2 === "/data") {
117529
+ const data = {};
117530
+ for (const name of files) {
117531
+ try {
117532
+ const p = join9(args.loopDir, name);
117533
+ if (fs.existsSync(p)) data[name] = fs.readFileSync(p, "utf-8");
117534
+ } catch {
117535
+ }
117536
+ }
117537
+ const state = {};
117538
+ try {
117539
+ const p = join9(args.loopDir, ".state", "fires.jsonl");
117540
+ if (fs.existsSync(p)) state["fires.jsonl"] = fs.readFileSync(p, "utf-8");
117541
+ } catch {
117542
+ }
117543
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
117544
+ res.end(
117545
+ JSON.stringify({
117546
+ loop: args.loop.slug,
117547
+ files: data,
117548
+ state,
117549
+ mode: "live",
117550
+ asOf: (/* @__PURE__ */ new Date()).toISOString()
117551
+ })
117552
+ );
117553
+ return;
117554
+ }
117555
+ res.writeHead(404, { "content-type": "text/plain" });
117556
+ res.end("not found");
117557
+ } catch {
117558
+ try {
117559
+ res.writeHead(500);
117560
+ res.end();
117561
+ } catch {
117562
+ }
117563
+ }
117564
+ };
117565
+ const bind = (port) => new Promise((resolve) => {
117566
+ const server2 = make(handler);
117567
+ server2.once("error", (err) => {
117568
+ try {
117569
+ server2.close();
117570
+ } catch {
117571
+ }
117572
+ resolve({ ok: false, err });
117573
+ });
117574
+ server2.listen(port, "127.0.0.1", () => {
117575
+ server2.unref();
117576
+ resolve({
117577
+ ok: true,
117578
+ handle: {
117579
+ server: server2,
117580
+ port,
117581
+ close() {
117582
+ try {
117583
+ server2.close();
117584
+ server2.closeAllConnections?.();
117585
+ } catch {
117586
+ }
117587
+ }
117588
+ }
117589
+ });
117590
+ });
117591
+ });
117592
+ return (async () => {
117593
+ const lastPort = Math.min(basePort + MAX_PORT_RETRIES, 65535);
117594
+ for (let port = basePort; port <= lastPort; port++) {
117595
+ const attempt = await bind(port);
117596
+ if (attempt.ok) {
117597
+ log(` loop dashboard: http://localhost:${attempt.handle.port}`);
117598
+ return attempt.handle;
117599
+ }
117600
+ if (attempt.err.code !== "EADDRINUSE") {
117601
+ log(
117602
+ ` (loop dashboard not started on :${port} \u2014 ${attempt.err.code ?? attempt.err.message}; run continues)`
117603
+ );
117604
+ return null;
117605
+ }
117606
+ }
117607
+ log(` (loop dashboard not started \u2014 :${basePort}-${lastPort} all in use; run continues)`);
117608
+ return null;
117609
+ })();
117610
+ }
117611
+
117612
+ // src/mcp-server/task-run-mode.ts
117613
+ init_esm_shims();
117614
+ var ACCEPTED_RUN_MODES = ["in-chat", "headless"];
117615
+ var FRONTMATTER_SCAN_LIMIT = 8192;
117616
+ function normalizeRunMode(raw) {
117617
+ if (typeof raw !== "string") return void 0;
117618
+ const v = raw.trim().toLowerCase();
117619
+ if (v === "headless") return "headless";
117620
+ if (v === "in-chat" || v === "inchat" || v === "in_chat") return "in-chat";
117621
+ return void 0;
117622
+ }
117623
+ function describeProvided(raw) {
117624
+ if (typeof raw === "string") return raw.trim();
117615
117625
  try {
117616
- const got = await sdk.tasks.get(apiKey, slug);
117617
- if (got?.status !== "ok") return void 0;
117618
- return normalizeMemoryManifest(got.task?.stateDocs ?? null);
117626
+ return JSON.stringify(raw) ?? String(raw);
117619
117627
  } catch {
117620
- return void 0;
117628
+ return String(raw);
117621
117629
  }
117622
117630
  }
117623
- function declaredDocScope(manifest, key) {
117624
- return manifest?.docs.find((d) => d.key.toLowerCase() === key.toLowerCase())?.scope;
117631
+ function classifyRunModeArgument(raw) {
117632
+ if (raw === void 0 || raw === null) return { kind: "omitted" };
117633
+ if (typeof raw === "string" && raw.trim() === "") return { kind: "omitted" };
117634
+ const mode2 = normalizeRunMode(raw);
117635
+ if (mode2) return { kind: "valid", mode: mode2 };
117636
+ return { kind: "invalid", provided: describeProvided(raw) };
117625
117637
  }
117626
- function declaredRecordScope(manifest, kind) {
117627
- return manifest?.records.find((r) => r.kind === kind)?.scope;
117638
+ function unquoteScalar(raw) {
117639
+ let v = raw.trim();
117640
+ const comment = v.match(/(?:^|\s)#.*$/);
117641
+ if (comment) v = v.slice(0, comment.index === 0 ? 0 : comment.index).trim();
117642
+ if (v.length >= 2 && (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'"))) {
117643
+ v = v.slice(1, -1).trim();
117644
+ }
117645
+ return v;
117628
117646
  }
117629
- function declaredRecordKinds(manifest) {
117630
- if (manifest === void 0) return void 0;
117631
- return manifest?.records.map((r) => r.kind) ?? [];
117647
+ function parseDefaultRunMode(body) {
117648
+ if (typeof body !== "string" || !body) return void 0;
117649
+ const head = body.replace(/^\uFEFF/, "").slice(0, FRONTMATTER_SCAN_LIMIT);
117650
+ const opener = head.match(/^---[ \t]*\r?\n/);
117651
+ if (!opener) return void 0;
117652
+ const rest = head.slice(opener[0].length);
117653
+ const closer = rest.search(/^(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/m);
117654
+ if (closer < 0) return void 0;
117655
+ const block = rest.slice(0, closer);
117656
+ const hit = block.match(/^defaultRunMode[ \t]*:[ \t]*(.*)$/m);
117657
+ if (!hit) return void 0;
117658
+ return normalizeRunMode(unquoteScalar(hit[1] ?? ""));
117659
+ }
117660
+ function resolveRunMode(explicit, body) {
117661
+ const arg = classifyRunModeArgument(explicit);
117662
+ if (arg.kind === "invalid") {
117663
+ return { ok: false, error: "invalid_mode", provided: arg.provided, accepted: ACCEPTED_RUN_MODES };
117664
+ }
117665
+ if (arg.kind === "valid") return { ok: true, mode: arg.mode, source: "explicit" };
117666
+ const fromBody = parseDefaultRunMode(body);
117667
+ if (fromBody) return { ok: true, mode: fromBody, source: "frontmatter" };
117668
+ return { ok: true, mode: "in-chat", source: "default" };
117632
117669
  }
117633
- function formatMemoryManifest(manifest) {
117634
- const normalized = normalizeMemoryManifest(manifest);
117635
- if (!normalized) return "(none)";
117636
- const lines = [
117637
- ...normalized.docs.map((d) => `doc ${d.key} \xB7 ${d.scope}`),
117638
- ...normalized.records.map((r) => `record ${r.kind} \xB7 ${r.scope}`)
117639
- ];
117640
- return lines.length ? lines.join("\n") : "(declared empty: no docs, no records)";
117670
+
117671
+ // src/loops/estimate.ts
117672
+ init_esm_shims();
117673
+ function estimateBlastRadius(loop2) {
117674
+ const g = loop2.graphJson ?? {};
117675
+ const nodes = Array.isArray(g.nodes) ? g.nodes : [];
117676
+ const steps = nodes.length;
117677
+ const paidSteps = nodes.filter(
117678
+ (n) => n?.type === "spend" || n?.data?.paid === true || n?.paid === true
117679
+ ).length;
117680
+ const costHints = nodes.map((n) => Number(n?.data?.estCostEur ?? n?.estCostEur)).filter((x) => !Number.isNaN(x));
117681
+ const estCostEur = costHints.length ? costHints.reduce((a, b) => a + b, 0) : null;
117682
+ return { steps, paidSteps, estCostEur };
117641
117683
  }
117642
117684
 
117643
- // src/loops/state-docs.ts
117644
- var RESERVED_FIRE_FILENAMES = [
117645
- "SKILL.md",
117685
+ // src/loops/dashboard-template.ts
117686
+ init_esm_shims();
117687
+ var DEFAULT_DASHBOARD_FILES = [
117646
117688
  "VISION.md",
117647
117689
  "CONSTRAINTS.md",
117648
- "README.md",
117690
+ "QUEUE.md",
117649
117691
  "STATUS.md",
117650
- "dashboard.html",
117651
- "dashboard.manifest.json"
117692
+ "README.md",
117693
+ "rounds.jsonl"
117652
117694
  ];
117653
- var KEY_EXTENSION = /\.[A-Za-z0-9]{1,8}$/;
117654
- function filenameForKey(key) {
117655
- return KEY_EXTENSION.test(key) ? key : `${key}.md`;
117656
- }
117657
- function isSafeBasename(filename) {
117658
- if (filename.length === 0) return false;
117659
- if (filename.startsWith(".")) return false;
117660
- if (filename.includes("/") || filename.includes("\\")) return false;
117661
- if (filename === ".." || filename.includes("\0")) return false;
117662
- return true;
117663
- }
117664
- function isReservedName(filename) {
117665
- return RESERVED_FIRE_FILENAMES.some((r) => r.toLowerCase() === filename.toLowerCase());
117666
- }
117667
- var DOC_LIST_LIMIT = 500;
117668
- async function discoverMemoryDocs(sdk, apiKey, slug) {
117669
- const out = { own: [], shared: [], failed: [], truncated: [] };
117670
- for (const scope of ["member", "shared"]) {
117671
- const res = await sdk.loops.memory.listDocs(apiKey, slug, { scope, limit: DOC_LIST_LIMIT });
117672
- if (res.status !== "ok") {
117673
- if (scope === "member" && isNotFound(res)) continue;
117674
- out.failed.push(scope);
117675
- continue;
117676
- }
117677
- if (res.items.length >= DOC_LIST_LIMIT) out.truncated.push(scope);
117678
- if (scope === "member") out.own = res.items;
117679
- else out.shared = res.items;
117680
- }
117681
- return out;
117682
- }
117683
- function planStateDocs(stateDocs, discovered) {
117684
- const manifest = normalizeMemoryManifest(stateDocs);
117685
- const candidates = [];
117686
- const claimed = /* @__PURE__ */ new Set();
117687
- for (const d of manifest?.docs ?? []) {
117688
- claimed.add(d.key);
117689
- candidates.push({ key: d.key, scope: d.scope, declared: true });
117690
- }
117691
- for (const [rows, scope] of [
117692
- [discovered.own, "member"],
117693
- [discovered.shared, "shared"]
117694
- ]) {
117695
- for (const m of rows) {
117696
- if (claimed.has(m.key)) continue;
117697
- claimed.add(m.key);
117698
- candidates.push({ key: m.key, scope, declared: false });
117699
- }
117695
+ function parseProcessSpec(manifest) {
117696
+ if (typeof manifest !== "string" || !manifest.trim()) return null;
117697
+ let parsed;
117698
+ try {
117699
+ parsed = JSON.parse(manifest);
117700
+ } catch {
117701
+ return null;
117700
117702
  }
117701
- const docs = [];
117702
- const skipped = [];
117703
- const taken = /* @__PURE__ */ new Map();
117704
- for (const c of candidates) {
117705
- const filename = filenameForKey(c.key);
117706
- if (!isSafeBasename(filename)) {
117707
- skipped.push({ key: c.key, reason: `'${filename}' is not a safe basename` });
117708
- continue;
117709
- }
117710
- if (isReservedName(filename)) {
117711
- skipped.push({
117712
- key: c.key,
117713
- reason: `'${filename}' is a file the wrapper writes itself \u2014 rename the key`
117714
- });
117715
- continue;
117716
- }
117717
- const owner = taken.get(filename.toLowerCase());
117718
- if (owner !== void 0) {
117719
- skipped.push({ key: c.key, reason: `'${filename}' is already taken by key '${owner}'` });
117720
- continue;
117703
+ const raw = parsed?.processSpec;
117704
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
117705
+ const spec = raw;
117706
+ const stages = (Array.isArray(spec.stages) ? spec.stages : []).map((s) => {
117707
+ if (typeof s === "string" && s.trim()) return { label: s.trim() };
117708
+ if (s && typeof s === "object" && !Array.isArray(s)) {
117709
+ const o = s;
117710
+ if (typeof o.label === "string" && o.label.trim()) {
117711
+ return {
117712
+ label: o.label.trim(),
117713
+ ...typeof o.id === "string" && o.id.trim() ? { id: o.id.trim() } : {},
117714
+ ...typeof o.detail === "string" && o.detail.trim() ? { detail: o.detail.trim() } : {}
117715
+ };
117716
+ }
117721
117717
  }
117722
- taken.set(filename.toLowerCase(), c.key);
117723
- docs.push({ key: c.key, filename, scope: c.scope, declared: c.declared });
117724
- }
117725
- return { docs, skipped, manifest };
117718
+ return null;
117719
+ }).filter((s) => s !== null);
117720
+ const strings = (v) => (Array.isArray(v) ? v : []).filter((x) => typeof x === "string" && x.trim() !== "");
117721
+ const out = {
117722
+ stages,
117723
+ inputs: strings(spec.inputs),
117724
+ outputs: strings(spec.outputs),
117725
+ ...typeof spec.title === "string" && spec.title.trim() ? { title: spec.title.trim() } : {}
117726
+ };
117727
+ return stages.length || out.inputs.length || out.outputs.length ? out : null;
117726
117728
  }
117727
- function describeSource(doc) {
117728
- const from14 = doc.scope === "shared" ? "shared" : "own";
117729
- const notes = [];
117730
- if (doc.created) notes.push(doc.seeded ? "created, seeded from shared" : "created");
117731
- if (!doc.declared) notes.push("not in manifest");
117732
- return `${doc.filename} \u2190 ${from14}${notes.length ? ` (${notes.join(", ")})` : ""}`;
117729
+ function defaultDashboardManifest() {
117730
+ return JSON.stringify({ files: [...DEFAULT_DASHBOARD_FILES] });
117733
117731
  }
117734
- async function readScoped(sdk, apiKey, slug, key, scope) {
117735
- const res = await sdk.loops.memory.getDoc(apiKey, slug, key, { scope });
117736
- if (res.status === "ok") return { status: "ok", body: res.doc.content ?? "" };
117737
- if (isNotFound(res)) return { status: "absent" };
117738
- return { status: "failed" };
117732
+ function embedJson(value2) {
117733
+ return JSON.stringify(value2).replace(/</g, "\\u003c");
117739
117734
  }
117740
- async function fetchStateDocs(sdk, apiKey, slug, docs) {
117741
- const fetched = [];
117742
- const failed = [];
117743
- for (const doc of docs) {
117744
- const read = await readScoped(sdk, apiKey, slug, doc.key, doc.scope);
117745
- if (read.status === "ok") {
117746
- fetched.push({ ...doc, body: read.body, created: false, seeded: false });
117747
- continue;
117748
- }
117749
- if (read.status === "failed") {
117750
- failed.push({ key: doc.key, reason: `could not read the ${doc.scope} row` });
117751
- continue;
117752
- }
117753
- if (!doc.declared) {
117754
- failed.push({ key: doc.key, reason: `the ${doc.scope} row vanished after the listing` });
117755
- continue;
117756
- }
117757
- let seed = "";
117758
- let seeded = false;
117759
- if (doc.scope === "member") {
117760
- const shared = await readScoped(sdk, apiKey, slug, doc.key, "shared");
117761
- if (shared.status === "failed") {
117762
- failed.push({ key: doc.key, reason: "could not read the shared row to seed the member row" });
117763
- continue;
117764
- }
117765
- if (shared.status === "ok") {
117766
- seed = shared.body;
117767
- seeded = true;
117768
- }
117769
- }
117770
- const put = await sdk.loops.memory.putDoc(apiKey, slug, doc.key, { content: seed, scope: doc.scope });
117771
- if (put.status !== "ok") {
117772
- failed.push({ key: doc.key, reason: `could not create the ${doc.scope} row (${put.error ?? "write refused"})` });
117773
- continue;
117774
- }
117775
- fetched.push({ ...doc, body: seed, created: true, seeded });
117735
+ function escapeHtml(s) {
117736
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
117737
+ }
117738
+ function renderLoopDashboardTemplate(args) {
117739
+ const title = escapeHtml(args.processSpec?.title ?? args.slug);
117740
+ const seed = embedJson({
117741
+ slug: args.slug,
117742
+ descriptionShort: args.descriptionShort ?? "",
117743
+ processSpec: args.processSpec ?? null
117744
+ });
117745
+ return `<!doctype html>
117746
+ <html lang="en">
117747
+ <head>
117748
+ <meta charset="utf-8">
117749
+ <meta name="viewport" content="width=device-width, initial-scale=1">
117750
+ <title>${title} \u2014 loop dashboard</title>
117751
+ <style>
117752
+ :root { --bg:#f7f7fb; --panel:#fff; --border:#e3e3ee; --muted:#6b6b80; --accent:#5b5bd6; --ok:#1a7f37; --bad:#b42318; }
117753
+ * { box-sizing: border-box; }
117754
+ body { margin:0; font:14px/1.5 -apple-system, "Segoe UI", Roboto, sans-serif; background:var(--bg); color:#1d1d2b; padding:24px; }
117755
+ h1 { font-size:20px; margin:0 0 4px; }
117756
+ h2 { font-size:14px; margin:24px 0 8px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
117757
+ .sub { color:var(--muted); margin:0 0 16px; }
117758
+ .panel { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px; }
117759
+ .map { display:flex; flex-wrap:wrap; align-items:stretch; gap:8px; }
117760
+ .stage { background:var(--panel); border:1px solid var(--border); border-left:3px solid var(--accent); border-radius:10px; padding:10px 14px; min-width:140px; flex:1; }
117761
+ .stage .label { font-weight:600; }
117762
+ .stage .detail { color:var(--muted); font-size:12px; margin-top:2px; }
117763
+ .arrow { align-self:center; color:var(--muted); }
117764
+ .io { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
117765
+ ul { margin:6px 0 0; padding-left:18px; }
117766
+ table { width:100%; border-collapse:collapse; background:var(--panel); border:1px solid var(--border); border-radius:12px; overflow:hidden; }
117767
+ th, td { text-align:left; padding:8px 12px; border-top:1px solid var(--border); font-size:13px; vertical-align:top; }
117768
+ thead th { border-top:none; background:var(--bg); color:var(--muted); font-weight:600; }
117769
+ .ok { color:var(--ok); } .bad { color:var(--bad); }
117770
+ details { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:10px 14px; margin-bottom:8px; }
117771
+ summary { cursor:pointer; font-weight:600; }
117772
+ pre { overflow-x:auto; font-size:12px; background:var(--bg); border-radius:8px; padding:10px; }
117773
+ .empty { color:var(--muted); font-style:italic; }
117774
+ #live { font-size:12px; color:var(--muted); float:right; }
117775
+ </style>
117776
+ </head>
117777
+ <body>
117778
+ <span id="live">loading\u2026</span>
117779
+ <h1 id="title"></h1>
117780
+ <p class="sub" id="desc"></p>
117781
+
117782
+ <h2>Process</h2>
117783
+ <div id="map" class="map panel"></div>
117784
+
117785
+ <h2>Inputs &amp; outputs</h2>
117786
+ <div class="io">
117787
+ <div class="panel"><strong>Inputs</strong><ul id="inputs"></ul></div>
117788
+ <div class="panel"><strong>Outputs</strong><ul id="outputs"></ul></div>
117789
+ </div>
117790
+
117791
+ <h2>Rounds</h2>
117792
+ <div id="rounds"></div>
117793
+
117794
+ <h2>Files</h2>
117795
+ <div id="files"></div>
117796
+
117797
+ <script type="application/json" id="loop-seed">${seed}</script>
117798
+ <script>
117799
+ (function () {
117800
+ "use strict";
117801
+ var seed = JSON.parse(document.getElementById("loop-seed").textContent);
117802
+ document.getElementById("title").textContent = (seed.processSpec && seed.processSpec.title) || seed.slug;
117803
+ document.getElementById("desc").textContent = seed.descriptionShort || "";
117804
+ document.title = seed.slug + " \u2014 loop dashboard";
117805
+
117806
+ function el(tag, cls, text) {
117807
+ var e = document.createElement(tag);
117808
+ if (cls) e.className = cls;
117809
+ if (text !== undefined) e.textContent = text;
117810
+ return e;
117776
117811
  }
117777
- return { fetched, failed };
117778
- }
117779
- function isNotFound(res) {
117780
- if (typeof res !== "object" || res === null) return false;
117781
- return res.code === 404;
117782
- }
117783
- function docMergeRefusal(boot, fresh, next, cleanAppend) {
117784
- if (next.trim() === "" && fresh.trim() !== "") {
117785
- return "would TRUNCATE the stored document to empty while the record still holds content \u2014 an emptied file is not an instruction to erase the record";
117812
+
117813
+ // \u2500\u2500 Process map + IO from the embedded spec (static \u2014 rendered once). \u2500\u2500
117814
+ var map = document.getElementById("map");
117815
+ var spec = seed.processSpec;
117816
+ if (spec && spec.stages && spec.stages.length) {
117817
+ spec.stages.forEach(function (s, i) {
117818
+ if (i > 0) map.appendChild(el("div", "arrow", "\\u2192"));
117819
+ var box = el("div", "stage");
117820
+ box.appendChild(el("div", "label", s.label));
117821
+ if (s.detail) box.appendChild(el("div", "detail", s.detail));
117822
+ map.appendChild(box);
117823
+ });
117824
+ } else {
117825
+ map.appendChild(el("span", "empty", "No process spec on this loop \\u2014 live files and rounds below."));
117786
117826
  }
117787
- if (!cleanAppend && fresh !== boot && headings(fresh).length === 0) {
117788
- return "this fire rewrote the document rather than appending to it, the record MOVED under us, and the document has no headings for the shrink guard to count \u2014 so accepting this write would silently discard whatever the concurrent writer added";
117827
+ function fillList(id, items) {
117828
+ var ul = document.getElementById(id);
117829
+ ul.textContent = "";
117830
+ if (!items || !items.length) { ul.appendChild(el("li", "empty", "\\u2014")); return; }
117831
+ items.forEach(function (x) { ul.appendChild(el("li", null, x)); });
117789
117832
  }
117790
- return void 0;
117791
- }
117792
- async function shipBackStateDocs(sdk, apiKey, slug, dir, boot) {
117793
- const outcomes = [];
117794
- for (const doc of boot) {
117795
- const path2 = join9(dir, doc.filename);
117796
- if (!existsSync8(path2)) {
117797
- outcomes.push({ key: doc.key, outcome: "unchanged" });
117798
- continue;
117799
- }
117800
- const materialized = readFileSync8(path2, "utf-8");
117801
- const reread = await readScoped(sdk, apiKey, slug, doc.key, doc.scope);
117802
- const fresh = reread.status === "ok" ? reread.body : reread.status === "absent" && doc.body === "" ? "" : void 0;
117803
- if (fresh === void 0) {
117804
- outcomes.push({
117805
- key: doc.key,
117806
- outcome: "failed",
117807
- detail: `could not re-read the ${doc.scope} row; ${path2} kept for recovery`
117808
- });
117809
- continue;
117810
- }
117811
- const merged = mergeConstraints(doc.body, materialized, fresh);
117812
- if (merged.refusal) {
117813
- outcomes.push({ key: doc.key, outcome: "refused", detail: merged.refusal });
117814
- continue;
117815
- }
117816
- if (merged.next === void 0) {
117817
- outcomes.push({ key: doc.key, outcome: "unchanged" });
117818
- continue;
117819
- }
117820
- const docRefusal = docMergeRefusal(doc.body, fresh, merged.next, merged.cleanAppend);
117821
- if (docRefusal) {
117822
- outcomes.push({
117823
- key: doc.key,
117824
- outcome: "refused",
117825
- detail: `${docRefusal}; ${path2} kept for recovery`
117826
- });
117827
- continue;
117833
+ fillList("inputs", spec && spec.inputs);
117834
+ fillList("outputs", spec && spec.outputs);
117835
+
117836
+ // \u2500\u2500 Live data: rounds + files, refreshed from GET /data. \u2500\u2500
117837
+ function parseJsonl(text) {
117838
+ var rows = [];
117839
+ (text || "").split("\\n").forEach(function (line) {
117840
+ line = line.trim();
117841
+ if (!line) return;
117842
+ try { rows.push(JSON.parse(line)); } catch (e) { /* skip malformed line */ }
117843
+ });
117844
+ return rows;
117845
+ }
117846
+
117847
+ function renderRounds(fires, rounds) {
117848
+ var host = document.getElementById("rounds");
117849
+ host.textContent = "";
117850
+ if (!fires.length && !rounds.length) {
117851
+ var p = el("div", "panel"); p.appendChild(el("span", "empty", "No rounds yet \\u2014 this fills in as the loop runs."));
117852
+ host.appendChild(p);
117853
+ return;
117828
117854
  }
117829
- const put = await sdk.loops.memory.putDoc(apiKey, slug, doc.key, {
117830
- content: merged.next,
117831
- scope: doc.scope
117855
+ var table = document.createElement("table");
117856
+ var thead = document.createElement("thead");
117857
+ var hr = document.createElement("tr");
117858
+ ["when", "duration", "turns", "tokens", "exit", "round detail (rounds.jsonl)"].forEach(function (h) {
117859
+ hr.appendChild(el("th", null, h));
117832
117860
  });
117833
- if (put.status !== "ok") {
117834
- outcomes.push({
117835
- key: doc.key,
117836
- outcome: "failed",
117837
- detail: `write to the ${doc.scope} row rejected; ${path2} kept for recovery`
117838
- });
117839
- continue;
117861
+ thead.appendChild(hr); table.appendChild(thead);
117862
+ var tbody = document.createElement("tbody");
117863
+ var n = Math.max(fires.length, rounds.length);
117864
+ for (var i = n - 1; i >= 0; i--) { // newest first
117865
+ var f = fires[i] || {};
117866
+ var r = rounds[i];
117867
+ var tr = document.createElement("tr");
117868
+ tr.appendChild(el("td", null, f.ts || (r && r.ts) || "\\u2014"));
117869
+ tr.appendChild(el("td", null, f.duration_s != null ? f.duration_s + "s" : "\\u2014"));
117870
+ tr.appendChild(el("td", null, f.turns != null ? String(f.turns) : "\\u2014"));
117871
+ tr.appendChild(el("td", null, f.tokens_total != null ? Number(f.tokens_total).toLocaleString() : "\\u2014"));
117872
+ tr.appendChild(el("td", f.exit === 0 ? "ok" : f.exit != null ? "bad" : null, f.exit != null ? String(f.exit) : "\\u2014"));
117873
+ var detail = el("td");
117874
+ if (f.launch_failed) { detail.textContent = "launch failed \\u2014 " + (f.reason || "unknown reason"); }
117875
+ else if (r) { var pre = document.createElement("pre"); pre.textContent = JSON.stringify(r, null, 1); detail.appendChild(pre); }
117876
+ else detail.textContent = "\\u2014";
117877
+ tr.appendChild(detail);
117878
+ tbody.appendChild(tr);
117840
117879
  }
117841
- const after = await sdk.loops.memory.getDoc(apiKey, slug, doc.key, { scope: doc.scope });
117842
- if (after.status !== "ok") {
117843
- outcomes.push({ key: doc.key, outcome: "shipped", detail: "could not verify it landed" });
117844
- continue;
117880
+ table.appendChild(tbody);
117881
+ host.appendChild(table);
117882
+ }
117883
+
117884
+ function renderFiles(files) {
117885
+ var host = document.getElementById("files");
117886
+ host.textContent = "";
117887
+ var names = Object.keys(files || {}).filter(function (n) { return n !== "rounds.jsonl"; });
117888
+ if (!names.length) { var p = el("div", "panel"); p.appendChild(el("span", "empty", "No files surfaced by the manifest.")); host.appendChild(p); return; }
117889
+ names.forEach(function (name) {
117890
+ var d = document.createElement("details");
117891
+ d.appendChild(el("summary", null, name));
117892
+ var pre = document.createElement("pre");
117893
+ pre.textContent = files[name];
117894
+ d.appendChild(pre);
117895
+ host.appendChild(d);
117896
+ });
117897
+ }
117898
+
117899
+ function refresh() {
117900
+ fetch("/data").then(function (res) { return res.json(); }).then(function (data) {
117901
+ var fires = parseJsonl(data.state && data.state["fires.jsonl"]);
117902
+ var rounds = parseJsonl(data.files && data.files["rounds.jsonl"]);
117903
+ renderRounds(fires, rounds);
117904
+ renderFiles(data.files);
117905
+ document.getElementById("live").textContent = "updated " + new Date().toLocaleTimeString();
117906
+ }).catch(function () {
117907
+ document.getElementById("live").textContent = "server stopped";
117908
+ });
117909
+ }
117910
+ refresh();
117911
+ setInterval(refresh, 5000);
117912
+ })();
117913
+ </script>
117914
+ </body>
117915
+ </html>
117916
+ `;
117917
+ }
117918
+ function injectDefaultDashboard(loop2) {
117919
+ const hasHtml = typeof loop2.dashboardHtml === "string" && loop2.dashboardHtml.trim() !== "";
117920
+ if (hasHtml) return;
117921
+ if (loop2.dashboardManifest && typeof loop2.dashboardManifest === "object") {
117922
+ try {
117923
+ loop2.dashboardManifest = JSON.stringify(loop2.dashboardManifest);
117924
+ } catch {
117845
117925
  }
117846
- const afterText = after.doc.content ?? "";
117847
- const missing = merged.addedBlocks.length > 0 ? verifyLanded(afterText, merged.addedBlocks) : afterText === merged.next ? [] : ["<rewrite did not survive>"];
117848
- outcomes.push(
117849
- missing.length === 0 ? { key: doc.key, outcome: "shipped" } : {
117850
- key: doc.key,
117851
- outcome: "failed",
117852
- detail: `${missing.length} section(s) did not survive a concurrent write; ${path2} kept`
117853
- }
117854
- );
117855
117926
  }
117856
- return outcomes;
117927
+ const manifest = typeof loop2.dashboardManifest === "string" ? loop2.dashboardManifest : void 0;
117928
+ loop2.dashboardHtml = renderLoopDashboardTemplate({
117929
+ slug: typeof loop2.slug === "string" ? loop2.slug : "loop",
117930
+ descriptionShort: typeof loop2.descriptionShort === "string" ? loop2.descriptionShort : void 0,
117931
+ processSpec: parseProcessSpec(manifest)
117932
+ });
117933
+ if (!manifest || !manifest.trim()) loop2.dashboardManifest = defaultDashboardManifest();
117857
117934
  }
117858
117935
 
117859
117936
  // src/loops/memory-verbs.ts
@@ -118178,6 +118255,27 @@ async function taskMemoryArchiveVerb(rawSlug, opts) {
118178
118255
  );
118179
118256
  }
118180
118257
 
118258
+ // src/mcp-server/memory-scope.ts
118259
+ init_esm_shims();
118260
+ var MEMORY_SCOPE_EXPLANATION = "Global memory: everyone who uses this task (when it is shared) reads and writes the same copy \u2014 edits can conflict and build up over time. Per-member memory: each person who uses it gets their own copy. For a private task the difference doesn't matter. If unsure, choose per-member to avoid conflicts. Examples: a product PBI someone picks up and leaves half-done \u2192 per member; competitor signals the whole workspace should see \u2192 global.";
118261
+ function memorySummaryLine(manifest) {
118262
+ const normalized = normalizeMemoryManifest(manifest);
118263
+ const entries = normalized ? [...normalized.docs, ...normalized.records] : [];
118264
+ const global2 = entries.filter((e) => e.scope === "shared").length;
118265
+ const perMember = entries.filter((e) => e.scope === "member").length;
118266
+ if (global2 > 0 && perMember > 0) return `Memory: mixed \u2014 ${global2} global \xB7 ${perMember} per member`;
118267
+ if (global2 > 0) return "Memory: global";
118268
+ if (perMember > 0) return "Memory: per member";
118269
+ return "Memory: none";
118270
+ }
118271
+ function defaultMemoryManifest() {
118272
+ return { docs: [], records: [{ kind: "run", scope: "member" }] };
118273
+ }
118274
+ var MEMORY_DEFAULTED_SUFFIX = " (defaulted \u2014 no manifest declared)";
118275
+ function declaresNoMemory(manifest) {
118276
+ return memorySummaryLine(manifest ?? null) === "Memory: none";
118277
+ }
118278
+
118181
118279
  // src/mcp-server/prompt-descriptors.ts
118182
118280
  init_esm_shims();
118183
118281
 
@@ -118456,6 +118554,15 @@ function buildReceipt(input) {
118456
118554
  if (typeof sent.draft === "boolean") {
118457
118555
  out.push({ field: "draft", action: classify(sent.draft, prev ? prev.draft : void 0) });
118458
118556
  }
118557
+ if (sent.stateDocs !== void 0) {
118558
+ const sentJson = JSON.stringify(sent.stateDocs ?? null);
118559
+ const prevHas = prev !== void 0 && prev.stateDocs !== void 0;
118560
+ const prevJson = prevHas ? JSON.stringify(prev.stateDocs ?? null) : void 0;
118561
+ const action = mode2 === "created" ? "set" : !previous || !previous.available ? "sent" : prevJson === void 0 ? "changed" : sentJson === prevJson ? "unchanged" : "changed";
118562
+ const rec = { field: "stateDocs", action, bytes: Buffer.byteLength(sentJson, "utf8") };
118563
+ if (prevJson !== void 0) rec.previousBytes = Buffer.byteLength(prevJson, "utf8");
118564
+ out.push(rec);
118565
+ }
118459
118566
  return out;
118460
118567
  }
118461
118568
  function deriveChanges(input, receipt) {
@@ -118542,7 +118649,15 @@ function enforceResponseCap(payload) {
118542
118649
  }
118543
118650
  if (size6(out) > MAX_RESPONSE_CHARS && Array.isArray(out.receipt?.fields)) {
118544
118651
  const r = out.receipt;
118545
- out.receipt = { fieldCount: r.fields.length, note: "receipt omitted \u2014 response cap" };
118652
+ out.receipt = {
118653
+ fieldCount: r.fields.length,
118654
+ note: "receipt omitted \u2014 response cap",
118655
+ // WHERE IT LANDED SURVIVES THE CAP. It is two short strings, and it is the
118656
+ // one line on this receipt whose absence was an incident: shrinking the
118657
+ // response must not be how a caller stops being told which workspace it
118658
+ // just wrote to.
118659
+ ...r.workspace !== void 0 ? { workspace: r.workspace } : {}
118660
+ };
118546
118661
  }
118547
118662
  if (size6(out) > MAX_RESPONSE_CHARS) {
118548
118663
  out.summary = TRUNCATION_MARKER;
@@ -118553,7 +118668,7 @@ function enforceResponseCap(payload) {
118553
118668
 
118554
118669
  // src/mcp-server/delegate-tools.ts
118555
118670
  init_esm_shims();
118556
- import { existsSync as existsSync9, statSync as statSync2 } from "fs";
118671
+ import { existsSync as existsSync10, statSync as statSync2 } from "fs";
118557
118672
  import { z as z3 } from "zod";
118558
118673
 
118559
118674
  // src/delegate/jobs.ts
@@ -121047,7 +121162,7 @@ function registerDelegateTools(deps) {
121047
121162
  const validated = validateStartInput(params, {
121048
121163
  isDirectory: (path2) => {
121049
121164
  try {
121050
- return existsSync9(path2) && statSync2(path2).isDirectory();
121165
+ return existsSync10(path2) && statSync2(path2).isDirectory();
121051
121166
  } catch {
121052
121167
  return false;
121053
121168
  }
@@ -121297,6 +121412,82 @@ function applyResolvedMethod(url2, method, inputs) {
121297
121412
 
121298
121413
  // src/mcp-server/index.ts
121299
121414
  init_plaintext_vault();
121415
+ init_paths();
121416
+ init_resolve();
121417
+
121418
+ // src/mcp-server/identity-reload.ts
121419
+ init_esm_shims();
121420
+ import { readFileSync as readFileSync12, statSync as statSync3 } from "fs";
121421
+ var NO_VAULT = "absent";
121422
+ var IDENTITY_CHECK_THROTTLE_MS = 1e3;
121423
+ function fingerprintVault(stat) {
121424
+ return stat ? `${stat.mtimeMs}:${stat.size}` : NO_VAULT;
121425
+ }
121426
+ function statVaultFile(path2) {
121427
+ try {
121428
+ const s = statSync3(path2);
121429
+ return { mtimeMs: s.mtimeMs, size: s.size };
121430
+ } catch {
121431
+ return null;
121432
+ }
121433
+ }
121434
+ var IdentityWatch = class {
121435
+ vaultPath;
121436
+ now;
121437
+ throttleMs;
121438
+ stat;
121439
+ /** False for an env-supplied key: `poll` is then a guaranteed no-op. */
121440
+ armed;
121441
+ fingerprint;
121442
+ lastCheckedAt = null;
121443
+ constructor(options) {
121444
+ this.vaultPath = options.vaultPath;
121445
+ this.now = options.now ?? Date.now;
121446
+ this.throttleMs = options.throttleMs ?? IDENTITY_CHECK_THROTTLE_MS;
121447
+ this.stat = options.stat ?? statVaultFile;
121448
+ this.armed = options.source !== "env";
121449
+ this.fingerprint = fingerprintVault(this.stat(this.vaultPath));
121450
+ }
121451
+ /** The fingerprint this watch currently considers current. */
121452
+ currentFingerprint() {
121453
+ return this.fingerprint;
121454
+ }
121455
+ /**
121456
+ * One throttled `stat`. Returns the change when the vault file differs from
121457
+ * the fingerprint on record — and ADOPTS it in the same step, so a reload the
121458
+ * caller then fails to apply is not retried on every subsequent tool call.
121459
+ * Returns `null` when nothing changed, when the throttle window is still open,
121460
+ * or when the watch is not armed.
121461
+ */
121462
+ poll() {
121463
+ if (!this.armed) return null;
121464
+ const at = this.now();
121465
+ if (this.lastCheckedAt !== null && at - this.lastCheckedAt < this.throttleMs) return null;
121466
+ this.lastCheckedAt = at;
121467
+ const next = fingerprintVault(this.stat(this.vaultPath));
121468
+ if (next === this.fingerprint) return null;
121469
+ const from14 = this.fingerprint;
121470
+ this.fingerprint = next;
121471
+ return { from: from14, to: next };
121472
+ }
121473
+ /**
121474
+ * The vault bytes this watch is pointed at, or `null` when there is none.
121475
+ *
121476
+ * Reading THROUGH the watch rather than through `loadVault()` is deliberate:
121477
+ * the freshness check and the reload must address the same file, and having
121478
+ * one owner of the path is what makes that true by construction instead of by
121479
+ * two constants agreeing.
121480
+ */
121481
+ readVault() {
121482
+ try {
121483
+ return readFileSync12(this.vaultPath, "utf-8");
121484
+ } catch {
121485
+ return null;
121486
+ }
121487
+ }
121488
+ };
121489
+
121490
+ // src/mcp-server/index.ts
121300
121491
  function buildDeclaredConnectorsSentence(catalog, unreadable) {
121301
121492
  const read = catalog === void 0 ? { connectors: cachedCatalogOrEmpty(), unreadable: mirrorGap() } : { connectors: catalog, unreadable: unreadable ?? [] };
121302
121493
  const gap = describeCatalogGap(read.unreadable);
@@ -121428,6 +121619,8 @@ var loopIndexCache = null;
121428
121619
  var taskIndexCache = null;
121429
121620
  var cardCounts = null;
121430
121621
  var lastNudgeAtCall = null;
121622
+ var WORKSPACE_IDENTITY_TTL_MS = 6e4;
121623
+ var workspaceIdentityCache = null;
121431
121624
  var sessionId = randomUUID4();
121432
121625
  var mcpEventLogger = null;
121433
121626
  var PROCESSED_APPROVAL_IDS_CAP = 50;
@@ -121524,6 +121717,83 @@ function maybeAppendCardNudge(res, toolName, toolCallIndex) {
121524
121717
  if (next !== res) lastNudgeAtCall = toolCallIndex;
121525
121718
  return next;
121526
121719
  }
121720
+ async function resolveWorkspaceIdentity() {
121721
+ const apiKey = currentCredentials.apiKey;
121722
+ if (!apiKey?.trim()) return { companyName: null, employeeName: null };
121723
+ const now = Date.now();
121724
+ if (workspaceIdentityCache && now - workspaceIdentityCache.at < WORKSPACE_IDENTITY_TTL_MS) {
121725
+ return workspaceIdentityCache.value;
121726
+ }
121727
+ let value2 = { companyName: null, employeeName: null };
121728
+ try {
121729
+ const sdk = await getSDK();
121730
+ const me = await sdk.virtualWalletsManagers.getManagerMe(apiKey);
121731
+ value2 = {
121732
+ companyName: typeof me?.companyName === "string" ? me.companyName : null,
121733
+ employeeName: typeof me?.employeeName === "string" ? me.employeeName : null
121734
+ };
121735
+ } catch (err) {
121736
+ console.error(
121737
+ `\u26A0\uFE0F workspace identity read failed \u2014 reporting nulls: ${err instanceof Error ? err.message : String(err)}`
121738
+ );
121739
+ }
121740
+ workspaceIdentityCache = { at: now, value: value2 };
121741
+ return value2;
121742
+ }
121743
+ var identityWatch = null;
121744
+ function dropIdentityScopedCaches() {
121745
+ walletsCache = null;
121746
+ policiesCache = null;
121747
+ transactionsCache = null;
121748
+ allowlistCache = null;
121749
+ capabilityIndexCache = null;
121750
+ compoundIndexCache = null;
121751
+ loopIndexCache = null;
121752
+ taskIndexCache = null;
121753
+ cardCounts = null;
121754
+ servicesDiscovered = null;
121755
+ workspaceIdentityCache = null;
121756
+ endpointSessionCache.clear();
121757
+ }
121758
+ async function applyIdentityReload() {
121759
+ const watch = identityWatch;
121760
+ if (!watch) return;
121761
+ const { readVaultAddress: readVaultAddress2 } = await Promise.resolve().then(() => (init_core(), core_exports));
121762
+ const vault = watch.readVault();
121763
+ const nextEoa = vault ? readVaultAddress2(vault) : "";
121764
+ const resolved = await resolveApiKey(cliConfig?.credentialStore ?? "keychain");
121765
+ const previousKey = currentCredentials.apiKey ?? "";
121766
+ const previousEoa = currentCredentials.eoaAddress ?? "";
121767
+ currentCredentials.walletKeystoreJson = vault ?? "";
121768
+ currentCredentials.eoaAddress = nextEoa;
121769
+ currentCredentials.signerAddress = nextEoa;
121770
+ if (resolved.apiKey) currentCredentials.apiKey = resolved.apiKey;
121771
+ refreshPlaintextVaultMode();
121772
+ clearWalletScopedCredentials(currentCredentials);
121773
+ delete currentCredentials.passphrase;
121774
+ delete currentCredentials.wrongPassphraseAttempts;
121775
+ delete currentCredentials.requiresNewPolicy;
121776
+ dropIdentityScopedCaches();
121777
+ console.error(
121778
+ `identity reloaded: ${maskApiKey(previousKey)} -> ${maskApiKey(currentCredentials.apiKey ?? "")}, eoa ${previousEoa || "(none)"} -> ${nextEoa || "(none)"}`
121779
+ );
121780
+ }
121781
+ async function ensureIdentityFresh() {
121782
+ let change = null;
121783
+ try {
121784
+ change = identityWatch?.poll() ?? null;
121785
+ } catch {
121786
+ return;
121787
+ }
121788
+ if (!change) return;
121789
+ try {
121790
+ await applyIdentityReload();
121791
+ } catch (err) {
121792
+ console.error(
121793
+ `\u26A0\uFE0F identity reload failed \u2014 continuing with the identity loaded at boot: ${err instanceof Error ? err.message : String(err)}`
121794
+ );
121795
+ }
121796
+ }
121527
121797
  var __registerTool = server.tool.bind(server);
121528
121798
  server.tool = (def, handler) => __registerTool(
121529
121799
  def,
@@ -121534,6 +121804,7 @@ server.tool = (def, handler) => __registerTool(
121534
121804
  // session, unconditionally. Only `severity: "block"` (an unsupported CLI)
121535
121805
  // still refuses, and that refusal happens inside the gate.
121536
121806
  runToolThroughInterstitialGate(def?.name, async () => {
121807
+ await ensureIdentityFresh();
121537
121808
  const res = await handler(params);
121538
121809
  maybeAutoShareOnToolCall();
121539
121810
  const withNudge = maybeAppendCardNudge(res, def?.name, toolCallCount);
@@ -122248,6 +122519,7 @@ async function initializeCredentials(walletKeystoreJson, eoaAddress, config) {
122248
122519
  currentCredentials.signerAddress = eoaAddress;
122249
122520
  currentCredentials.eoaAddress = eoaAddress;
122250
122521
  currentCredentials.apiKey = config.apiKey;
122522
+ identityWatch = new IdentityWatch({ vaultPath: VAULT_PATH, source: config.apiKeySource ?? null });
122251
122523
  mcpEventLogger = new McpEventLogger(
122252
122524
  () => getSDK(),
122253
122525
  () => currentCredentials.apiKey,
@@ -123086,7 +123358,7 @@ server.tool(
123086
123358
  server.tool(
123087
123359
  {
123088
123360
  name: "getWalletStatus",
123089
- description: "Get wallet status: auth, balance, policy, allowlist, and the last 10 transactions (amount, merchant, timestamp, status). A transaction `amount` is a EUR display string like `\u20AC0.0126`, or the em dash `\u2014` when the row carries no usable amount (none recorded, or a value that is not a base-units integer) \u2014 `\u2014` means UNKNOWN, never zero. The unformatted base-units value is on `amountRaw`, which is null for exactly those rows.",
123361
+ description: "Get wallet status: auth, balance, policy, allowlist, WHICH WORKSPACE this server is acting in, and the last 10 transactions (amount, merchant, timestamp, status). `workspace` is `{companyName, employeeName}` \u2014 the Ametyst workspace every task, memory document and payment from this server lands in; both fields are null when the profile could not be read, which means UNKNOWN, never 'no workspace'. A transaction `amount` is a EUR display string like `\u20AC0.0126`, or the em dash `\u2014` when the row carries no usable amount (none recorded, or a value that is not a base-units integer) \u2014 `\u2014` means UNKNOWN, never zero. The unformatted base-units value is on `amountRaw`, which is null for exactly those rows.",
123090
123362
  inputs: []
123091
123363
  },
123092
123364
  async () => {
@@ -123141,7 +123413,12 @@ server.tool(
123141
123413
  status: currentCredentials.authorizationStatus || "none",
123142
123414
  eoaAddress: currentCredentials.eoaAddress || null,
123143
123415
  walletAddress: currentCredentials.walletAddress || null,
123144
- kernelClientActive: !!currentCredentials.virtualWalletKernelAccountClient
123416
+ kernelClientActive: !!currentCredentials.virtualWalletKernelAccountClient,
123417
+ // WHICH WORKSPACE THIS IS. The 2026-08-31 incident produced writes into
123418
+ // the wrong workspace with nothing in any payload naming one — an agent
123419
+ // reading wallet status could not have told, and neither could a human
123420
+ // reading the transcript afterwards. Nulls when `me` could not be read.
123421
+ workspace: await resolveWorkspaceIdentity()
123145
123422
  };
123146
123423
  if (policiesCache && currentCredentials.policyId) {
123147
123424
  const policy = policiesCache.find((p) => p.id === parseInt(currentCredentials.policyId, 10));
@@ -123597,6 +123874,7 @@ async function resolveTasksCore(params, flavor) {
123597
123874
  if (slug && flavor.fetchManifest) {
123598
123875
  const manifest = await flavor.fetchManifest(sdk, currentCredentials.apiKey, slug);
123599
123876
  if (manifest) payload.stateDocs = manifest;
123877
+ if (manifest !== void 0) payload.memory = memorySummaryLine(manifest);
123600
123878
  }
123601
123879
  }
123602
123880
  }
@@ -123642,6 +123920,7 @@ function taskMemoryBlock(slug, docKey) {
123642
123920
  };
123643
123921
  }
123644
123922
  var liveDashboards = /* @__PURE__ */ new Map();
123923
+ var LIVE_DASHBOARD_WATCH_MS = 15e3;
123645
123924
  function closeAllLiveDashboards() {
123646
123925
  for (const h of liveDashboards.values()) {
123647
123926
  try {
@@ -123669,6 +123948,22 @@ async function startLiveDashboard(entity, dir) {
123669
123948
  });
123670
123949
  if (!handle) return null;
123671
123950
  liveDashboards.set(slug, handle);
123951
+ const watcher = setInterval(() => {
123952
+ if (liveDashboards.get(slug) !== handle) {
123953
+ clearInterval(watcher);
123954
+ return;
123955
+ }
123956
+ if (!existsSync12(dir)) {
123957
+ clearInterval(watcher);
123958
+ liveDashboards.delete(slug);
123959
+ try {
123960
+ handle.close();
123961
+ } catch {
123962
+ }
123963
+ console.error(`(live dashboard for ${slug} closed \u2014 run folder ${dir} is gone)`);
123964
+ }
123965
+ }, LIVE_DASHBOARD_WATCH_MS);
123966
+ watcher.unref?.();
123672
123967
  const url2 = `http://localhost:${handle.port}`;
123673
123968
  openDashboardInBrowser(url2, { isTTY: true });
123674
123969
  return url2;
@@ -123733,6 +124028,9 @@ async function runTaskCore(params, flavor) {
123733
124028
  }
123734
124029
  const memoryDocKey = defaultDocKey(normalizeMemoryManifest(entity?.stateDocs ?? null));
123735
124030
  const materialized = flavor.materialize(entity, randomUUID4());
124031
+ const docBoot = await materializeMemoryDocs(sdk, apiKey, entity, materialized.dir);
124032
+ for (const note of docBoot.notes) console.error(`(${entity.slug} memory docs: ${note})`);
124033
+ const runFiles = { ...materialized.files, ...docBoot.files };
123736
124034
  const est = estimateBlastRadius(entity);
123737
124035
  const shipBack = flavor.buildShipBack({ dir: materialized.dir, entity });
123738
124036
  const dashboardUrl = await startLiveDashboard(entity, materialized.dir);
@@ -123744,14 +124042,14 @@ async function runTaskCore(params, flavor) {
123744
124042
  dir: materialized.dir,
123745
124043
  ...dashboardUrl ? { dashboard: dashboardUrl } : {},
123746
124044
  blastRadius: est,
123747
- files: materialized.files,
124045
+ files: runFiles,
123748
124046
  ...materialized.skipped ? { skippedEmptyFiles: materialized.skipped } : {},
123749
124047
  // The run's durable memory (loop-memory-primitive). `dir` dies with the run, so
123750
124048
  // anything the NEXT run needs has to be written here instead. Named explicitly with
123751
124049
  // the slug because the run context carries no task identity — which is exactly why
123752
124050
  // the taskMemory* tools take an explicit `taskSlug`.
123753
124051
  memory: taskMemoryBlock(entity.slug, memoryDocKey),
123754
- directive: flavor.buildDirective({ dir: materialized.dir, slug: entity.slug, files: materialized.files, docKey: memoryDocKey }),
124052
+ directive: flavor.buildDirective({ dir: materialized.dir, slug: entity.slug, files: runFiles, docKey: memoryDocKey }),
123755
124053
  ...shipBack ? { shipBack } : {}
123756
124054
  }) }, { type: "text", text: dashboardLine }] };
123757
124055
  } catch (error) {
@@ -124281,7 +124579,7 @@ async function upsertTaskCore(params, flavor) {
124281
124579
  return { available: false, reason: err instanceof Error ? err.message : String(err) };
124282
124580
  }
124283
124581
  };
124284
- const upsert = async (entity, sourcePaths = {}) => {
124582
+ const upsert = async (entity, sourcePaths = {}, memoryDefaulted = false) => {
124285
124583
  if (!id && flavor.injectDashboardOnCreate) {
124286
124584
  try {
124287
124585
  injectDefaultDashboard(entity);
@@ -124293,6 +124591,7 @@ async function upsertTaskCore(params, flavor) {
124293
124591
  if (res.status === "ok") {
124294
124592
  void refreshDynamicPrompts();
124295
124593
  const mode2 = id ? "modified" : "created";
124594
+ const effectiveManifest = "stateDocs" in entity ? entity.stateDocs : !id ? null : previous && previous.available ? previous.content.stateDocs ?? null : void 0;
124296
124595
  const summary = buildLoopUpsertSummary({
124297
124596
  mode: mode2,
124298
124597
  sent: entity,
@@ -124303,7 +124602,26 @@ async function upsertTaskCore(params, flavor) {
124303
124602
  success: true,
124304
124603
  mode: mode2,
124305
124604
  [flavor.responseKey]: projectLoopIdentity(flavor.pick(res)),
124306
- receipt: { fields: summary.receipt },
124605
+ // WHERE IT LANDED, on the receipt itself. The 2026-08-31 incident put a
124606
+ // task into the wrong (admin) workspace and the success payload named
124607
+ // no workspace at all, so neither the agent nor the human reading the
124608
+ // transcript afterwards could tell. Nulls when `me` could not be read —
124609
+ // unknown, never a guess.
124610
+ receipt: {
124611
+ fields: summary.receipt,
124612
+ workspace: await resolveWorkspaceIdentity(),
124613
+ // WHERE THE TASK'S MEMORY LANDED, said in the user's vocabulary rather than left
124614
+ // implicit in the manifest bytes. `effectiveManifest` is what the task HAS after
124615
+ // this call: what this call sent, or — on a MODIFY that did not send one — the
124616
+ // stored manifest the diff already read. Never a fourth read.
124617
+ //
124618
+ // ⛔ THE FLOOR IS NAMED, NOT HIDDEN. When the default fired the line still reports
124619
+ // where the memory landed (`Memory: per member` — it really is per member), with a
124620
+ // suffix saying the author declared nothing. A receipt printing the bare line would
124621
+ // present the floor as an authored choice, which is the one thing this injection
124622
+ // must never be allowed to look like.
124623
+ ...effectiveManifest !== void 0 ? { memory: memorySummaryLine(effectiveManifest) + (memoryDefaulted ? MEMORY_DEFAULTED_SUFFIX : "") } : {}
124624
+ },
124307
124625
  summary: summary.text,
124308
124626
  summaryTruncated: summary.truncated,
124309
124627
  // ON CREATE ONLY. A task lands PRIVATE to the author, and the one step
@@ -124394,8 +124712,13 @@ async function upsertTaskCore(params, flavor) {
124394
124712
  }
124395
124713
  stateDocs = checked.manifest;
124396
124714
  }
124715
+ let memoryDefaulted = false;
124716
+ if (!id && declaresNoMemory(stateDocs)) {
124717
+ stateDocs = defaultMemoryManifest();
124718
+ memoryDefaulted = true;
124719
+ }
124397
124720
  const body = { ...resolvedMd };
124398
- if (stateDocsProvided) body.stateDocs = stateDocs;
124721
+ if (stateDocsProvided || memoryDefaulted) body.stateDocs = stateDocs;
124399
124722
  if (slug) body.slug = slug;
124400
124723
  if (descriptionShort) body.descriptionShort = descriptionShort;
124401
124724
  if (markdownBody) body.markdownBody = markdownBody;
@@ -124410,7 +124733,7 @@ async function upsertTaskCore(params, flavor) {
124410
124733
  sourcePaths[mdKey] = params[fileKey].trim();
124411
124734
  }
124412
124735
  }
124413
- return await upsert(body, sourcePaths);
124736
+ return await upsert(body, sourcePaths, memoryDefaulted);
124414
124737
  }
124415
124738
  const hasMessages = typeof params.messages === "string" && params.messages.trim();
124416
124739
  const hasBrief = typeof params.brief === "string" && params.brief.trim();
@@ -124435,7 +124758,7 @@ async function upsertTaskCore(params, flavor) {
124435
124758
  server.tool(
124436
124759
  {
124437
124760
  name: "createTask",
124438
- description: "Create OR modify a workspace TASK (upsert). A task is the unified card \u2014 what used to be published as either a 'compound skill' or a 'loop' is ONE row now, so this ONE tool authors both: a task with only a `markdownBody` is what a compound was, a task that also carries visionMd/constraintsMd/readmeMd is what a loop was. Pass `id` to MODIFY, omit it to CREATE. MODIFY IS A NO-CLOBBER PATCH: send `id` plus ONLY the field(s) you want to change \u2014 any of markdownBody/visionMd/constraintsMd/readmeMd/dashboardHtml/dashboardManifest/stateDocs/descriptionShort/category/draft/graphJson \u2014 fields you don't send are preserved, so a constraints-only ship-back never has to resend the body. VERBATIM BODIES: for any LONG markdown field, WRITE it to a local file first and pass the matching *FilePath (`filePath` for the body, `visionFilePath`/`constraintsFilePath`/`readmeFilePath`/`dashboardHtmlFilePath`/`dashboardManifestFilePath`) INSTEAD of inlining it \u2014 the local MCP server reads the bytes from disk and pushes them verbatim (no arg-size limit, no drift). RESPONSE: a closing `summary` derived strictly from the published content (never invented) plus a compact `receipt` of per-field byte counts and source paths; the full bodies are NOT echoed back. NOT AN AUTHORING TOOL: it publishes what you give it. To create, publish or audit a task, run `task-architect` first \u2014 this tool is only the final upsert it performs. NOTE: if the user's intent is to publish/upload/import local skills to Ametyst, call getTask FIRST to fetch the `task-architect` task \u2014 it contains the publish procedure to follow before using this tool. MEMORY: `stateDocs` is the task's memory manifest \u2014 which memory docs / record kinds it owns and whether each is `shared` by the workspace or per-`member`; the runner creates the declared docs at boot and ships each back to its declared namespace. DASHBOARD: createTask does NOT inject a default monitoring page on create \u2014 pass `dashboardHtml` if the task wants one.",
124761
+ description: "Create OR modify a workspace TASK (upsert). A task is the unified card \u2014 what used to be published as either a 'compound skill' or a 'loop' is ONE row now, so this ONE tool authors both: a task with only a `markdownBody` is what a compound was, a task that also carries visionMd/constraintsMd/readmeMd is what a loop was. Pass `id` to MODIFY, omit it to CREATE. MODIFY IS A NO-CLOBBER PATCH: send `id` plus ONLY the field(s) you want to change \u2014 any of markdownBody/visionMd/constraintsMd/readmeMd/dashboardHtml/dashboardManifest/stateDocs/descriptionShort/category/draft/graphJson \u2014 fields you don't send are preserved, so a constraints-only ship-back never has to resend the body. VERBATIM BODIES: for any LONG markdown field, WRITE it to a local file first and pass the matching *FilePath (`filePath` for the body, `visionFilePath`/`constraintsFilePath`/`readmeFilePath`/`dashboardHtmlFilePath`/`dashboardManifestFilePath`) INSTEAD of inlining it \u2014 the local MCP server reads the bytes from disk and pushes them verbatim (no arg-size limit, no drift). RESPONSE: a closing `summary` derived strictly from the published content (never invented) plus a compact `receipt` of per-field byte counts and source paths; the full bodies are NOT echoed back. NOT AN AUTHORING TOOL: it publishes what you give it. To create, publish or audit a task, run `task-architect` first \u2014 this tool is only the final upsert it performs. NOTE: if the user's intent is to publish/upload/import local skills to Ametyst, call getTask FIRST to fetch the `task-architect` task \u2014 it contains the publish procedure to follow before using this tool. MEMORY: `stateDocs` is the task's memory manifest \u2014 which memory docs / record kinds it owns and whether each is `shared` by the workspace or per-`member`; the runner creates the declared docs at boot and ships each back to its declared namespace. \u26D4 THE SCOPE CHOICE IS THE USER'S, NOT YOURS: BEFORE you create or modify a task that carries a memory manifest, EXPLAIN THE CHOICE TO THEM in your own words \u2014 never as `stateDocs` or `scope` \u2014 covering all of the following, and ASK them when their intent is ambiguous instead of deciding for them. Quote it verbatim if that is clearer; never contradict it: \"" + MEMORY_SCOPE_EXPLANATION + '" The response says which one the task ended up with, as a `memory` line on the receipt (`Memory: global` / `Memory: per member` / `Memory: mixed \u2014 N global \xB7 M per member` / `Memory: none`); getTask carries the same line. \u26D4 DECLARING THE MANIFEST IS EXPECTED ON EVERY TASK: a CREATE that declares none \u2014 or a declared-empty one \u2014 is given a floor rather than being born with no memory (the run diary, per member: `{"docs":[],"records":[{"kind":"run","scope":"member"}]}`), and the receipt says so with `(defaulted \u2014 no manifest declared)`, but that floor is a backstop and NOT a substitute for deriving the manifest this task actually needs and explaining the scope choice to the user first. DASHBOARD: createTask does NOT inject a default monitoring page on create \u2014 pass `dashboardHtml` if the task wants one.',
124439
124762
  inputs: [
124440
124763
  { name: "slug", type: "string", required: false, description: "URL-safe unique slug for the task within the workspace. Required on CREATE." },
124441
124764
  { name: "descriptionShort", type: "string", required: false, description: "One-line description of what the task does. Required on CREATE." },
@@ -124451,7 +124774,7 @@ server.tool(
124451
124774
  { name: "dashboardHtmlFilePath", type: "string", required: false, description: "Path to a local file read verbatim as the dashboard HTML. Takes precedence over `dashboardHtml`." },
124452
124775
  { name: "dashboardManifest", type: "string", required: false, description: `JSON (as text) naming which materialized files the dashboard's /data endpoint surfaces (e.g. {"files":["STATUS.md"]}).` },
124453
124776
  { name: "dashboardManifestFilePath", type: "string", required: false, description: "Path to a local file read verbatim as the dashboard manifest. Takes precedence over `dashboardManifest`." },
124454
- { name: "stateDocs", type: "string", required: false, description: 'THE MEMORY MANIFEST \u2014 JSON TEXT: `{"docs":[{"key":"board","scope":"shared"}],"records":[{"kind":"run","scope":"member"}]}`. It declares WHICH memory documents and record kinds this task owns and WHERE each one lives: `scope` is `shared` (ONE row for the whole workspace \u2014 every member reads and writes the same document) or `member` (one row PER SEAT \u2014 each member has their own). `records[].kind` is a RESERVED diary kind (run | decision | error | import \u2014 always accepted by the server, declared or not; declaring one only fixes its scope) or a FREE item kind the task invents (a token [a-z0-9-]{1,64}, e.g. `pbi`, `test`, `needs-patrick` \u2014 keyed, versioned, closed with taskMemoryArchive; see the TASK MEMORY MODEL in your instructions). Both arrays are REQUIRED and may be empty (`{"docs":[],"records":[]}` = this task keeps no memory). \u26D4 A STRING, ALWAYS: send the JSON object as text, `"null"` to clear an existing manifest, and omit the field to leave it untouched; a bare object or a bare null is REJECTED by this tool\'s input schema before the call runs, and an EMPTY STRING is treated exactly like omitting the field. WHAT THE RUNNER DOES WITH IT: at boot every declared doc is read from its declared namespace and CREATED there (empty) if it does not exist yet \u2014 a `member` doc is seeded from the `shared` row of the same key when one exists \u2014 and materialized as `<key>.md`; at exit each changed doc is shipped back to the SAME namespace. A write through taskMemoryAppend to a declared key/kind needs no scope (the server resolves it and refuses a disagreeing one). RULES, enforced here AND by the server (the call fails and the task is neither created nor modified): `key` is a safe basename of 1..128 chars (no `/`, `\\`, no leading dot), unique case-insensitively, and NOT one of the seven files the runner writes itself (SKILL.md, VISION.md, CONSTRAINTS.md, README.md, STATUS.md, dashboard.html, dashboard.manifest.json \u2014 with or without `.md`); at most 64 docs and 16 records, one entry per kind. The manifest has NO archive section \u2014 an archived item is the same kind in the same namespace with archived: true. The retired array shape (`[{key, filename, \u2026}]`, including `"[]"`) is INVALID. \u26D4 DERIVE the manifest from what the task actually needs to remember between runs and who needs to see it \u2014 a board the whole team works is `shared`, a personal cursor is `member` \u2014 and explain it to the author IN THEIR WORDS ("everyone on the team sees the same to-do list"), never as `stateDocs` or `scope`.' },
124777
+ { name: "stateDocs", type: "string", required: false, description: 'THE MEMORY MANIFEST \u2014 JSON TEXT: `{"docs":[{"key":"board","scope":"shared"}],"records":[{"kind":"run","scope":"member"}]}`. It declares WHICH memory documents and record kinds this task owns and WHERE each one lives: `scope` is `shared` (ONE row for the whole workspace \u2014 every member reads and writes the same document) or `member` (one row PER SEAT \u2014 each member has their own). `records[].kind` is a RESERVED diary kind (run | decision | error | import \u2014 always accepted by the server, declared or not; declaring one only fixes its scope) or a FREE item kind the task invents (a token [a-z0-9-]{1,64}, e.g. `pbi`, `test`, `needs-patrick` \u2014 keyed, versioned, closed with taskMemoryArchive; see the TASK MEMORY MODEL in your instructions). Both arrays are REQUIRED and may be empty (`{"docs":[],"records":[]}` = this task keeps no memory). \u26D4 A STRING, ALWAYS: send the JSON object as text, `"null"` to clear an existing manifest, and omit the field to leave it untouched; a bare object or a bare null is REJECTED by this tool\'s input schema before the call runs, and an EMPTY STRING is treated exactly like omitting the field. WHAT THE RUNNER DOES WITH IT: at boot every declared doc is read from its declared namespace and CREATED there (empty) if it does not exist yet \u2014 a `member` doc is seeded from the `shared` row of the same key when one exists \u2014 and materialized as `<key>.md`; at exit each changed doc is shipped back to the SAME namespace. A write through taskMemoryAppend to a declared key/kind needs no scope (the server resolves it and refuses a disagreeing one). RULES, enforced here AND by the server (the call fails and the task is neither created nor modified): `key` is a safe basename of 1..128 chars (no `/`, `\\`, no leading dot), unique case-insensitively, and NOT one of the seven files the runner writes itself (SKILL.md, VISION.md, CONSTRAINTS.md, README.md, STATUS.md, dashboard.html, dashboard.manifest.json \u2014 with or without `.md`); at most 64 docs and 16 records, one entry per kind. The manifest has NO archive section \u2014 an archived item is the same kind in the same namespace with archived: true. The retired array shape (`[{key, filename, \u2026}]`, including `"[]"`) is INVALID. \u26D4 DERIVE the manifest from what the task actually needs to remember between runs and who needs to see it \u2014 a board the whole team works is `shared`, a personal cursor is `member` \u2014 and explain it to the author IN THEIR WORDS ("everyone on the team sees the same to-do list"), never as `stateDocs` or `scope`. \u26D4 AND YOU DO THAT BEFORE THIS CALL, NOT AFTER \u2014 the choice is the user\'s: cover all of this with them, and ASK when their intent is ambiguous instead of deciding for them. "' + MEMORY_SCOPE_EXPLANATION + '"' },
124455
124778
  { name: "graphJson", type: "string", required: false, description: "JSON string of the canvas node graph (for re-edit). Defaults to {} on create." },
124456
124779
  { name: "draft", type: "boolean", required: false, description: "Whether the task is a draft (excluded from discovery). Defaults to true on create." },
124457
124780
  { name: "category", type: "string", required: false, description: "Free-text category for browsing/filtering (e.g. 'research'). Must be explicit \u2014 the tool asks rather than guessing." },
@@ -125809,14 +126132,14 @@ function killPreviousServeInstances(deps) {
125809
126132
 
125810
126133
  // src/commands/autosync-skills.ts
125811
126134
  init_esm_shims();
125812
- import { existsSync as existsSync12 } from "fs";
126135
+ import { existsSync as existsSync14 } from "fs";
125813
126136
  import { homedir as homedir12 } from "os";
125814
126137
  import { join as join18 } from "path";
125815
126138
 
125816
126139
  // src/compounds/sync-skills.ts
125817
126140
  init_esm_shims();
125818
126141
  init_paths();
125819
- import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync12, rmSync, writeFileSync as writeFileSync10 } from "fs";
126142
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync13, rmSync, writeFileSync as writeFileSync10 } from "fs";
125820
126143
  import { join as join17 } from "path";
125821
126144
  var MANAGED_MARKER = "<!-- ametyst-managed: sync-skills -->";
125822
126145
  var GITIGNORE_HEADER = "# ametyst-managed: sync-skills \u2014 pointer skills generated for this account; never commit them.";
@@ -125861,7 +126184,7 @@ ${taskRunSection(slug, kind)}`;
125861
126184
  }
125862
126185
  function isManaged(file) {
125863
126186
  try {
125864
- return readFileSync12(file, "utf8").includes(MANAGED_MARKER);
126187
+ return readFileSync13(file, "utf8").includes(MANAGED_MARKER);
125865
126188
  } catch {
125866
126189
  return false;
125867
126190
  }
@@ -125875,8 +126198,8 @@ function buildGitignoreContent(managedSlugs) {
125875
126198
  function maintainGitignore(root2) {
125876
126199
  const file = join17(root2, ".gitignore");
125877
126200
  const managed = listManagedDirs(root2);
125878
- if (existsSync11(file)) {
125879
- const current = readFileSync12(file, "utf8");
126201
+ if (existsSync13(file)) {
126202
+ const current = readFileSync13(file, "utf8");
125880
126203
  const firstLine = current.split(/\r?\n/, 1)[0];
125881
126204
  if (firstLine !== GITIGNORE_HEADER) {
125882
126205
  console.error(
@@ -125939,7 +126262,7 @@ async function syncSkills(opts = {}) {
125939
126262
  const skipped = [];
125940
126263
  for (const [dir, item] of desired) {
125941
126264
  const file = join17(dir, "SKILL.md");
125942
- if (existsSync11(file) && !isManaged(file)) {
126265
+ if (existsSync13(file) && !isManaged(file)) {
125943
126266
  skipped.push(item.slug);
125944
126267
  console.warn(`\u26A0\uFE0F sync-skills: skipping "${item.slug}" \u2014 an unmanaged skill already exists at ${file}`);
125945
126268
  continue;
@@ -125967,7 +126290,7 @@ function isSkillsAutosyncEnabled(env = process.env) {
125967
126290
  return !(raw === "0" || raw === "false" || raw === "off" || raw === "no");
125968
126291
  }
125969
126292
  function detectPresentTargets(deps = {}) {
125970
- const exists = deps.existsSync ?? existsSync12;
126293
+ const exists = deps.existsSync ?? existsSync14;
125971
126294
  const cwd = deps.cwd ?? (() => process.cwd());
125972
126295
  const home = deps.homedir ?? homedir12;
125973
126296
  const targets = [];
@@ -126047,7 +126370,7 @@ async function serveCommand() {
126047
126370
  console.error(
126048
126371
  persisted ? isPlaintextVault(persisted) ? "Starting MCP server... (persisted UNENCRYPTED wallet found \u2014 always unlocked, no start_session needed)" : "Starting MCP server... (persisted wallet found \u2014 unlock with start_session)" : "Starting MCP server... (no saved wallet \u2014 one will be created on start_session)"
126049
126372
  );
126050
- await startMCPServer(keystore, eoa, { ...config, apiKey }, versionNotice, {
126373
+ await startMCPServer(keystore, eoa, { ...config, apiKey, apiKeySource: resolved.source ?? void 0 }, versionNotice, {
126051
126374
  // Best-effort: materialize local `/`-command pointer skills for every published
126052
126375
  // compound + loop so they're available without a manual `compound sync-skills`.
126053
126376
  // Deferred from boot to the MCP initialize handshake so the sync is CLIENT-AWARE:
@@ -126986,7 +127309,7 @@ function recordFailedLaunch(args) {
126986
127309
  init_esm_shims();
126987
127310
  import { spawn as spawn2 } from "child_process";
126988
127311
  import { randomUUID as randomUUID5 } from "crypto";
126989
- import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync13, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "fs";
127312
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "fs";
126990
127313
  import { dirname as dirname8, join as join23 } from "path";
126991
127314
 
126992
127315
  // src/loops/claude-binary.ts
@@ -127178,7 +127501,7 @@ async function runLoop(loopId, opts = {}) {
127178
127501
  `Loop ${loop2.slug}: ${est.steps} steps (${est.paidSteps} paid), est \u20AC${est.estCostEur ?? "?"} \u2014 ${capLabel}`
127179
127502
  );
127180
127503
  const statusPath = join23(dir, "STATUS.md");
127181
- const statusBefore = existsSync13(statusPath) ? readFileSync13(statusPath, "utf-8") : "";
127504
+ const statusBefore = existsSync15(statusPath) ? readFileSync14(statusPath, "utf-8") : "";
127182
127505
  writeFileSync12(
127183
127506
  statusPath,
127184
127507
  statusBefore.replace(
@@ -127238,7 +127561,7 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
127238
127561
  async function shipConstraints() {
127239
127562
  try {
127240
127563
  const constraintsPath = join23(dir, "CONSTRAINTS.md");
127241
- const materializedConstraints = existsSync13(constraintsPath) ? readFileSync13(constraintsPath, "utf-8") : void 0;
127564
+ const materializedConstraints = existsSync15(constraintsPath) ? readFileSync14(constraintsPath, "utf-8") : void 0;
127242
127565
  if (materializedConstraints === void 0) return;
127243
127566
  const boot = loop2.constraintsMd ?? "";
127244
127567
  for (let attempt = 1; attempt <= 2; attempt++) {
@@ -127349,7 +127672,7 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
127349
127672
  startedAtEpochMs
127350
127673
  });
127351
127674
  }
127352
- const statusAfter = existsSync13(statusPath) ? readFileSync13(statusPath, "utf-8") : "";
127675
+ const statusAfter = existsSync15(statusPath) ? readFileSync14(statusPath, "utf-8") : "";
127353
127676
  const clean2 = exitCode === 0 && /(vision\s*done|queue\s*drained|status:\s*done)/i.test(statusAfter) && !/(brake|crash|crashed|errored)/i.test(statusAfter);
127354
127677
  await shipBackConstraints();
127355
127678
  process.off("SIGINT", onSignal);
@@ -127387,7 +127710,7 @@ async function showLoop(loopId) {
127387
127710
  // src/loops/schedule.ts
127388
127711
  init_esm_shims();
127389
127712
  init_paths();
127390
- import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync11, rmSync as rmSync3, existsSync as existsSync14, readdirSync as readdirSync5, readFileSync as readFileSync14, accessSync as accessSync2, constants as constants2 } from "fs";
127713
+ import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync11, rmSync as rmSync3, existsSync as existsSync16, readdirSync as readdirSync5, readFileSync as readFileSync15, accessSync as accessSync2, constants as constants2 } from "fs";
127391
127714
  import { execFileSync as execFileSync4 } from "child_process";
127392
127715
  import { join as join24, dirname as dirname9 } from "path";
127393
127716
  import { homedir as homedir14 } from "os";
@@ -127467,7 +127790,7 @@ function isLoaded(lbl) {
127467
127790
  }
127468
127791
  function readInteractiveDefaultModel(home) {
127469
127792
  try {
127470
- const raw = readFileSync14(join24(home, ".claude", "settings.json"), "utf-8");
127793
+ const raw = readFileSync15(join24(home, ".claude", "settings.json"), "utf-8");
127471
127794
  const model = JSON.parse(String(raw)).model;
127472
127795
  return typeof model === "string" && model.trim() ? model.trim() : void 0;
127473
127796
  } catch {
@@ -127695,7 +128018,7 @@ function list(kind, opts = {}) {
127695
128018
  const prefix = kind.labelPrefix;
127696
128019
  if (platform === "darwin") {
127697
128020
  const dir = join24(home, "Library", "LaunchAgents");
127698
- if (!existsSync14(dir)) return [];
128021
+ if (!existsSync16(dir)) return [];
127699
128022
  return readdirSync5(dir).filter((f) => f.startsWith(prefix) && f.endsWith(".plist")).map((f) => f.slice(prefix.length, -".plist".length));
127700
128023
  }
127701
128024
  return readCrontab().split("\n").filter((l) => l.includes(`# ${prefix}`)).map((l) => l.trimEnd().slice(l.trimEnd().lastIndexOf(prefix) + prefix.length));
@@ -127757,12 +128080,12 @@ function listEntries(kind, opts = {}) {
127757
128080
  const prefix = kind.labelPrefix;
127758
128081
  if (platform === "darwin") {
127759
128082
  const dir = join24(home, "Library", "LaunchAgents");
127760
- if (!existsSync14(dir)) return [];
128083
+ if (!existsSync16(dir)) return [];
127761
128084
  return readdirSync5(dir).filter((f) => f.startsWith(prefix) && f.endsWith(".plist")).map((f) => {
127762
128085
  const slug = f.slice(prefix.length, -".plist".length);
127763
128086
  let envKeys = [];
127764
128087
  try {
127765
- envKeys = plistEnvKeys(String(readFileSync14(join24(dir, f), "utf-8")));
128088
+ envKeys = plistEnvKeys(String(readFileSync15(join24(dir, f), "utf-8")));
127766
128089
  } catch {
127767
128090
  envKeys = [];
127768
128091
  }