@ametyst/cli 0.3.0 → 0.3.5

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 +1402 -1019
  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.5" : "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 = {}) {
@@ -115375,6 +115417,46 @@ var McpEventLogger = class {
115375
115417
  }
115376
115418
  };
115377
115419
 
115420
+ // src/mcp-server/newest-grant.ts
115421
+ init_esm_shims();
115422
+ function nonEmptyLowerCase(value2) {
115423
+ return typeof value2 === "string" && value2.trim() !== "" ? value2.toLowerCase() : null;
115424
+ }
115425
+ function isNewerGrant(candidate, incumbent) {
115426
+ const candidateId = Number(candidate?.id);
115427
+ const incumbentId = Number(incumbent?.id);
115428
+ const candidateIdUsable = Number.isFinite(candidateId);
115429
+ const incumbentIdUsable = Number.isFinite(incumbentId);
115430
+ if (candidateIdUsable && incumbentIdUsable && candidateId !== incumbentId) {
115431
+ return candidateId > incumbentId;
115432
+ }
115433
+ const candidateAt = Date.parse(typeof candidate?.createdAt === "string" ? candidate.createdAt : "");
115434
+ const incumbentAt = Date.parse(typeof incumbent?.createdAt === "string" ? incumbent.createdAt : "");
115435
+ if (Number.isFinite(candidateAt) && Number.isFinite(incumbentAt) && candidateAt !== incumbentAt) {
115436
+ return candidateAt > incumbentAt;
115437
+ }
115438
+ return candidateIdUsable && !incumbentIdUsable;
115439
+ }
115440
+ function selectNewestApprovedWallet(wallets, eoaAddress) {
115441
+ if (!Array.isArray(wallets)) return void 0;
115442
+ const eoa = nonEmptyLowerCase(eoaAddress);
115443
+ if (!eoa) return void 0;
115444
+ let newest;
115445
+ for (const wallet of wallets) {
115446
+ if (wallet?.status !== "approved") continue;
115447
+ if (nonEmptyLowerCase(wallet?.address) !== eoa) continue;
115448
+ if (newest === void 0 || isNewerGrant(wallet, newest)) newest = wallet;
115449
+ }
115450
+ return newest;
115451
+ }
115452
+ function findCurrentApprovedWallet(wallets, eoaAddress, pendingWalletId) {
115453
+ if (!Array.isArray(wallets)) return void 0;
115454
+ if (pendingWalletId) {
115455
+ return wallets.find((w) => String(w?.id) === String(pendingWalletId) && w?.status === "approved");
115456
+ }
115457
+ return selectNewestApprovedWallet(wallets, eoaAddress);
115458
+ }
115459
+
115378
115460
  // src/mcp-server/start-session.ts
115379
115461
  init_esm_shims();
115380
115462
  init_dist();
@@ -115509,38 +115591,57 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
115509
115591
  let virtualWalletId = ctx.virtualWalletId;
115510
115592
  let paymentManagerAddress = ctx.paymentManagerAddress;
115511
115593
  let policyId = ctx.policyId;
115512
- if (!walletAddress || !virtualWalletId) {
115513
- try {
115514
- const approvedWallet = wallets.find(
115515
- (w) => w.address?.toLowerCase() === ctx.eoaAddress?.toLowerCase() && w.status === "approved"
115516
- );
115517
- if (approvedWallet) {
115518
- walletAddress = approvedWallet.walletAddress || approvedWallet.kernelAccountAddress;
115519
- virtualWalletId = String(approvedWallet.id);
115520
- paymentManagerAddress = approvedWallet.paymentManagerAddress;
115521
- policyId = String(approvedWallet.policyAssociated);
115522
- credentials.walletAddress = walletAddress;
115523
- credentials.virtualWalletId = virtualWalletId;
115524
- credentials.paymentManagerAddress = paymentManagerAddress;
115525
- credentials.policyId = policyId;
115526
- credentials.authorizationStatus = "approved";
115527
- if (approvedWallet.policyValidUntil != null) {
115528
- credentials.policyValidUntil = BigInt(approvedWallet.policyValidUntil);
115529
- }
115530
- }
115531
- } catch (backendError) {
115594
+ let activeWallet;
115595
+ let grantChanged = false;
115596
+ try {
115597
+ activeWallet = selectNewestApprovedWallet(wallets, ctx.eoaAddress);
115598
+ if (activeWallet) {
115599
+ const newestId = String(activeWallet.id);
115600
+ const rowWalletAddress = activeWallet.walletAddress || activeWallet.kernelAccountAddress;
115601
+ const rowPaymentManagerAddress = activeWallet.paymentManagerAddress;
115602
+ const rowPolicyId = activeWallet.policyAssociated != null ? String(activeWallet.policyAssociated) : void 0;
115603
+ grantChanged = !!virtualWalletId && virtualWalletId !== newestId;
115604
+ if (grantChanged) {
115605
+ console.error(
115606
+ `[start_session] The active grant moved on \u2014 virtual wallet ${virtualWalletId} -> ${newestId}. Adopting the newest approved grant and dropping the session state derived from the old one.`
115607
+ );
115608
+ walletAddress = rowWalletAddress;
115609
+ paymentManagerAddress = rowPaymentManagerAddress;
115610
+ policyId = rowPolicyId;
115611
+ } else {
115612
+ walletAddress = rowWalletAddress || walletAddress;
115613
+ paymentManagerAddress = rowPaymentManagerAddress || paymentManagerAddress;
115614
+ policyId = rowPolicyId ?? policyId;
115615
+ }
115616
+ virtualWalletId = newestId;
115617
+ credentials.walletAddress = walletAddress;
115618
+ credentials.virtualWalletId = virtualWalletId;
115619
+ credentials.paymentManagerAddress = paymentManagerAddress;
115620
+ credentials.policyId = policyId;
115621
+ credentials.authorizationStatus = "approved";
115622
+ if (activeWallet.policyName != null) {
115623
+ credentials.policyName = activeWallet.policyName;
115624
+ }
115625
+ if (activeWallet.policyValidUntil != null) {
115626
+ credentials.policyValidUntil = BigInt(activeWallet.policyValidUntil);
115627
+ }
115628
+ } else if (walletsFetchFailed && virtualWalletId) {
115532
115629
  console.warn(
115533
- "[start_session] Could not check backend:",
115534
- backendError instanceof Error ? backendError.message : String(backendError)
115630
+ `[start_session] \u26A0\uFE0F could not verify the active grant \u2014 signing with the cached one (virtual wallet ${virtualWalletId}, policy ${policyId ?? "unknown"}). If a newer grant was approved since, spends are metered against the older one and may hit its spending limit.`
115535
115631
  );
115536
115632
  }
115633
+ } catch (backendError) {
115634
+ console.warn(
115635
+ "[start_session] Could not check backend:",
115636
+ backendError instanceof Error ? backendError.message : String(backendError)
115637
+ );
115537
115638
  }
115538
115639
  const mergedAuth = credentials.authorizationStatus !== void 0 ? credentials.authorizationStatus : ctx.authorizationStatus;
115539
115640
  console.error(
115540
115641
  `[start_session] State check \u2014 walletAddress: ${walletAddress}, virtualWalletId: ${virtualWalletId}, paymentManagerAddress: ${paymentManagerAddress}, policyId: ${policyId}`
115541
115642
  );
115542
115643
  if (mergedAuth === "expired") {
115543
- return { responseKey: "unlocked_expired", credentials, timingReport: timing.report() };
115644
+ return { responseKey: "unlocked_expired", credentials, timingReport: timing.report(), grantChanged };
115544
115645
  }
115545
115646
  if (walletAddress && virtualWalletId && paymentManagerAddress && policyId) {
115546
115647
  let privateKey = null;
@@ -115562,12 +115663,9 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
115562
115663
  }
115563
115664
  credentials.walletAddress = walletAddress;
115564
115665
  credentials.paymentManagerAddress = paymentManagerAddress;
115565
- const approvedWallet = wallets.find(
115566
- (w) => w.address?.toLowerCase() === ctx.eoaAddress?.toLowerCase() && w.status === "approved"
115567
- );
115568
- const preloadedWalletData = approvedWallet != null && approvedWallet.policyValidAfter != null && approvedWallet.policyValidUntil != null ? {
115569
- policyValidAfter: Number(approvedWallet.policyValidAfter),
115570
- policyValidUntil: Number(approvedWallet.policyValidUntil)
115666
+ const preloadedWalletData = activeWallet != null && activeWallet.policyValidAfter != null && activeWallet.policyValidUntil != null ? {
115667
+ policyValidAfter: Number(activeWallet.policyValidAfter),
115668
+ policyValidUntil: Number(activeWallet.policyValidUntil)
115571
115669
  } : void 0;
115572
115670
  if (ctx.readPolicyActiveOnchain) {
115573
115671
  const nowSeconds = Math.floor(Date.now() / 1e3);
@@ -115589,7 +115687,7 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
115589
115687
  maxAttempts
115590
115688
  });
115591
115689
  if (bootAction === "reauthorize") {
115592
- return { responseKey: "unlocked_pending_authorization", credentials, timingReport: timing.report() };
115690
+ return { responseKey: "unlocked_pending_authorization", credentials, timingReport: timing.report(), grantChanged };
115593
115691
  }
115594
115692
  }
115595
115693
  const { kernelClient: virtualWalletKernelAccountClient, permissionPlugin: virtualWalletPermissionPlugin, kernelDomain } = await virtualWalletsManagers2.configureVirtualWalletKernelAccount(
@@ -115619,13 +115717,14 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
115619
115717
  console.warn("[start_session] \u26A0\uFE0F Could not fetch credit token address (Path B routing disabled):", e);
115620
115718
  }
115621
115719
  }
115622
- return { responseKey: "unlocked_and_authorized", credentials, timingReport: timing.report() };
115720
+ return { responseKey: "unlocked_and_authorized", credentials, timingReport: timing.report(), grantChanged };
115623
115721
  } catch (error) {
115624
115722
  console.error("\u274C [start_session] Kernel client creation failed:", error);
115625
115723
  return {
115626
115724
  responseKey: "unlocked_not_configured",
115627
115725
  credentials,
115628
115726
  timingReport: timing.report(),
115727
+ grantChanged,
115629
115728
  kernelErrorMessage: error instanceof Error ? error.message : String(error)
115630
115729
  };
115631
115730
  } finally {
@@ -115633,9 +115732,9 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
115633
115732
  }
115634
115733
  }
115635
115734
  if (!walletsFetchFailed && wallets.length === 0) {
115636
- return { responseKey: "unlocked_provisioning_pending", credentials, timingReport: timing.report() };
115735
+ return { responseKey: "unlocked_provisioning_pending", credentials, timingReport: timing.report(), grantChanged };
115637
115736
  }
115638
- return { responseKey: "unlocked_pending_authorization", credentials, timingReport: timing.report() };
115737
+ return { responseKey: "unlocked_pending_authorization", credentials, timingReport: timing.report(), grantChanged };
115639
115738
  }
115640
115739
  var WALLET_SCOPED_CREDENTIAL_KEYS = [
115641
115740
  "virtualWalletId",
@@ -115665,9 +115764,9 @@ function clearWalletScopedCredentials(target) {
115665
115764
  function sameAddress(a, b) {
115666
115765
  return String(a ?? "").toLowerCase() === String(b ?? "").toLowerCase();
115667
115766
  }
115668
- function mergeStartSessionCredentials(target, patch, responseKey) {
115767
+ function mergeStartSessionCredentials(target, patch, responseKey, options = {}) {
115669
115768
  const walletChanged = patch.eoaAddress !== void 0 && !sameAddress(patch.eoaAddress, target.eoaAddress);
115670
- if (walletChanged) {
115769
+ if (walletChanged || options.grantChanged) {
115671
115770
  clearWalletScopedCredentials(target);
115672
115771
  }
115673
115772
  if (patch.walletKeystoreJson !== void 0) {
@@ -116848,1012 +116947,1047 @@ init_local_state();
116848
116947
  init_esm_shims();
116849
116948
  init_paths();
116850
116949
  import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync8 } from "fs";
116950
+ import { join as join8 } from "path";
116951
+
116952
+ // src/loops/state-docs.ts
116953
+ init_esm_shims();
116954
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
116851
116955
  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);
116956
+
116957
+ // src/loops/shipback.ts
116958
+ init_esm_shims();
116959
+ function scanHeadings(text) {
116960
+ return scanDoc(text).items;
116961
+ }
116962
+ function scanDoc(text) {
116963
+ const out = [];
116964
+ let offset = 0;
116965
+ let inFence = false;
116966
+ for (const line of text.split("\n")) {
116967
+ const bare = line.endsWith("\r") ? line.slice(0, -1) : line;
116968
+ const trimmed = bare.trimStart();
116969
+ if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) inFence = !inFence;
116970
+ else if (!inFence) {
116971
+ const m = /^###\s+(.*)$/.exec(bare);
116972
+ if (m) out.push({ heading: m[1].trim(), start: offset });
116872
116973
  }
116974
+ offset += line.length + 1;
116873
116975
  }
116874
- writeFileSync8(
116875
- join7(dir, "STATUS.md"),
116876
- `# STATUS \u2014 ${task.slug}
116877
-
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 };
116976
+ return { items: out, unbalanced: inFence };
116887
116977
  }
116888
-
116889
- // src/loops/dashboard.ts
116890
- 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 [];
116978
+ function headingsIgnoringFences(text) {
116979
+ const out = [];
116980
+ for (const line of text.split("\n")) {
116981
+ const bare = line.endsWith("\r") ? line.slice(0, -1) : line;
116982
+ const m = /^ {0,3}###\s+(.*)$/.exec(bare);
116983
+ if (m) out.push(m[1].trim());
116908
116984
  }
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));
116985
+ return out;
116911
116986
  }
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;
116987
+ function headings(text) {
116988
+ return scanHeadings(text).map((h) => h.heading);
116915
116989
  }
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;
116933
- }
116990
+ function countByHeading(list2) {
116991
+ const m = /* @__PURE__ */ new Map();
116992
+ for (const h of list2) m.set(h, (m.get(h) ?? 0) + 1);
116993
+ return m;
116934
116994
  }
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
- }
116995
+ function sections(fragment) {
116996
+ const found = scanHeadings(fragment);
116997
+ if (found.length === 0) return { preamble: fragment, blocks: [] };
116998
+ const preamble = fragment.slice(0, found[0].start);
116999
+ const blocks = found.map((h, i) => ({
117000
+ heading: h.heading,
117001
+ text: fragment.slice(h.start, i + 1 < found.length ? found[i + 1].start : fragment.length)
117002
+ }));
117003
+ return { preamble, blocks };
117004
+ }
117005
+ function norm(s) {
117006
+ return s.replace(/\r\n/g, "\n").trimEnd();
117007
+ }
117008
+ function mergeConstraints(boot, materialized, fresh) {
117009
+ if (materialized === boot) return { next: void 0, added: [], addedBlocks: [], cleanAppend: true };
117010
+ const recordUnchanged = fresh === boot;
117011
+ let next;
117012
+ let cleanAppend;
117013
+ if (materialized.startsWith(boot)) {
117014
+ const suffix = materialized.slice(boot.length);
117015
+ if (recordUnchanged) {
117016
+ next = materialized;
117017
+ } else {
117018
+ const { preamble, blocks } = sections(suffix);
117019
+ const present = new Set(sections(fresh).blocks.map((b) => norm(b.text)));
117020
+ const novel = blocks.filter((b) => !present.has(norm(b.text)));
117021
+ const keepPreamble = preamble.trim().length > 0 && !norm(fresh).endsWith(norm(preamble));
117022
+ const appended = (keepPreamble ? preamble : "") + novel.map((b) => b.text).join("");
117023
+ const seam = appended.length > 0 && fresh.length > 0 && !fresh.endsWith("\n") ? "\n" : "";
117024
+ next = fresh + seam + appended;
116986
117025
  }
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
- }
117026
+ cleanAppend = true;
117027
+ } else {
117028
+ next = materialized;
117029
+ cleanAppend = false;
117030
+ }
117031
+ const guardCounts = (text) => countByHeading(headingsIgnoringFences(text));
117032
+ const freshCounts = guardCounts(fresh);
117033
+ const nextCounts = guardCounts(next);
117034
+ const countAll = (m) => [...m.values()].reduce((a, b) => a + b, 0);
117035
+ const lost = [];
117036
+ for (const [h, n] of freshCounts) {
117037
+ const kept = nextCounts.get(h) ?? 0;
117038
+ if (kept < n) lost.push(`${h}${n > 1 ? ` (${n}\u2192${kept})` : ""}`);
117039
+ }
117040
+ if (lost.length > 0) {
117041
+ const before = countAll(freshCounts);
117042
+ const after = countAll(nextCounts);
117043
+ return {
117044
+ refusal: `ship-back REFUSED: ${lost.length} section(s) present on the record would be lost by this write (${before} \u2192 ${after} headings). Lost: ${lost.slice(0, 5).map((h) => JSON.stringify(h)).join(", ")}${lost.length > 5 ? ` \u2026and ${lost.length - 5} more` : ""}. TO RECOVER: put the bare \`### \` heading line(s) named above back into the refused document and re-run \u2014 the guard counts headings, so an empty section is enough to let the write through.`,
117045
+ added: [],
117046
+ addedBlocks: [],
117047
+ cleanAppend
117048
+ };
117049
+ }
117050
+ if (!cleanAppend) {
117051
+ const bootBlocks = new Set(sections(boot).blocks.map((b) => norm(b.text)));
117052
+ const nextBlocks = new Set(sections(next).blocks.map((b) => norm(b.text)));
117053
+ const clobbered = sections(fresh).blocks.filter((b) => !bootBlocks.has(norm(b.text))).filter((b) => !nextBlocks.has(norm(b.text)));
117054
+ if (clobbered.length > 0) {
117055
+ return {
117056
+ refusal: `ship-back REFUSED: ${clobbered.length} section(s) added to the record while this fire was running would be REWRITTEN by this fire's document, which never saw them. Affected: ${clobbered.slice(0, 5).map((b) => JSON.stringify(b.heading)).join(", ")}${clobbered.length > 5 ? ` \u2026and ${clobbered.length - 5} more` : ""}. TO RECOVER: copy those section(s) verbatim off the record into the refused document and re-run \u2014 here it is the section BODY that must survive, so a bare heading is not enough on this branch.`,
117057
+ added: [],
117058
+ addedBlocks: [],
117059
+ cleanAppend
117060
+ };
117029
117061
  }
117030
- log(` (loop dashboard not started \u2014 :${basePort}-${lastPort} all in use; run continues)`);
117031
- return null;
117032
- })();
117062
+ }
117063
+ if (next === fresh) return { next: void 0, added: [], addedBlocks: [], cleanAppend };
117064
+ const added = [];
117065
+ const remaining = new Map(freshCounts);
117066
+ for (const h of headings(next)) {
117067
+ const owed = remaining.get(h) ?? 0;
117068
+ if (owed > 0) remaining.set(h, owed - 1);
117069
+ else added.push(h);
117070
+ }
117071
+ const freshBlocks = new Set(sections(fresh).blocks.map((b) => norm(b.text)));
117072
+ const addedBlocks = sections(next).blocks.map((b) => norm(b.text)).filter((t) => !freshBlocks.has(t));
117073
+ return { next, added, addedBlocks, cleanAppend };
117074
+ }
117075
+ function verifyLanded(afterWrite, expectedBlocks) {
117076
+ const have = new Set(sections(afterWrite).blocks.map((b) => norm(b.text)));
117077
+ return expectedBlocks.filter((t) => !have.has(t));
117033
117078
  }
117034
117079
 
117035
- // src/mcp-server/task-run-mode.ts
117080
+ // src/loops/memory-manifest.ts
117036
117081
  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;
117082
+ var MEMORY_MANIFEST_MAX_DOCS = 64;
117083
+ var MEMORY_MANIFEST_MAX_RECORDS = 16;
117084
+ var KEY_MAX_LENGTH = 128;
117085
+ function isReservedManifestKey(key, reserved) {
117086
+ const k = key.toLowerCase();
117087
+ return reserved.some((r) => {
117088
+ const name = r.toLowerCase();
117089
+ return k === name || name.endsWith(".md") && k === name.slice(0, -3);
117090
+ });
117045
117091
  }
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
- }
117092
+ function isSafeKey(key) {
117093
+ if (key.length === 0 || key.length > KEY_MAX_LENGTH) return false;
117094
+ if (key === "." || key === "..") return false;
117095
+ if (key.startsWith(".")) return false;
117096
+ if (key.includes("/") || key.includes("\\") || key.includes("\0")) return false;
117097
+ return true;
117053
117098
  }
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) };
117099
+ function normalizeMemoryScope(scope) {
117100
+ switch (scope) {
117101
+ case "shared":
117102
+ case "workspace":
117103
+ return "shared";
117104
+ case "member":
117105
+ case "profile":
117106
+ return "member";
117107
+ default:
117108
+ return void 0;
117109
+ }
117060
117110
  }
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();
117111
+ function validateMemoryManifest(value2, reserved) {
117112
+ if (value2 === null) return { ok: true, manifest: null };
117113
+ if (Array.isArray(value2)) {
117114
+ return {
117115
+ ok: false,
117116
+ 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.'
117117
+ };
117067
117118
  }
117068
- return v;
117119
+ if (typeof value2 !== "object") {
117120
+ return { ok: false, error: "stateDocs must be a JSON object with `docs` and `records` arrays, or null." };
117121
+ }
117122
+ const obj = value2;
117123
+ const extra = Object.keys(obj).filter((k) => k !== "docs" && k !== "records");
117124
+ if (extra.length > 0) {
117125
+ return { ok: false, error: `stateDocs carries unknown propert${extra.length === 1 ? "y" : "ies"} ${extra.join(", ")} \u2014 only \`docs\` and \`records\` are allowed.` };
117126
+ }
117127
+ if (!Array.isArray(obj.docs) || !Array.isArray(obj.records)) {
117128
+ return { ok: false, error: "stateDocs needs BOTH `docs` and `records` arrays (either may be empty)." };
117129
+ }
117130
+ if (obj.docs.length > MEMORY_MANIFEST_MAX_DOCS) {
117131
+ return { ok: false, error: `stateDocs.docs holds ${obj.docs.length} entries \u2014 the maximum is ${MEMORY_MANIFEST_MAX_DOCS}.` };
117132
+ }
117133
+ if (obj.records.length > MEMORY_MANIFEST_MAX_RECORDS) {
117134
+ return { ok: false, error: `stateDocs.records holds ${obj.records.length} entries \u2014 the maximum is ${MEMORY_MANIFEST_MAX_RECORDS}.` };
117135
+ }
117136
+ const docs = [];
117137
+ const seenKeys = /* @__PURE__ */ new Set();
117138
+ for (const [i, raw] of obj.docs.entries()) {
117139
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
117140
+ return { ok: false, error: `stateDocs.docs[${i}] must be an object {key, scope}.` };
117141
+ }
117142
+ const d = raw;
117143
+ const extraDoc = Object.keys(d).filter((k) => k !== "key" && k !== "scope");
117144
+ if (extraDoc.length > 0) {
117145
+ return { ok: false, error: `stateDocs.docs[${i}] carries unknown propert${extraDoc.length === 1 ? "y" : "ies"} ${extraDoc.join(", ")} \u2014 a doc is exactly {key, scope}.` };
117146
+ }
117147
+ if (typeof d.key !== "string" || !isSafeKey(d.key)) {
117148
+ return { ok: false, error: `stateDocs.docs[${i}].key must be a safe basename of 1..${KEY_MAX_LENGTH} chars (no \`/\`, \`\\\`, NUL, no leading dot).` };
117149
+ }
117150
+ if (isReservedManifestKey(d.key, reserved)) {
117151
+ return { ok: false, error: `stateDocs.docs[${i}].key "${d.key}" names a file the runner writes itself (${reserved.join(", ")}) \u2014 pick another key.` };
117152
+ }
117153
+ const lower = d.key.toLowerCase();
117154
+ if (seenKeys.has(lower)) {
117155
+ return { ok: false, error: `stateDocs.docs has "${d.key}" more than once (keys are unique case-insensitively).` };
117156
+ }
117157
+ seenKeys.add(lower);
117158
+ if (d.scope !== "shared" && d.scope !== "member") {
117159
+ return { ok: false, error: `stateDocs.docs[${i}].scope must be "shared" or "member".` };
117160
+ }
117161
+ docs.push({ key: d.key, scope: d.scope });
117162
+ }
117163
+ const records = [];
117164
+ const seenKinds = /* @__PURE__ */ new Set();
117165
+ for (const [i, raw] of obj.records.entries()) {
117166
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
117167
+ return { ok: false, error: `stateDocs.records[${i}] must be an object {kind, scope}.` };
117168
+ }
117169
+ const r = raw;
117170
+ const extraRec = Object.keys(r).filter((k) => k !== "kind" && k !== "scope");
117171
+ if (extraRec.length > 0) {
117172
+ return { ok: false, error: `stateDocs.records[${i}] carries unknown propert${extraRec.length === 1 ? "y" : "ies"} ${extraRec.join(", ")} \u2014 a record is exactly {kind, scope}.` };
117173
+ }
117174
+ if (!isRecordKindToken(r.kind)) {
117175
+ return {
117176
+ ok: false,
117177
+ 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").`
117178
+ };
117179
+ }
117180
+ if (seenKinds.has(r.kind)) {
117181
+ return { ok: false, error: `stateDocs.records declares "${r.kind}" more than once.` };
117182
+ }
117183
+ seenKinds.add(r.kind);
117184
+ if (r.scope !== "shared" && r.scope !== "member") {
117185
+ return { ok: false, error: `stateDocs.records[${i}].scope must be "shared" or "member".` };
117186
+ }
117187
+ records.push({ kind: r.kind, scope: r.scope });
117188
+ }
117189
+ return { ok: true, manifest: { docs, records } };
117069
117190
  }
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] ?? ""));
117191
+ function normalizeMemoryManifest(manifest) {
117192
+ if (!manifest || typeof manifest !== "object") return null;
117193
+ const docs = [];
117194
+ for (const d of Array.isArray(manifest.docs) ? manifest.docs : []) {
117195
+ const scope = normalizeMemoryScope(d?.scope);
117196
+ if (typeof d?.key !== "string" || d.key.length === 0 || !scope) continue;
117197
+ docs.push({ key: d.key, scope });
117198
+ }
117199
+ const records = [];
117200
+ for (const r of Array.isArray(manifest.records) ? manifest.records : []) {
117201
+ const scope = normalizeMemoryScope(r?.scope);
117202
+ if (!isRecordKindToken(r?.kind) || !scope) continue;
117203
+ records.push({ kind: r.kind, scope });
117204
+ }
117205
+ return { docs, records };
117082
117206
  }
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 };
117207
+ async function readTaskManifest(sdk, apiKey, slug) {
117208
+ try {
117209
+ const got = await sdk.tasks.get(apiKey, slug);
117210
+ if (got?.status !== "ok") return void 0;
117211
+ return normalizeMemoryManifest(got.task?.stateDocs ?? null);
117212
+ } catch {
117213
+ return void 0;
117087
117214
  }
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
117215
  }
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 };
117216
+ function declaredDocScope(manifest, key) {
117217
+ return manifest?.docs.find((d) => d.key.toLowerCase() === key.toLowerCase())?.scope;
117218
+ }
117219
+ function declaredRecordScope(manifest, kind) {
117220
+ return manifest?.records.find((r) => r.kind === kind)?.scope;
117221
+ }
117222
+ function declaredRecordKinds(manifest) {
117223
+ if (manifest === void 0) return void 0;
117224
+ return manifest?.records.map((r) => r.kind) ?? [];
117225
+ }
117226
+ function formatMemoryManifest(manifest) {
117227
+ const normalized = normalizeMemoryManifest(manifest);
117228
+ if (!normalized) return "(none)";
117229
+ const lines = [
117230
+ ...normalized.docs.map((d) => `doc ${d.key} \xB7 ${d.scope}`),
117231
+ ...normalized.records.map((r) => `record ${r.kind} \xB7 ${r.scope}`)
117232
+ ];
117233
+ return lines.length ? lines.join("\n") : "(declared empty: no docs, no records)";
117106
117234
  }
117107
117235
 
117108
- // src/loops/dashboard-template.ts
117109
- init_esm_shims();
117110
- var DEFAULT_DASHBOARD_FILES = [
117236
+ // src/loops/state-docs.ts
117237
+ var RESERVED_FIRE_FILENAMES = [
117238
+ "SKILL.md",
117111
117239
  "VISION.md",
117112
117240
  "CONSTRAINTS.md",
117113
- "QUEUE.md",
117114
- "STATUS.md",
117115
117241
  "README.md",
117116
- "rounds.jsonl"
117242
+ "STATUS.md",
117243
+ "dashboard.html",
117244
+ "dashboard.manifest.json"
117117
117245
  ];
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] });
117246
+ var KEY_EXTENSION = /\.[A-Za-z0-9]{1,8}$/;
117247
+ function filenameForKey(key) {
117248
+ return KEY_EXTENSION.test(key) ? key : `${key}.md`;
117154
117249
  }
117155
- function embedJson(value2) {
117156
- return JSON.stringify(value2).replace(/</g, "\\u003c");
117250
+ function isSafeBasename(filename) {
117251
+ if (filename.length === 0) return false;
117252
+ if (filename.startsWith(".")) return false;
117253
+ if (filename.includes("/") || filename.includes("\\")) return false;
117254
+ if (filename === ".." || filename.includes("\0")) return false;
117255
+ return true;
117157
117256
  }
117158
- function escapeHtml(s) {
117159
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
117257
+ function isReservedName(filename) {
117258
+ return RESERVED_FIRE_FILENAMES.some((r) => r.toLowerCase() === filename.toLowerCase());
117160
117259
  }
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."));
117260
+ var DOC_LIST_LIMIT = 500;
117261
+ async function discoverMemoryDocs(sdk, apiKey, slug) {
117262
+ const out = { own: [], shared: [], failed: [], truncated: [] };
117263
+ for (const scope of ["member", "shared"]) {
117264
+ const res = await sdk.loops.memory.listDocs(apiKey, slug, { scope, limit: DOC_LIST_LIMIT });
117265
+ if (res.status !== "ok") {
117266
+ if (scope === "member" && isNotFound(res)) continue;
117267
+ out.failed.push(scope);
117268
+ continue;
117269
+ }
117270
+ if (res.items.length >= DOC_LIST_LIMIT) out.truncated.push(scope);
117271
+ if (scope === "member") out.own = res.items;
117272
+ else out.shared = res.items;
117249
117273
  }
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)); });
117274
+ return out;
117275
+ }
117276
+ function planStateDocs(stateDocs, discovered) {
117277
+ const manifest = normalizeMemoryManifest(stateDocs);
117278
+ const candidates = [];
117279
+ const claimed = /* @__PURE__ */ new Set();
117280
+ for (const d of manifest?.docs ?? []) {
117281
+ claimed.add(d.key);
117282
+ candidates.push({ key: d.key, scope: d.scope, declared: true });
117255
117283
  }
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;
117284
+ for (const [rows, scope] of [
117285
+ [discovered.own, "member"],
117286
+ [discovered.shared, "shared"]
117287
+ ]) {
117288
+ for (const m of rows) {
117289
+ if (claimed.has(m.key)) continue;
117290
+ claimed.add(m.key);
117291
+ candidates.push({ key: m.key, scope, declared: false });
117292
+ }
117268
117293
  }
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;
117294
+ const docs = [];
117295
+ const skipped = [];
117296
+ const taken = /* @__PURE__ */ new Map();
117297
+ for (const c of candidates) {
117298
+ const filename = filenameForKey(c.key);
117299
+ if (!isSafeBasename(filename)) {
117300
+ skipped.push({ key: c.key, reason: `'${filename}' is not a safe basename` });
117301
+ continue;
117277
117302
  }
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);
117303
+ if (isReservedName(filename)) {
117304
+ skipped.push({
117305
+ key: c.key,
117306
+ reason: `'${filename}' is a file the wrapper writes itself \u2014 rename the key`
117307
+ });
117308
+ continue;
117302
117309
  }
117303
- table.appendChild(tbody);
117304
- host.appendChild(table);
117310
+ const owner = taken.get(filename.toLowerCase());
117311
+ if (owner !== void 0) {
117312
+ skipped.push({ key: c.key, reason: `'${filename}' is already taken by key '${owner}'` });
117313
+ continue;
117314
+ }
117315
+ taken.set(filename.toLowerCase(), c.key);
117316
+ docs.push({ key: c.key, filename, scope: c.scope, declared: c.declared });
117305
117317
  }
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
- });
117318
+ return { docs, skipped, manifest };
117319
+ }
117320
+ function describeSource(doc) {
117321
+ const from14 = doc.scope === "shared" ? "shared" : "own";
117322
+ const notes = [];
117323
+ if (doc.created) notes.push(doc.seeded ? "created, seeded from shared" : "created");
117324
+ if (!doc.declared) notes.push("not in manifest");
117325
+ return `${doc.filename} \u2190 ${from14}${notes.length ? ` (${notes.join(", ")})` : ""}`;
117326
+ }
117327
+ async function readScoped(sdk, apiKey, slug, key, scope) {
117328
+ const res = await sdk.loops.memory.getDoc(apiKey, slug, key, { scope });
117329
+ if (res.status === "ok") return { status: "ok", body: res.doc.content ?? "" };
117330
+ if (isNotFound(res)) return { status: "absent" };
117331
+ return { status: "failed" };
117332
+ }
117333
+ async function fetchStateDocs(sdk, apiKey, slug, docs) {
117334
+ const fetched = [];
117335
+ const failed = [];
117336
+ for (const doc of docs) {
117337
+ const read = await readScoped(sdk, apiKey, slug, doc.key, doc.scope);
117338
+ if (read.status === "ok") {
117339
+ fetched.push({ ...doc, body: read.body, created: false, seeded: false });
117340
+ continue;
117341
+ }
117342
+ if (read.status === "failed") {
117343
+ failed.push({ key: doc.key, reason: `could not read the ${doc.scope} row` });
117344
+ continue;
117345
+ }
117346
+ if (!doc.declared) {
117347
+ failed.push({ key: doc.key, reason: `the ${doc.scope} row vanished after the listing` });
117348
+ continue;
117349
+ }
117350
+ let seed = "";
117351
+ let seeded = false;
117352
+ if (doc.scope === "member") {
117353
+ const shared = await readScoped(sdk, apiKey, slug, doc.key, "shared");
117354
+ if (shared.status === "failed") {
117355
+ failed.push({ key: doc.key, reason: "could not read the shared row to seed the member row" });
117356
+ continue;
117357
+ }
117358
+ if (shared.status === "ok") {
117359
+ seed = shared.body;
117360
+ seeded = true;
117361
+ }
117362
+ }
117363
+ const put = await sdk.loops.memory.putDoc(apiKey, slug, doc.key, { content: seed, scope: doc.scope });
117364
+ if (put.status !== "ok") {
117365
+ failed.push({ key: doc.key, reason: `could not create the ${doc.scope} row (${put.error ?? "write refused"})` });
117366
+ continue;
117367
+ }
117368
+ fetched.push({ ...doc, body: seed, created: true, seeded });
117320
117369
  }
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
- });
117370
+ return { fetched, failed };
117371
+ }
117372
+ function isNotFound(res) {
117373
+ if (typeof res !== "object" || res === null) return false;
117374
+ return res.code === 404;
117375
+ }
117376
+ function docMergeRefusal(boot, fresh, next, cleanAppend) {
117377
+ if (next.trim() === "" && fresh.trim() !== "") {
117378
+ 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";
117332
117379
  }
117333
- refresh();
117334
- setInterval(refresh, 5000);
117335
- })();
117336
- </script>
117337
- </body>
117338
- </html>
117339
- `;
117380
+ if (!cleanAppend && fresh !== boot && headings(fresh).length === 0) {
117381
+ 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";
117382
+ }
117383
+ return void 0;
117340
117384
  }
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 {
117385
+ async function shipBackStateDocs(sdk, apiKey, slug, dir, boot) {
117386
+ const outcomes = [];
117387
+ for (const doc of boot) {
117388
+ const path2 = join7(dir, doc.filename);
117389
+ if (!existsSync9(path2)) {
117390
+ outcomes.push({ key: doc.key, outcome: "unchanged" });
117391
+ continue;
117392
+ }
117393
+ const materialized = readFileSync8(path2, "utf-8");
117394
+ const reread = await readScoped(sdk, apiKey, slug, doc.key, doc.scope);
117395
+ const fresh = reread.status === "ok" ? reread.body : reread.status === "absent" && doc.body === "" ? "" : void 0;
117396
+ if (fresh === void 0) {
117397
+ outcomes.push({
117398
+ key: doc.key,
117399
+ outcome: "failed",
117400
+ detail: `could not re-read the ${doc.scope} row; ${path2} kept for recovery`
117401
+ });
117402
+ continue;
117403
+ }
117404
+ const merged = mergeConstraints(doc.body, materialized, fresh);
117405
+ if (merged.refusal) {
117406
+ outcomes.push({ key: doc.key, outcome: "refused", detail: merged.refusal });
117407
+ continue;
117408
+ }
117409
+ if (merged.next === void 0) {
117410
+ outcomes.push({ key: doc.key, outcome: "unchanged" });
117411
+ continue;
117412
+ }
117413
+ const docRefusal = docMergeRefusal(doc.body, fresh, merged.next, merged.cleanAppend);
117414
+ if (docRefusal) {
117415
+ outcomes.push({
117416
+ key: doc.key,
117417
+ outcome: "refused",
117418
+ detail: `${docRefusal}; ${path2} kept for recovery`
117419
+ });
117420
+ continue;
117421
+ }
117422
+ const put = await sdk.loops.memory.putDoc(apiKey, slug, doc.key, {
117423
+ content: merged.next,
117424
+ scope: doc.scope
117425
+ });
117426
+ if (put.status !== "ok") {
117427
+ outcomes.push({
117428
+ key: doc.key,
117429
+ outcome: "failed",
117430
+ detail: `write to the ${doc.scope} row rejected; ${path2} kept for recovery`
117431
+ });
117432
+ continue;
117433
+ }
117434
+ const after = await sdk.loops.memory.getDoc(apiKey, slug, doc.key, { scope: doc.scope });
117435
+ if (after.status !== "ok") {
117436
+ outcomes.push({ key: doc.key, outcome: "shipped", detail: "could not verify it landed" });
117437
+ continue;
117348
117438
  }
117439
+ const afterText = after.doc.content ?? "";
117440
+ const missing = merged.addedBlocks.length > 0 ? verifyLanded(afterText, merged.addedBlocks) : afterText === merged.next ? [] : ["<rewrite did not survive>"];
117441
+ outcomes.push(
117442
+ missing.length === 0 ? { key: doc.key, outcome: "shipped" } : {
117443
+ key: doc.key,
117444
+ outcome: "failed",
117445
+ detail: `${missing.length} section(s) did not survive a concurrent write; ${path2} kept`
117446
+ }
117447
+ );
117349
117448
  }
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();
117449
+ return outcomes;
117357
117450
  }
117358
117451
 
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";
117452
+ // src/mcp-server/materialize-task.ts
117453
+ var TASK_DEFINITION_FILES = [
117454
+ ["skill", "SKILL.md", "markdownBody"],
117455
+ ["vision", "VISION.md", "visionMd"],
117456
+ ["constraints", "CONSTRAINTS.md", "constraintsMd"],
117457
+ ["readme", "README.md", "readmeMd"],
117458
+ ["dashboardHtml", "dashboard.html", "dashboardHtml"],
117459
+ ["dashboardManifest", "dashboard.manifest.json", "dashboardManifest"]
117460
+ ];
117461
+ function materializeTask(task, runId) {
117462
+ const dir = loopFireDir(task.slug, runId);
117463
+ mkdirSync6(dir, { recursive: true, mode: 448 });
117464
+ const files = {};
117465
+ const skipped = [];
117466
+ for (const [label2, filename, field] of TASK_DEFINITION_FILES) {
117467
+ const body = task[field];
117468
+ if (typeof body === "string" && body.length > 0) {
117469
+ writeFileSync8(join8(dir, filename), body, { mode: 384 });
117470
+ files[label2] = filename;
117471
+ } else {
117472
+ skipped.push(label2);
117473
+ }
117474
+ }
117475
+ writeFileSync8(
117476
+ join8(dir, "STATUS.md"),
117477
+ `# STATUS \u2014 ${task.slug}
117363
117478
 
117364
- // src/loops/shipback.ts
117365
- init_esm_shims();
117366
- function scanHeadings(text) {
117367
- return scanDoc(text).items;
117479
+ task_id: ${task.id}
117480
+ run_id: ${runId}
117481
+ started: pending
117482
+ queue: not started
117483
+ `,
117484
+ { mode: 384 }
117485
+ );
117486
+ files.status = "STATUS.md";
117487
+ return { dir, files, skipped };
117368
117488
  }
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 });
117489
+ async function materializeMemoryDocs(sdk, apiKey, task, dir) {
117490
+ const files = {};
117491
+ const notes = [];
117492
+ try {
117493
+ const discovered = await discoverMemoryDocs(sdk, apiKey, task.slug);
117494
+ for (const scope of discovered.failed) {
117495
+ notes.push(`could not list the ${scope} memory docs \u2014 undeclared docs there are not materialized`);
117380
117496
  }
117381
- offset += line.length + 1;
117497
+ const plan = planStateDocs(task.stateDocs ?? null, discovered);
117498
+ for (const s of plan.skipped) {
117499
+ notes.push(`memory doc '${s.key}' not materialized: ${s.reason}`);
117500
+ }
117501
+ if (plan.docs.length === 0) return { files, notes };
117502
+ const { fetched, failed } = await fetchStateDocs(sdk, apiKey, task.slug, plan.docs);
117503
+ for (const f of failed) {
117504
+ notes.push(`memory doc '${f.key}' not materialized: ${f.reason}`);
117505
+ }
117506
+ for (const doc of fetched) {
117507
+ writeFileSync8(join8(dir, doc.filename), doc.body, { mode: 384 });
117508
+ files[`doc:${doc.key}`] = doc.filename;
117509
+ notes.push(describeSource(doc));
117510
+ }
117511
+ return { files, notes };
117512
+ } catch (err) {
117513
+ notes.push(
117514
+ `memory docs not materialized (${err instanceof Error ? err.message : String(err)}) \u2014 the run continues; read them with taskMemoryGet instead`
117515
+ );
117516
+ return { files, notes };
117382
117517
  }
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();
117414
- }
117415
- function mergeConstraints(boot, materialized, fresh) {
117416
- if (materialized === boot) return { next: void 0, added: [], addedBlocks: [], cleanAppend: true };
117417
- const recordUnchanged = fresh === boot;
117418
- let next;
117419
- let cleanAppend;
117420
- if (materialized.startsWith(boot)) {
117421
- const suffix = materialized.slice(boot.length);
117422
- if (recordUnchanged) {
117423
- next = materialized;
117424
- } else {
117425
- const { preamble, blocks } = sections(suffix);
117426
- const present = new Set(sections(fresh).blocks.map((b) => norm(b.text)));
117427
- const novel = blocks.filter((b) => !present.has(norm(b.text)));
117428
- const keepPreamble = preamble.trim().length > 0 && !norm(fresh).endsWith(norm(preamble));
117429
- const appended = (keepPreamble ? preamble : "") + novel.map((b) => b.text).join("");
117430
- const seam = appended.length > 0 && fresh.length > 0 && !fresh.endsWith("\n") ? "\n" : "";
117431
- next = fresh + seam + appended;
117432
- }
117433
- cleanAppend = true;
117434
- } else {
117435
- next = materialized;
117436
- cleanAppend = false;
117437
- }
117438
- const guardCounts = (text) => countByHeading(headingsIgnoringFences(text));
117439
- const freshCounts = guardCounts(fresh);
117440
- const nextCounts = guardCounts(next);
117441
- const countAll = (m) => [...m.values()].reduce((a, b) => a + b, 0);
117442
- const lost = [];
117443
- for (const [h, n] of freshCounts) {
117444
- const kept = nextCounts.get(h) ?? 0;
117445
- if (kept < n) lost.push(`${h}${n > 1 ? ` (${n}\u2192${kept})` : ""}`);
117446
- }
117447
- if (lost.length > 0) {
117448
- const before = countAll(freshCounts);
117449
- const after = countAll(nextCounts);
117450
- return {
117451
- refusal: `ship-back REFUSED: ${lost.length} section(s) present on the record would be lost by this write (${before} \u2192 ${after} headings). Lost: ${lost.slice(0, 5).map((h) => JSON.stringify(h)).join(", ")}${lost.length > 5 ? ` \u2026and ${lost.length - 5} more` : ""}. TO RECOVER: put the bare \`### \` heading line(s) named above back into the refused document and re-run \u2014 the guard counts headings, so an empty section is enough to let the write through.`,
117452
- added: [],
117453
- addedBlocks: [],
117454
- cleanAppend
117455
- };
117456
- }
117457
- if (!cleanAppend) {
117458
- const bootBlocks = new Set(sections(boot).blocks.map((b) => norm(b.text)));
117459
- const nextBlocks = new Set(sections(next).blocks.map((b) => norm(b.text)));
117460
- const clobbered = sections(fresh).blocks.filter((b) => !bootBlocks.has(norm(b.text))).filter((b) => !nextBlocks.has(norm(b.text)));
117461
- if (clobbered.length > 0) {
117462
- return {
117463
- refusal: `ship-back REFUSED: ${clobbered.length} section(s) added to the record while this fire was running would be REWRITTEN by this fire's document, which never saw them. Affected: ${clobbered.slice(0, 5).map((b) => JSON.stringify(b.heading)).join(", ")}${clobbered.length > 5 ? ` \u2026and ${clobbered.length - 5} more` : ""}. TO RECOVER: copy those section(s) verbatim off the record into the refused document and re-run \u2014 here it is the section BODY that must survive, so a bare heading is not enough on this branch.`,
117464
- added: [],
117465
- addedBlocks: [],
117466
- cleanAppend
117467
- };
117468
- }
117469
- }
117470
- if (next === fresh) return { next: void 0, added: [], addedBlocks: [], cleanAppend };
117471
- const added = [];
117472
- const remaining = new Map(freshCounts);
117473
- for (const h of headings(next)) {
117474
- const owed = remaining.get(h) ?? 0;
117475
- if (owed > 0) remaining.set(h, owed - 1);
117476
- else added.push(h);
117477
- }
117478
- const freshBlocks = new Set(sections(fresh).blocks.map((b) => norm(b.text)));
117479
- const addedBlocks = sections(next).blocks.map((b) => norm(b.text)).filter((t) => !freshBlocks.has(t));
117480
- return { next, added, addedBlocks, cleanAppend };
117481
- }
117482
- function verifyLanded(afterWrite, expectedBlocks) {
117483
- const have = new Set(sections(afterWrite).blocks.map((b) => norm(b.text)));
117484
- return expectedBlocks.filter((t) => !have.has(t));
117485
117518
  }
117486
117519
 
117487
- // src/loops/memory-manifest.ts
117520
+ // src/mcp-server/index.ts
117521
+ import { existsSync as existsSync12 } from "fs";
117522
+
117523
+ // src/loops/dashboard.ts
117488
117524
  init_esm_shims();
117489
- var MEMORY_MANIFEST_MAX_DOCS = 64;
117490
- var MEMORY_MANIFEST_MAX_RECORDS = 16;
117491
- var KEY_MAX_LENGTH = 128;
117492
- function isReservedManifestKey(key, reserved) {
117493
- const k = key.toLowerCase();
117494
- return reserved.some((r) => {
117495
- const name = r.toLowerCase();
117496
- return k === name || name.endsWith(".md") && k === name.slice(0, -3);
117497
- });
117525
+ import { createServer as createServer2 } from "http";
117526
+ import * as realFs from "fs";
117527
+ import { spawn as realSpawn } from "child_process";
117528
+ import { join as join9 } from "path";
117529
+ var DEFAULT_PORT = 4477;
117530
+ var DASHBOARD_PORT_ENV = "AMETYST_LOOP_DASHBOARD_PORT";
117531
+ var DASHBOARD_NO_OPEN_ENV = "AMETYST_DASHBOARD_NO_OPEN";
117532
+ var NO_DASHBOARD_MESSAGE = "no dashboard on this task \u2014 ask your agent to create one";
117533
+ var MAX_PORT_RETRIES = 20;
117534
+ var SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
117535
+ function parseManifestFiles(manifest) {
117536
+ if (typeof manifest !== "string" || !manifest.trim()) return [];
117537
+ let parsed;
117538
+ try {
117539
+ parsed = JSON.parse(manifest);
117540
+ } catch {
117541
+ return [];
117542
+ }
117543
+ const list2 = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.files) ? parsed.files : [];
117544
+ return list2.filter((f) => typeof f === "string" && SAFE_NAME.test(f));
117498
117545
  }
117499
- function isSafeKey(key) {
117500
- if (key.length === 0 || key.length > KEY_MAX_LENGTH) return false;
117501
- if (key === "." || key === "..") return false;
117502
- if (key.startsWith(".")) return false;
117503
- if (key.includes("/") || key.includes("\\") || key.includes("\0")) return false;
117504
- return true;
117546
+ function dashboardPort(env = process.env) {
117547
+ const raw = Number(env[DASHBOARD_PORT_ENV]);
117548
+ return Number.isInteger(raw) && raw > 0 && raw < 65536 ? raw : DEFAULT_PORT;
117505
117549
  }
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;
117550
+ function openDashboardInBrowser(url2, deps = {}) {
117551
+ const env = deps.env ?? process.env;
117552
+ if (env[DASHBOARD_NO_OPEN_ENV] === "1") return false;
117553
+ const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
117554
+ if (!isTTY) return false;
117555
+ const platform = deps.platform ?? process.platform;
117556
+ const cmd = platform === "darwin" ? "open" : platform === "linux" ? "xdg-open" : null;
117557
+ if (!cmd) return false;
117558
+ const spawn4 = deps.spawn ?? realSpawn;
117559
+ try {
117560
+ const child = spawn4(cmd, [url2], { stdio: "ignore", detached: true });
117561
+ child.once?.("error", () => {
117562
+ });
117563
+ child.unref?.();
117564
+ return true;
117565
+ } catch {
117566
+ return false;
117516
117567
  }
117517
117568
  }
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
- };
117525
- }
117526
- if (typeof value2 !== "object") {
117527
- return { ok: false, error: "stateDocs must be a JSON object with `docs` and `records` arrays, or null." };
117528
- }
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.` };
117533
- }
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)." };
117536
- }
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}.` };
117539
- }
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}.` };
117542
- }
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}.` };
117548
- }
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}.` };
117553
- }
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).` };
117556
- }
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.` };
117559
- }
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).` };
117563
- }
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".` };
117567
- }
117568
- docs.push({ key: d.key, scope: d.scope });
117569
- }
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}.` };
117575
- }
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}.` };
117580
- }
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
- };
117586
- }
117587
- if (seenKinds.has(r.kind)) {
117588
- return { ok: false, error: `stateDocs.records declares "${r.kind}" more than once.` };
117569
+ function startDashboardServer(args) {
117570
+ const html = args.loop.dashboardHtml;
117571
+ if (typeof html !== "string" || !html) return Promise.resolve(null);
117572
+ const fs = args.deps?.fs ?? realFs;
117573
+ const log = args.deps?.log ?? ((line) => console.error(line));
117574
+ const make = args.deps?.createServer ?? createServer2;
117575
+ const basePort = args.port ?? dashboardPort();
117576
+ const files = parseManifestFiles(args.loop.dashboardManifest);
117577
+ const handler = (req, res) => {
117578
+ try {
117579
+ const url2 = (req.url ?? "/").split("?")[0];
117580
+ if (req.method === "GET" && url2 === "/") {
117581
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
117582
+ res.end(html);
117583
+ return;
117584
+ }
117585
+ if (req.method === "GET" && url2 === "/data") {
117586
+ const data = {};
117587
+ for (const name of files) {
117588
+ try {
117589
+ const p = join9(args.loopDir, name);
117590
+ if (fs.existsSync(p)) data[name] = fs.readFileSync(p, "utf-8");
117591
+ } catch {
117592
+ }
117593
+ }
117594
+ const state = {};
117595
+ try {
117596
+ const p = join9(args.loopDir, ".state", "fires.jsonl");
117597
+ if (fs.existsSync(p)) state["fires.jsonl"] = fs.readFileSync(p, "utf-8");
117598
+ } catch {
117599
+ }
117600
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
117601
+ res.end(
117602
+ JSON.stringify({
117603
+ loop: args.loop.slug,
117604
+ files: data,
117605
+ state,
117606
+ mode: "live",
117607
+ asOf: (/* @__PURE__ */ new Date()).toISOString()
117608
+ })
117609
+ );
117610
+ return;
117611
+ }
117612
+ res.writeHead(404, { "content-type": "text/plain" });
117613
+ res.end("not found");
117614
+ } catch {
117615
+ try {
117616
+ res.writeHead(500);
117617
+ res.end();
117618
+ } catch {
117619
+ }
117589
117620
  }
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".` };
117621
+ };
117622
+ const bind = (port) => new Promise((resolve) => {
117623
+ const server2 = make(handler);
117624
+ server2.once("error", (err) => {
117625
+ try {
117626
+ server2.close();
117627
+ } catch {
117628
+ }
117629
+ resolve({ ok: false, err });
117630
+ });
117631
+ server2.listen(port, "127.0.0.1", () => {
117632
+ server2.unref();
117633
+ resolve({
117634
+ ok: true,
117635
+ handle: {
117636
+ server: server2,
117637
+ port,
117638
+ close() {
117639
+ try {
117640
+ server2.close();
117641
+ server2.closeAllConnections?.();
117642
+ } catch {
117643
+ }
117644
+ }
117645
+ }
117646
+ });
117647
+ });
117648
+ });
117649
+ return (async () => {
117650
+ const lastPort = Math.min(basePort + MAX_PORT_RETRIES, 65535);
117651
+ for (let port = basePort; port <= lastPort; port++) {
117652
+ const attempt = await bind(port);
117653
+ if (attempt.ok) {
117654
+ log(` loop dashboard: http://localhost:${attempt.handle.port}`);
117655
+ return attempt.handle;
117656
+ }
117657
+ if (attempt.err.code !== "EADDRINUSE") {
117658
+ log(
117659
+ ` (loop dashboard not started on :${port} \u2014 ${attempt.err.code ?? attempt.err.message}; run continues)`
117660
+ );
117661
+ return null;
117662
+ }
117593
117663
  }
117594
- records.push({ kind: r.kind, scope: r.scope });
117595
- }
117596
- return { ok: true, manifest: { docs, records } };
117664
+ log(` (loop dashboard not started \u2014 :${basePort}-${lastPort} all in use; run continues)`);
117665
+ return null;
117666
+ })();
117597
117667
  }
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 });
117605
- }
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 });
117611
- }
117612
- return { docs, records };
117668
+
117669
+ // src/mcp-server/task-run-mode.ts
117670
+ init_esm_shims();
117671
+ var ACCEPTED_RUN_MODES = ["in-chat", "headless"];
117672
+ var FRONTMATTER_SCAN_LIMIT = 8192;
117673
+ function normalizeRunMode(raw) {
117674
+ if (typeof raw !== "string") return void 0;
117675
+ const v = raw.trim().toLowerCase();
117676
+ if (v === "headless") return "headless";
117677
+ if (v === "in-chat" || v === "inchat" || v === "in_chat") return "in-chat";
117678
+ return void 0;
117613
117679
  }
117614
- async function readTaskManifest(sdk, apiKey, slug) {
117680
+ function describeProvided(raw) {
117681
+ if (typeof raw === "string") return raw.trim();
117615
117682
  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);
117683
+ return JSON.stringify(raw) ?? String(raw);
117619
117684
  } catch {
117620
- return void 0;
117685
+ return String(raw);
117621
117686
  }
117622
117687
  }
117623
- function declaredDocScope(manifest, key) {
117624
- return manifest?.docs.find((d) => d.key.toLowerCase() === key.toLowerCase())?.scope;
117688
+ function classifyRunModeArgument(raw) {
117689
+ if (raw === void 0 || raw === null) return { kind: "omitted" };
117690
+ if (typeof raw === "string" && raw.trim() === "") return { kind: "omitted" };
117691
+ const mode2 = normalizeRunMode(raw);
117692
+ if (mode2) return { kind: "valid", mode: mode2 };
117693
+ return { kind: "invalid", provided: describeProvided(raw) };
117625
117694
  }
117626
- function declaredRecordScope(manifest, kind) {
117627
- return manifest?.records.find((r) => r.kind === kind)?.scope;
117695
+ function unquoteScalar(raw) {
117696
+ let v = raw.trim();
117697
+ const comment = v.match(/(?:^|\s)#.*$/);
117698
+ if (comment) v = v.slice(0, comment.index === 0 ? 0 : comment.index).trim();
117699
+ if (v.length >= 2 && (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'"))) {
117700
+ v = v.slice(1, -1).trim();
117701
+ }
117702
+ return v;
117628
117703
  }
117629
- function declaredRecordKinds(manifest) {
117630
- if (manifest === void 0) return void 0;
117631
- return manifest?.records.map((r) => r.kind) ?? [];
117704
+ function parseDefaultRunMode(body) {
117705
+ if (typeof body !== "string" || !body) return void 0;
117706
+ const head = body.replace(/^\uFEFF/, "").slice(0, FRONTMATTER_SCAN_LIMIT);
117707
+ const opener = head.match(/^---[ \t]*\r?\n/);
117708
+ if (!opener) return void 0;
117709
+ const rest = head.slice(opener[0].length);
117710
+ const closer = rest.search(/^(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/m);
117711
+ if (closer < 0) return void 0;
117712
+ const block = rest.slice(0, closer);
117713
+ const hit = block.match(/^defaultRunMode[ \t]*:[ \t]*(.*)$/m);
117714
+ if (!hit) return void 0;
117715
+ return normalizeRunMode(unquoteScalar(hit[1] ?? ""));
117632
117716
  }
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)";
117717
+ function resolveRunMode(explicit, body) {
117718
+ const arg = classifyRunModeArgument(explicit);
117719
+ if (arg.kind === "invalid") {
117720
+ return { ok: false, error: "invalid_mode", provided: arg.provided, accepted: ACCEPTED_RUN_MODES };
117721
+ }
117722
+ if (arg.kind === "valid") return { ok: true, mode: arg.mode, source: "explicit" };
117723
+ const fromBody = parseDefaultRunMode(body);
117724
+ if (fromBody) return { ok: true, mode: fromBody, source: "frontmatter" };
117725
+ return { ok: true, mode: "in-chat", source: "default" };
117726
+ }
117727
+
117728
+ // src/loops/estimate.ts
117729
+ init_esm_shims();
117730
+ function estimateBlastRadius(loop2) {
117731
+ const g = loop2.graphJson ?? {};
117732
+ const nodes = Array.isArray(g.nodes) ? g.nodes : [];
117733
+ const steps = nodes.length;
117734
+ const paidSteps = nodes.filter(
117735
+ (n) => n?.type === "spend" || n?.data?.paid === true || n?.paid === true
117736
+ ).length;
117737
+ const costHints = nodes.map((n) => Number(n?.data?.estCostEur ?? n?.estCostEur)).filter((x) => !Number.isNaN(x));
117738
+ const estCostEur = costHints.length ? costHints.reduce((a, b) => a + b, 0) : null;
117739
+ return { steps, paidSteps, estCostEur };
117641
117740
  }
117642
117741
 
117643
- // src/loops/state-docs.ts
117644
- var RESERVED_FIRE_FILENAMES = [
117645
- "SKILL.md",
117742
+ // src/loops/dashboard-template.ts
117743
+ init_esm_shims();
117744
+ var DEFAULT_DASHBOARD_FILES = [
117646
117745
  "VISION.md",
117647
117746
  "CONSTRAINTS.md",
117648
- "README.md",
117747
+ "QUEUE.md",
117649
117748
  "STATUS.md",
117650
- "dashboard.html",
117651
- "dashboard.manifest.json"
117749
+ "README.md",
117750
+ "rounds.jsonl"
117652
117751
  ];
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
- }
117752
+ function parseProcessSpec(manifest) {
117753
+ if (typeof manifest !== "string" || !manifest.trim()) return null;
117754
+ let parsed;
117755
+ try {
117756
+ parsed = JSON.parse(manifest);
117757
+ } catch {
117758
+ return null;
117700
117759
  }
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;
117760
+ const raw = parsed?.processSpec;
117761
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
117762
+ const spec = raw;
117763
+ const stages = (Array.isArray(spec.stages) ? spec.stages : []).map((s) => {
117764
+ if (typeof s === "string" && s.trim()) return { label: s.trim() };
117765
+ if (s && typeof s === "object" && !Array.isArray(s)) {
117766
+ const o = s;
117767
+ if (typeof o.label === "string" && o.label.trim()) {
117768
+ return {
117769
+ label: o.label.trim(),
117770
+ ...typeof o.id === "string" && o.id.trim() ? { id: o.id.trim() } : {},
117771
+ ...typeof o.detail === "string" && o.detail.trim() ? { detail: o.detail.trim() } : {}
117772
+ };
117773
+ }
117721
117774
  }
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 };
117775
+ return null;
117776
+ }).filter((s) => s !== null);
117777
+ const strings = (v) => (Array.isArray(v) ? v : []).filter((x) => typeof x === "string" && x.trim() !== "");
117778
+ const out = {
117779
+ stages,
117780
+ inputs: strings(spec.inputs),
117781
+ outputs: strings(spec.outputs),
117782
+ ...typeof spec.title === "string" && spec.title.trim() ? { title: spec.title.trim() } : {}
117783
+ };
117784
+ return stages.length || out.inputs.length || out.outputs.length ? out : null;
117726
117785
  }
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(", ")})` : ""}`;
117786
+ function defaultDashboardManifest() {
117787
+ return JSON.stringify({ files: [...DEFAULT_DASHBOARD_FILES] });
117733
117788
  }
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" };
117789
+ function embedJson(value2) {
117790
+ return JSON.stringify(value2).replace(/</g, "\\u003c");
117739
117791
  }
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 });
117792
+ function escapeHtml(s) {
117793
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
117794
+ }
117795
+ function renderLoopDashboardTemplate(args) {
117796
+ const title = escapeHtml(args.processSpec?.title ?? args.slug);
117797
+ const seed = embedJson({
117798
+ slug: args.slug,
117799
+ descriptionShort: args.descriptionShort ?? "",
117800
+ processSpec: args.processSpec ?? null
117801
+ });
117802
+ return `<!doctype html>
117803
+ <html lang="en">
117804
+ <head>
117805
+ <meta charset="utf-8">
117806
+ <meta name="viewport" content="width=device-width, initial-scale=1">
117807
+ <title>${title} \u2014 loop dashboard</title>
117808
+ <style>
117809
+ :root { --bg:#f7f7fb; --panel:#fff; --border:#e3e3ee; --muted:#6b6b80; --accent:#5b5bd6; --ok:#1a7f37; --bad:#b42318; }
117810
+ * { box-sizing: border-box; }
117811
+ body { margin:0; font:14px/1.5 -apple-system, "Segoe UI", Roboto, sans-serif; background:var(--bg); color:#1d1d2b; padding:24px; }
117812
+ h1 { font-size:20px; margin:0 0 4px; }
117813
+ h2 { font-size:14px; margin:24px 0 8px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
117814
+ .sub { color:var(--muted); margin:0 0 16px; }
117815
+ .panel { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px; }
117816
+ .map { display:flex; flex-wrap:wrap; align-items:stretch; gap:8px; }
117817
+ .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; }
117818
+ .stage .label { font-weight:600; }
117819
+ .stage .detail { color:var(--muted); font-size:12px; margin-top:2px; }
117820
+ .arrow { align-self:center; color:var(--muted); }
117821
+ .io { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
117822
+ ul { margin:6px 0 0; padding-left:18px; }
117823
+ table { width:100%; border-collapse:collapse; background:var(--panel); border:1px solid var(--border); border-radius:12px; overflow:hidden; }
117824
+ th, td { text-align:left; padding:8px 12px; border-top:1px solid var(--border); font-size:13px; vertical-align:top; }
117825
+ thead th { border-top:none; background:var(--bg); color:var(--muted); font-weight:600; }
117826
+ .ok { color:var(--ok); } .bad { color:var(--bad); }
117827
+ details { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:10px 14px; margin-bottom:8px; }
117828
+ summary { cursor:pointer; font-weight:600; }
117829
+ pre { overflow-x:auto; font-size:12px; background:var(--bg); border-radius:8px; padding:10px; }
117830
+ .empty { color:var(--muted); font-style:italic; }
117831
+ #live { font-size:12px; color:var(--muted); float:right; }
117832
+ </style>
117833
+ </head>
117834
+ <body>
117835
+ <span id="live">loading\u2026</span>
117836
+ <h1 id="title"></h1>
117837
+ <p class="sub" id="desc"></p>
117838
+
117839
+ <h2>Process</h2>
117840
+ <div id="map" class="map panel"></div>
117841
+
117842
+ <h2>Inputs &amp; outputs</h2>
117843
+ <div class="io">
117844
+ <div class="panel"><strong>Inputs</strong><ul id="inputs"></ul></div>
117845
+ <div class="panel"><strong>Outputs</strong><ul id="outputs"></ul></div>
117846
+ </div>
117847
+
117848
+ <h2>Rounds</h2>
117849
+ <div id="rounds"></div>
117850
+
117851
+ <h2>Files</h2>
117852
+ <div id="files"></div>
117853
+
117854
+ <script type="application/json" id="loop-seed">${seed}</script>
117855
+ <script>
117856
+ (function () {
117857
+ "use strict";
117858
+ var seed = JSON.parse(document.getElementById("loop-seed").textContent);
117859
+ document.getElementById("title").textContent = (seed.processSpec && seed.processSpec.title) || seed.slug;
117860
+ document.getElementById("desc").textContent = seed.descriptionShort || "";
117861
+ document.title = seed.slug + " \u2014 loop dashboard";
117862
+
117863
+ function el(tag, cls, text) {
117864
+ var e = document.createElement(tag);
117865
+ if (cls) e.className = cls;
117866
+ if (text !== undefined) e.textContent = text;
117867
+ return e;
117868
+ }
117869
+
117870
+ // \u2500\u2500 Process map + IO from the embedded spec (static \u2014 rendered once). \u2500\u2500
117871
+ var map = document.getElementById("map");
117872
+ var spec = seed.processSpec;
117873
+ if (spec && spec.stages && spec.stages.length) {
117874
+ spec.stages.forEach(function (s, i) {
117875
+ if (i > 0) map.appendChild(el("div", "arrow", "\\u2192"));
117876
+ var box = el("div", "stage");
117877
+ box.appendChild(el("div", "label", s.label));
117878
+ if (s.detail) box.appendChild(el("div", "detail", s.detail));
117879
+ map.appendChild(box);
117880
+ });
117881
+ } else {
117882
+ map.appendChild(el("span", "empty", "No process spec on this loop \\u2014 live files and rounds below."));
117776
117883
  }
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";
117884
+ function fillList(id, items) {
117885
+ var ul = document.getElementById(id);
117886
+ ul.textContent = "";
117887
+ if (!items || !items.length) { ul.appendChild(el("li", "empty", "\\u2014")); return; }
117888
+ items.forEach(function (x) { ul.appendChild(el("li", null, x)); });
117786
117889
  }
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";
117890
+ fillList("inputs", spec && spec.inputs);
117891
+ fillList("outputs", spec && spec.outputs);
117892
+
117893
+ // \u2500\u2500 Live data: rounds + files, refreshed from GET /data. \u2500\u2500
117894
+ function parseJsonl(text) {
117895
+ var rows = [];
117896
+ (text || "").split("\\n").forEach(function (line) {
117897
+ line = line.trim();
117898
+ if (!line) return;
117899
+ try { rows.push(JSON.parse(line)); } catch (e) { /* skip malformed line */ }
117900
+ });
117901
+ return rows;
117789
117902
  }
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;
117903
+
117904
+ function renderRounds(fires, rounds) {
117905
+ var host = document.getElementById("rounds");
117906
+ host.textContent = "";
117907
+ if (!fires.length && !rounds.length) {
117908
+ var p = el("div", "panel"); p.appendChild(el("span", "empty", "No rounds yet \\u2014 this fills in as the loop runs."));
117909
+ host.appendChild(p);
117910
+ return;
117828
117911
  }
117829
- const put = await sdk.loops.memory.putDoc(apiKey, slug, doc.key, {
117830
- content: merged.next,
117831
- scope: doc.scope
117912
+ var table = document.createElement("table");
117913
+ var thead = document.createElement("thead");
117914
+ var hr = document.createElement("tr");
117915
+ ["when", "duration", "turns", "tokens", "exit", "round detail (rounds.jsonl)"].forEach(function (h) {
117916
+ hr.appendChild(el("th", null, h));
117832
117917
  });
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;
117918
+ thead.appendChild(hr); table.appendChild(thead);
117919
+ var tbody = document.createElement("tbody");
117920
+ var n = Math.max(fires.length, rounds.length);
117921
+ for (var i = n - 1; i >= 0; i--) { // newest first
117922
+ var f = fires[i] || {};
117923
+ var r = rounds[i];
117924
+ var tr = document.createElement("tr");
117925
+ tr.appendChild(el("td", null, f.ts || (r && r.ts) || "\\u2014"));
117926
+ tr.appendChild(el("td", null, f.duration_s != null ? f.duration_s + "s" : "\\u2014"));
117927
+ tr.appendChild(el("td", null, f.turns != null ? String(f.turns) : "\\u2014"));
117928
+ tr.appendChild(el("td", null, f.tokens_total != null ? Number(f.tokens_total).toLocaleString() : "\\u2014"));
117929
+ tr.appendChild(el("td", f.exit === 0 ? "ok" : f.exit != null ? "bad" : null, f.exit != null ? String(f.exit) : "\\u2014"));
117930
+ var detail = el("td");
117931
+ if (f.launch_failed) { detail.textContent = "launch failed \\u2014 " + (f.reason || "unknown reason"); }
117932
+ else if (r) { var pre = document.createElement("pre"); pre.textContent = JSON.stringify(r, null, 1); detail.appendChild(pre); }
117933
+ else detail.textContent = "\\u2014";
117934
+ tr.appendChild(detail);
117935
+ tbody.appendChild(tr);
117840
117936
  }
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;
117937
+ table.appendChild(tbody);
117938
+ host.appendChild(table);
117939
+ }
117940
+
117941
+ function renderFiles(files) {
117942
+ var host = document.getElementById("files");
117943
+ host.textContent = "";
117944
+ var names = Object.keys(files || {}).filter(function (n) { return n !== "rounds.jsonl"; });
117945
+ if (!names.length) { var p = el("div", "panel"); p.appendChild(el("span", "empty", "No files surfaced by the manifest.")); host.appendChild(p); return; }
117946
+ names.forEach(function (name) {
117947
+ var d = document.createElement("details");
117948
+ d.appendChild(el("summary", null, name));
117949
+ var pre = document.createElement("pre");
117950
+ pre.textContent = files[name];
117951
+ d.appendChild(pre);
117952
+ host.appendChild(d);
117953
+ });
117954
+ }
117955
+
117956
+ function refresh() {
117957
+ fetch("/data").then(function (res) { return res.json(); }).then(function (data) {
117958
+ var fires = parseJsonl(data.state && data.state["fires.jsonl"]);
117959
+ var rounds = parseJsonl(data.files && data.files["rounds.jsonl"]);
117960
+ renderRounds(fires, rounds);
117961
+ renderFiles(data.files);
117962
+ document.getElementById("live").textContent = "updated " + new Date().toLocaleTimeString();
117963
+ }).catch(function () {
117964
+ document.getElementById("live").textContent = "server stopped";
117965
+ });
117966
+ }
117967
+ refresh();
117968
+ setInterval(refresh, 5000);
117969
+ })();
117970
+ </script>
117971
+ </body>
117972
+ </html>
117973
+ `;
117974
+ }
117975
+ function injectDefaultDashboard(loop2) {
117976
+ const hasHtml = typeof loop2.dashboardHtml === "string" && loop2.dashboardHtml.trim() !== "";
117977
+ if (hasHtml) return;
117978
+ if (loop2.dashboardManifest && typeof loop2.dashboardManifest === "object") {
117979
+ try {
117980
+ loop2.dashboardManifest = JSON.stringify(loop2.dashboardManifest);
117981
+ } catch {
117845
117982
  }
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
117983
  }
117856
- return outcomes;
117984
+ const manifest = typeof loop2.dashboardManifest === "string" ? loop2.dashboardManifest : void 0;
117985
+ loop2.dashboardHtml = renderLoopDashboardTemplate({
117986
+ slug: typeof loop2.slug === "string" ? loop2.slug : "loop",
117987
+ descriptionShort: typeof loop2.descriptionShort === "string" ? loop2.descriptionShort : void 0,
117988
+ processSpec: parseProcessSpec(manifest)
117989
+ });
117990
+ if (!manifest || !manifest.trim()) loop2.dashboardManifest = defaultDashboardManifest();
117857
117991
  }
117858
117992
 
117859
117993
  // src/loops/memory-verbs.ts
@@ -118178,6 +118312,27 @@ async function taskMemoryArchiveVerb(rawSlug, opts) {
118178
118312
  );
118179
118313
  }
118180
118314
 
118315
+ // src/mcp-server/memory-scope.ts
118316
+ init_esm_shims();
118317
+ 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.";
118318
+ function memorySummaryLine(manifest) {
118319
+ const normalized = normalizeMemoryManifest(manifest);
118320
+ const entries = normalized ? [...normalized.docs, ...normalized.records] : [];
118321
+ const global2 = entries.filter((e) => e.scope === "shared").length;
118322
+ const perMember = entries.filter((e) => e.scope === "member").length;
118323
+ if (global2 > 0 && perMember > 0) return `Memory: mixed \u2014 ${global2} global \xB7 ${perMember} per member`;
118324
+ if (global2 > 0) return "Memory: global";
118325
+ if (perMember > 0) return "Memory: per member";
118326
+ return "Memory: none";
118327
+ }
118328
+ function defaultMemoryManifest() {
118329
+ return { docs: [], records: [{ kind: "run", scope: "member" }] };
118330
+ }
118331
+ var MEMORY_DEFAULTED_SUFFIX = " (defaulted \u2014 no manifest declared)";
118332
+ function declaresNoMemory(manifest) {
118333
+ return memorySummaryLine(manifest ?? null) === "Memory: none";
118334
+ }
118335
+
118181
118336
  // src/mcp-server/prompt-descriptors.ts
118182
118337
  init_esm_shims();
118183
118338
 
@@ -118456,6 +118611,15 @@ function buildReceipt(input) {
118456
118611
  if (typeof sent.draft === "boolean") {
118457
118612
  out.push({ field: "draft", action: classify(sent.draft, prev ? prev.draft : void 0) });
118458
118613
  }
118614
+ if (sent.stateDocs !== void 0) {
118615
+ const sentJson = JSON.stringify(sent.stateDocs ?? null);
118616
+ const prevHas = prev !== void 0 && prev.stateDocs !== void 0;
118617
+ const prevJson = prevHas ? JSON.stringify(prev.stateDocs ?? null) : void 0;
118618
+ const action = mode2 === "created" ? "set" : !previous || !previous.available ? "sent" : prevJson === void 0 ? "changed" : sentJson === prevJson ? "unchanged" : "changed";
118619
+ const rec = { field: "stateDocs", action, bytes: Buffer.byteLength(sentJson, "utf8") };
118620
+ if (prevJson !== void 0) rec.previousBytes = Buffer.byteLength(prevJson, "utf8");
118621
+ out.push(rec);
118622
+ }
118459
118623
  return out;
118460
118624
  }
118461
118625
  function deriveChanges(input, receipt) {
@@ -118542,7 +118706,15 @@ function enforceResponseCap(payload) {
118542
118706
  }
118543
118707
  if (size6(out) > MAX_RESPONSE_CHARS && Array.isArray(out.receipt?.fields)) {
118544
118708
  const r = out.receipt;
118545
- out.receipt = { fieldCount: r.fields.length, note: "receipt omitted \u2014 response cap" };
118709
+ out.receipt = {
118710
+ fieldCount: r.fields.length,
118711
+ note: "receipt omitted \u2014 response cap",
118712
+ // WHERE IT LANDED SURVIVES THE CAP. It is two short strings, and it is the
118713
+ // one line on this receipt whose absence was an incident: shrinking the
118714
+ // response must not be how a caller stops being told which workspace it
118715
+ // just wrote to.
118716
+ ...r.workspace !== void 0 ? { workspace: r.workspace } : {}
118717
+ };
118546
118718
  }
118547
118719
  if (size6(out) > MAX_RESPONSE_CHARS) {
118548
118720
  out.summary = TRUNCATION_MARKER;
@@ -118553,7 +118725,7 @@ function enforceResponseCap(payload) {
118553
118725
 
118554
118726
  // src/mcp-server/delegate-tools.ts
118555
118727
  init_esm_shims();
118556
- import { existsSync as existsSync9, statSync as statSync2 } from "fs";
118728
+ import { existsSync as existsSync10, statSync as statSync2 } from "fs";
118557
118729
  import { z as z3 } from "zod";
118558
118730
 
118559
118731
  // src/delegate/jobs.ts
@@ -121047,7 +121219,7 @@ function registerDelegateTools(deps) {
121047
121219
  const validated = validateStartInput(params, {
121048
121220
  isDirectory: (path2) => {
121049
121221
  try {
121050
- return existsSync9(path2) && statSync2(path2).isDirectory();
121222
+ return existsSync10(path2) && statSync2(path2).isDirectory();
121051
121223
  } catch {
121052
121224
  return false;
121053
121225
  }
@@ -121297,6 +121469,82 @@ function applyResolvedMethod(url2, method, inputs) {
121297
121469
 
121298
121470
  // src/mcp-server/index.ts
121299
121471
  init_plaintext_vault();
121472
+ init_paths();
121473
+ init_resolve();
121474
+
121475
+ // src/mcp-server/identity-reload.ts
121476
+ init_esm_shims();
121477
+ import { readFileSync as readFileSync12, statSync as statSync3 } from "fs";
121478
+ var NO_VAULT = "absent";
121479
+ var IDENTITY_CHECK_THROTTLE_MS = 1e3;
121480
+ function fingerprintVault(stat) {
121481
+ return stat ? `${stat.mtimeMs}:${stat.size}` : NO_VAULT;
121482
+ }
121483
+ function statVaultFile(path2) {
121484
+ try {
121485
+ const s = statSync3(path2);
121486
+ return { mtimeMs: s.mtimeMs, size: s.size };
121487
+ } catch {
121488
+ return null;
121489
+ }
121490
+ }
121491
+ var IdentityWatch = class {
121492
+ vaultPath;
121493
+ now;
121494
+ throttleMs;
121495
+ stat;
121496
+ /** False for an env-supplied key: `poll` is then a guaranteed no-op. */
121497
+ armed;
121498
+ fingerprint;
121499
+ lastCheckedAt = null;
121500
+ constructor(options) {
121501
+ this.vaultPath = options.vaultPath;
121502
+ this.now = options.now ?? Date.now;
121503
+ this.throttleMs = options.throttleMs ?? IDENTITY_CHECK_THROTTLE_MS;
121504
+ this.stat = options.stat ?? statVaultFile;
121505
+ this.armed = options.source !== "env";
121506
+ this.fingerprint = fingerprintVault(this.stat(this.vaultPath));
121507
+ }
121508
+ /** The fingerprint this watch currently considers current. */
121509
+ currentFingerprint() {
121510
+ return this.fingerprint;
121511
+ }
121512
+ /**
121513
+ * One throttled `stat`. Returns the change when the vault file differs from
121514
+ * the fingerprint on record — and ADOPTS it in the same step, so a reload the
121515
+ * caller then fails to apply is not retried on every subsequent tool call.
121516
+ * Returns `null` when nothing changed, when the throttle window is still open,
121517
+ * or when the watch is not armed.
121518
+ */
121519
+ poll() {
121520
+ if (!this.armed) return null;
121521
+ const at = this.now();
121522
+ if (this.lastCheckedAt !== null && at - this.lastCheckedAt < this.throttleMs) return null;
121523
+ this.lastCheckedAt = at;
121524
+ const next = fingerprintVault(this.stat(this.vaultPath));
121525
+ if (next === this.fingerprint) return null;
121526
+ const from14 = this.fingerprint;
121527
+ this.fingerprint = next;
121528
+ return { from: from14, to: next };
121529
+ }
121530
+ /**
121531
+ * The vault bytes this watch is pointed at, or `null` when there is none.
121532
+ *
121533
+ * Reading THROUGH the watch rather than through `loadVault()` is deliberate:
121534
+ * the freshness check and the reload must address the same file, and having
121535
+ * one owner of the path is what makes that true by construction instead of by
121536
+ * two constants agreeing.
121537
+ */
121538
+ readVault() {
121539
+ try {
121540
+ return readFileSync12(this.vaultPath, "utf-8");
121541
+ } catch {
121542
+ return null;
121543
+ }
121544
+ }
121545
+ };
121546
+
121547
+ // src/mcp-server/index.ts
121300
121548
  function buildDeclaredConnectorsSentence(catalog, unreadable) {
121301
121549
  const read = catalog === void 0 ? { connectors: cachedCatalogOrEmpty(), unreadable: mirrorGap() } : { connectors: catalog, unreadable: unreadable ?? [] };
121302
121550
  const gap = describeCatalogGap(read.unreadable);
@@ -121428,6 +121676,8 @@ var loopIndexCache = null;
121428
121676
  var taskIndexCache = null;
121429
121677
  var cardCounts = null;
121430
121678
  var lastNudgeAtCall = null;
121679
+ var WORKSPACE_IDENTITY_TTL_MS = 6e4;
121680
+ var workspaceIdentityCache = null;
121431
121681
  var sessionId = randomUUID4();
121432
121682
  var mcpEventLogger = null;
121433
121683
  var PROCESSED_APPROVAL_IDS_CAP = 50;
@@ -121524,6 +121774,83 @@ function maybeAppendCardNudge(res, toolName, toolCallIndex) {
121524
121774
  if (next !== res) lastNudgeAtCall = toolCallIndex;
121525
121775
  return next;
121526
121776
  }
121777
+ async function resolveWorkspaceIdentity() {
121778
+ const apiKey = currentCredentials.apiKey;
121779
+ if (!apiKey?.trim()) return { companyName: null, employeeName: null };
121780
+ const now = Date.now();
121781
+ if (workspaceIdentityCache && now - workspaceIdentityCache.at < WORKSPACE_IDENTITY_TTL_MS) {
121782
+ return workspaceIdentityCache.value;
121783
+ }
121784
+ let value2 = { companyName: null, employeeName: null };
121785
+ try {
121786
+ const sdk = await getSDK();
121787
+ const me = await sdk.virtualWalletsManagers.getManagerMe(apiKey);
121788
+ value2 = {
121789
+ companyName: typeof me?.companyName === "string" ? me.companyName : null,
121790
+ employeeName: typeof me?.employeeName === "string" ? me.employeeName : null
121791
+ };
121792
+ } catch (err) {
121793
+ console.error(
121794
+ `\u26A0\uFE0F workspace identity read failed \u2014 reporting nulls: ${err instanceof Error ? err.message : String(err)}`
121795
+ );
121796
+ }
121797
+ workspaceIdentityCache = { at: now, value: value2 };
121798
+ return value2;
121799
+ }
121800
+ var identityWatch = null;
121801
+ function dropIdentityScopedCaches() {
121802
+ walletsCache = null;
121803
+ policiesCache = null;
121804
+ transactionsCache = null;
121805
+ allowlistCache = null;
121806
+ capabilityIndexCache = null;
121807
+ compoundIndexCache = null;
121808
+ loopIndexCache = null;
121809
+ taskIndexCache = null;
121810
+ cardCounts = null;
121811
+ servicesDiscovered = null;
121812
+ workspaceIdentityCache = null;
121813
+ endpointSessionCache.clear();
121814
+ }
121815
+ async function applyIdentityReload() {
121816
+ const watch = identityWatch;
121817
+ if (!watch) return;
121818
+ const { readVaultAddress: readVaultAddress2 } = await Promise.resolve().then(() => (init_core(), core_exports));
121819
+ const vault = watch.readVault();
121820
+ const nextEoa = vault ? readVaultAddress2(vault) : "";
121821
+ const resolved = await resolveApiKey(cliConfig?.credentialStore ?? "keychain");
121822
+ const previousKey = currentCredentials.apiKey ?? "";
121823
+ const previousEoa = currentCredentials.eoaAddress ?? "";
121824
+ currentCredentials.walletKeystoreJson = vault ?? "";
121825
+ currentCredentials.eoaAddress = nextEoa;
121826
+ currentCredentials.signerAddress = nextEoa;
121827
+ if (resolved.apiKey) currentCredentials.apiKey = resolved.apiKey;
121828
+ refreshPlaintextVaultMode();
121829
+ clearWalletScopedCredentials(currentCredentials);
121830
+ delete currentCredentials.passphrase;
121831
+ delete currentCredentials.wrongPassphraseAttempts;
121832
+ delete currentCredentials.requiresNewPolicy;
121833
+ dropIdentityScopedCaches();
121834
+ console.error(
121835
+ `identity reloaded: ${maskApiKey(previousKey)} -> ${maskApiKey(currentCredentials.apiKey ?? "")}, eoa ${previousEoa || "(none)"} -> ${nextEoa || "(none)"}`
121836
+ );
121837
+ }
121838
+ async function ensureIdentityFresh() {
121839
+ let change = null;
121840
+ try {
121841
+ change = identityWatch?.poll() ?? null;
121842
+ } catch {
121843
+ return;
121844
+ }
121845
+ if (!change) return;
121846
+ try {
121847
+ await applyIdentityReload();
121848
+ } catch (err) {
121849
+ console.error(
121850
+ `\u26A0\uFE0F identity reload failed \u2014 continuing with the identity loaded at boot: ${err instanceof Error ? err.message : String(err)}`
121851
+ );
121852
+ }
121853
+ }
121527
121854
  var __registerTool = server.tool.bind(server);
121528
121855
  server.tool = (def, handler) => __registerTool(
121529
121856
  def,
@@ -121534,6 +121861,7 @@ server.tool = (def, handler) => __registerTool(
121534
121861
  // session, unconditionally. Only `severity: "block"` (an unsupported CLI)
121535
121862
  // still refuses, and that refusal happens inside the gate.
121536
121863
  runToolThroughInterstitialGate(def?.name, async () => {
121864
+ await ensureIdentityFresh();
121537
121865
  const res = await handler(params);
121538
121866
  maybeAutoShareOnToolCall();
121539
121867
  const withNudge = maybeAppendCardNudge(res, def?.name, toolCallCount);
@@ -122248,6 +122576,7 @@ async function initializeCredentials(walletKeystoreJson, eoaAddress, config) {
122248
122576
  currentCredentials.signerAddress = eoaAddress;
122249
122577
  currentCredentials.eoaAddress = eoaAddress;
122250
122578
  currentCredentials.apiKey = config.apiKey;
122579
+ identityWatch = new IdentityWatch({ vaultPath: VAULT_PATH, source: config.apiKeySource ?? null });
122251
122580
  mcpEventLogger = new McpEventLogger(
122252
122581
  () => getSDK(),
122253
122582
  () => currentCredentials.apiKey,
@@ -122467,17 +122796,6 @@ var approvalWaitConfig = (() => {
122467
122796
  delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
122468
122797
  };
122469
122798
  })();
122470
- function findCurrentApprovedWallet(wallets, eoaAddress, pendingWalletId) {
122471
- if (!Array.isArray(wallets)) return void 0;
122472
- if (pendingWalletId) {
122473
- return wallets.find(
122474
- (w) => String(w?.id) === String(pendingWalletId) && w?.status === "approved"
122475
- );
122476
- }
122477
- return wallets.find(
122478
- (w) => w?.address?.toLowerCase() === eoaAddress?.toLowerCase() && w?.status === "approved"
122479
- );
122480
- }
122481
122799
  async function tryResolvePendingApproval(probe) {
122482
122800
  if (currentCredentials.authorizationStatus === "approved") return true;
122483
122801
  if (currentCredentials.authorizationStatus !== "pending" || !currentCredentials.apiKey || !currentCredentials.eoaAddress) {
@@ -122615,11 +122933,20 @@ async function applyStartSessionUnlock(passphrase, resetRequested) {
122615
122933
  persistVault: persistVault2,
122616
122934
  wipeVault: wipeVault2,
122617
122935
  sdk,
122618
- fetchWallets: () => fetchVirtualWalletsFromBackend(currentCredentials.apiKey),
122936
+ // FORCED REFRESH. `start_session` verifies the session's grant against the
122937
+ // backend (see `runStartSession`), and a verification run against the cache
122938
+ // this process loaded at boot verifies nothing: a grant approved since boot
122939
+ // would be invisible to exactly the check that exists to find it.
122940
+ fetchWallets: () => fetchVirtualWalletsFromBackend(currentCredentials.apiKey, true),
122619
122941
  readPolicyActiveOnchain: buildPolicyActiveOnchainReader(sdk)
122620
122942
  }
122621
122943
  );
122622
- const { walletChanged } = mergeStartSessionCredentials(currentCredentials, result.credentials, result.responseKey);
122944
+ const { walletChanged } = mergeStartSessionCredentials(
122945
+ currentCredentials,
122946
+ result.credentials,
122947
+ result.responseKey,
122948
+ { grantChanged: result.grantChanged }
122949
+ );
122623
122950
  refreshPlaintextVaultMode();
122624
122951
  if (walletChanged) {
122625
122952
  walletsCache = null;
@@ -123086,7 +123413,7 @@ server.tool(
123086
123413
  server.tool(
123087
123414
  {
123088
123415
  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.",
123416
+ 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
123417
  inputs: []
123091
123418
  },
123092
123419
  async () => {
@@ -123118,10 +123445,9 @@ server.tool(
123118
123445
  fetchVirtualWalletsFromBackend(currentCredentials.apiKey, !refreshedFromBackend),
123119
123446
  fetchPoliciesFromBackend(currentCredentials.apiKey, true)
123120
123447
  ]);
123121
- const freshWallet = freshWallets.find(
123122
- (w) => w.address?.toLowerCase() === currentCredentials.eoaAddress?.toLowerCase()
123123
- );
123124
- if (freshWallet && freshWallet.status === "approved") {
123448
+ const freshWallet = selectNewestApprovedWallet(freshWallets, currentCredentials.eoaAddress);
123449
+ const freshWalletIsActiveGrant = String(freshWallet?.id) === String(currentCredentials.virtualWalletId);
123450
+ if (freshWallet && freshWalletIsActiveGrant) {
123125
123451
  if (freshWallet.policyAssociated != null) {
123126
123452
  currentCredentials.policyId = String(freshWallet.policyAssociated);
123127
123453
  }
@@ -123141,7 +123467,20 @@ server.tool(
123141
123467
  status: currentCredentials.authorizationStatus || "none",
123142
123468
  eoaAddress: currentCredentials.eoaAddress || null,
123143
123469
  walletAddress: currentCredentials.walletAddress || null,
123144
- kernelClientActive: !!currentCredentials.virtualWalletKernelAccountClient
123470
+ kernelClientActive: !!currentCredentials.virtualWalletKernelAccountClient,
123471
+ // WHICH GRANT THIS SESSION SIGNS WITH. The 2026-09-01 incident — three
123472
+ // approved rows for one EOA, every session silently signing with the oldest
123473
+ // — was invisible in every payload: diagnosing it took a nonce-key autopsy.
123474
+ // These two ids are the row `start_session` actually selected, so the next
123475
+ // stale grant is a field to read rather than a forensic exercise. Null until
123476
+ // a session has adopted one.
123477
+ activeVirtualWalletId: currentCredentials.virtualWalletId != null ? String(currentCredentials.virtualWalletId) : null,
123478
+ activePolicyId: currentCredentials.policyId != null ? String(currentCredentials.policyId) : null,
123479
+ // WHICH WORKSPACE THIS IS. The 2026-08-31 incident produced writes into
123480
+ // the wrong workspace with nothing in any payload naming one — an agent
123481
+ // reading wallet status could not have told, and neither could a human
123482
+ // reading the transcript afterwards. Nulls when `me` could not be read.
123483
+ workspace: await resolveWorkspaceIdentity()
123145
123484
  };
123146
123485
  if (policiesCache && currentCredentials.policyId) {
123147
123486
  const policy = policiesCache.find((p) => p.id === parseInt(currentCredentials.policyId, 10));
@@ -123150,7 +123489,7 @@ server.tool(
123150
123489
  }
123151
123490
  }
123152
123491
  if (walletsCache && currentCredentials.eoaAddress) {
123153
- const wallet = walletsCache.find(
123492
+ const wallet = selectNewestApprovedWallet(walletsCache, currentCredentials.eoaAddress) ?? walletsCache.find(
123154
123493
  (w) => w.address?.toLowerCase() === currentCredentials.eoaAddress?.toLowerCase()
123155
123494
  );
123156
123495
  if (wallet) {
@@ -123597,6 +123936,7 @@ async function resolveTasksCore(params, flavor) {
123597
123936
  if (slug && flavor.fetchManifest) {
123598
123937
  const manifest = await flavor.fetchManifest(sdk, currentCredentials.apiKey, slug);
123599
123938
  if (manifest) payload.stateDocs = manifest;
123939
+ if (manifest !== void 0) payload.memory = memorySummaryLine(manifest);
123600
123940
  }
123601
123941
  }
123602
123942
  }
@@ -123642,6 +123982,7 @@ function taskMemoryBlock(slug, docKey) {
123642
123982
  };
123643
123983
  }
123644
123984
  var liveDashboards = /* @__PURE__ */ new Map();
123985
+ var LIVE_DASHBOARD_WATCH_MS = 15e3;
123645
123986
  function closeAllLiveDashboards() {
123646
123987
  for (const h of liveDashboards.values()) {
123647
123988
  try {
@@ -123669,6 +124010,22 @@ async function startLiveDashboard(entity, dir) {
123669
124010
  });
123670
124011
  if (!handle) return null;
123671
124012
  liveDashboards.set(slug, handle);
124013
+ const watcher = setInterval(() => {
124014
+ if (liveDashboards.get(slug) !== handle) {
124015
+ clearInterval(watcher);
124016
+ return;
124017
+ }
124018
+ if (!existsSync12(dir)) {
124019
+ clearInterval(watcher);
124020
+ liveDashboards.delete(slug);
124021
+ try {
124022
+ handle.close();
124023
+ } catch {
124024
+ }
124025
+ console.error(`(live dashboard for ${slug} closed \u2014 run folder ${dir} is gone)`);
124026
+ }
124027
+ }, LIVE_DASHBOARD_WATCH_MS);
124028
+ watcher.unref?.();
123672
124029
  const url2 = `http://localhost:${handle.port}`;
123673
124030
  openDashboardInBrowser(url2, { isTTY: true });
123674
124031
  return url2;
@@ -123733,6 +124090,9 @@ async function runTaskCore(params, flavor) {
123733
124090
  }
123734
124091
  const memoryDocKey = defaultDocKey(normalizeMemoryManifest(entity?.stateDocs ?? null));
123735
124092
  const materialized = flavor.materialize(entity, randomUUID4());
124093
+ const docBoot = await materializeMemoryDocs(sdk, apiKey, entity, materialized.dir);
124094
+ for (const note of docBoot.notes) console.error(`(${entity.slug} memory docs: ${note})`);
124095
+ const runFiles = { ...materialized.files, ...docBoot.files };
123736
124096
  const est = estimateBlastRadius(entity);
123737
124097
  const shipBack = flavor.buildShipBack({ dir: materialized.dir, entity });
123738
124098
  const dashboardUrl = await startLiveDashboard(entity, materialized.dir);
@@ -123744,14 +124104,14 @@ async function runTaskCore(params, flavor) {
123744
124104
  dir: materialized.dir,
123745
124105
  ...dashboardUrl ? { dashboard: dashboardUrl } : {},
123746
124106
  blastRadius: est,
123747
- files: materialized.files,
124107
+ files: runFiles,
123748
124108
  ...materialized.skipped ? { skippedEmptyFiles: materialized.skipped } : {},
123749
124109
  // The run's durable memory (loop-memory-primitive). `dir` dies with the run, so
123750
124110
  // anything the NEXT run needs has to be written here instead. Named explicitly with
123751
124111
  // the slug because the run context carries no task identity — which is exactly why
123752
124112
  // the taskMemory* tools take an explicit `taskSlug`.
123753
124113
  memory: taskMemoryBlock(entity.slug, memoryDocKey),
123754
- directive: flavor.buildDirective({ dir: materialized.dir, slug: entity.slug, files: materialized.files, docKey: memoryDocKey }),
124114
+ directive: flavor.buildDirective({ dir: materialized.dir, slug: entity.slug, files: runFiles, docKey: memoryDocKey }),
123755
124115
  ...shipBack ? { shipBack } : {}
123756
124116
  }) }, { type: "text", text: dashboardLine }] };
123757
124117
  } catch (error) {
@@ -124281,7 +124641,7 @@ async function upsertTaskCore(params, flavor) {
124281
124641
  return { available: false, reason: err instanceof Error ? err.message : String(err) };
124282
124642
  }
124283
124643
  };
124284
- const upsert = async (entity, sourcePaths = {}) => {
124644
+ const upsert = async (entity, sourcePaths = {}, memoryDefaulted = false) => {
124285
124645
  if (!id && flavor.injectDashboardOnCreate) {
124286
124646
  try {
124287
124647
  injectDefaultDashboard(entity);
@@ -124293,6 +124653,7 @@ async function upsertTaskCore(params, flavor) {
124293
124653
  if (res.status === "ok") {
124294
124654
  void refreshDynamicPrompts();
124295
124655
  const mode2 = id ? "modified" : "created";
124656
+ const effectiveManifest = "stateDocs" in entity ? entity.stateDocs : !id ? null : previous && previous.available ? previous.content.stateDocs ?? null : void 0;
124296
124657
  const summary = buildLoopUpsertSummary({
124297
124658
  mode: mode2,
124298
124659
  sent: entity,
@@ -124303,7 +124664,26 @@ async function upsertTaskCore(params, flavor) {
124303
124664
  success: true,
124304
124665
  mode: mode2,
124305
124666
  [flavor.responseKey]: projectLoopIdentity(flavor.pick(res)),
124306
- receipt: { fields: summary.receipt },
124667
+ // WHERE IT LANDED, on the receipt itself. The 2026-08-31 incident put a
124668
+ // task into the wrong (admin) workspace and the success payload named
124669
+ // no workspace at all, so neither the agent nor the human reading the
124670
+ // transcript afterwards could tell. Nulls when `me` could not be read —
124671
+ // unknown, never a guess.
124672
+ receipt: {
124673
+ fields: summary.receipt,
124674
+ workspace: await resolveWorkspaceIdentity(),
124675
+ // WHERE THE TASK'S MEMORY LANDED, said in the user's vocabulary rather than left
124676
+ // implicit in the manifest bytes. `effectiveManifest` is what the task HAS after
124677
+ // this call: what this call sent, or — on a MODIFY that did not send one — the
124678
+ // stored manifest the diff already read. Never a fourth read.
124679
+ //
124680
+ // ⛔ THE FLOOR IS NAMED, NOT HIDDEN. When the default fired the line still reports
124681
+ // where the memory landed (`Memory: per member` — it really is per member), with a
124682
+ // suffix saying the author declared nothing. A receipt printing the bare line would
124683
+ // present the floor as an authored choice, which is the one thing this injection
124684
+ // must never be allowed to look like.
124685
+ ...effectiveManifest !== void 0 ? { memory: memorySummaryLine(effectiveManifest) + (memoryDefaulted ? MEMORY_DEFAULTED_SUFFIX : "") } : {}
124686
+ },
124307
124687
  summary: summary.text,
124308
124688
  summaryTruncated: summary.truncated,
124309
124689
  // ON CREATE ONLY. A task lands PRIVATE to the author, and the one step
@@ -124394,8 +124774,13 @@ async function upsertTaskCore(params, flavor) {
124394
124774
  }
124395
124775
  stateDocs = checked.manifest;
124396
124776
  }
124777
+ let memoryDefaulted = false;
124778
+ if (!id && declaresNoMemory(stateDocs)) {
124779
+ stateDocs = defaultMemoryManifest();
124780
+ memoryDefaulted = true;
124781
+ }
124397
124782
  const body = { ...resolvedMd };
124398
- if (stateDocsProvided) body.stateDocs = stateDocs;
124783
+ if (stateDocsProvided || memoryDefaulted) body.stateDocs = stateDocs;
124399
124784
  if (slug) body.slug = slug;
124400
124785
  if (descriptionShort) body.descriptionShort = descriptionShort;
124401
124786
  if (markdownBody) body.markdownBody = markdownBody;
@@ -124410,7 +124795,7 @@ async function upsertTaskCore(params, flavor) {
124410
124795
  sourcePaths[mdKey] = params[fileKey].trim();
124411
124796
  }
124412
124797
  }
124413
- return await upsert(body, sourcePaths);
124798
+ return await upsert(body, sourcePaths, memoryDefaulted);
124414
124799
  }
124415
124800
  const hasMessages = typeof params.messages === "string" && params.messages.trim();
124416
124801
  const hasBrief = typeof params.brief === "string" && params.brief.trim();
@@ -124435,7 +124820,7 @@ async function upsertTaskCore(params, flavor) {
124435
124820
  server.tool(
124436
124821
  {
124437
124822
  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.",
124823
+ 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
124824
  inputs: [
124440
124825
  { name: "slug", type: "string", required: false, description: "URL-safe unique slug for the task within the workspace. Required on CREATE." },
124441
124826
  { name: "descriptionShort", type: "string", required: false, description: "One-line description of what the task does. Required on CREATE." },
@@ -124451,7 +124836,7 @@ server.tool(
124451
124836
  { name: "dashboardHtmlFilePath", type: "string", required: false, description: "Path to a local file read verbatim as the dashboard HTML. Takes precedence over `dashboardHtml`." },
124452
124837
  { 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
124838
  { 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`.' },
124839
+ { 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
124840
  { name: "graphJson", type: "string", required: false, description: "JSON string of the canvas node graph (for re-edit). Defaults to {} on create." },
124456
124841
  { name: "draft", type: "boolean", required: false, description: "Whether the task is a draft (excluded from discovery). Defaults to true on create." },
124457
124842
  { 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." },
@@ -125469,9 +125854,7 @@ async function reconcileApprovalStateOnConnect() {
125469
125854
  }
125470
125855
  try {
125471
125856
  const wallets = await fetchVirtualWalletsFromBackend(currentCredentials.apiKey, true);
125472
- const approved = wallets.find(
125473
- (w) => w.address?.toLowerCase() === currentCredentials.eoaAddress?.toLowerCase() && w.status === "approved"
125474
- );
125857
+ const approved = selectNewestApprovedWallet(wallets, currentCredentials.eoaAddress);
125475
125858
  if (!approved) return;
125476
125859
  if (typeof approved.id === "number" && hasProcessedApprovalEventId(approved.id)) return;
125477
125860
  console.error(
@@ -125809,14 +126192,14 @@ function killPreviousServeInstances(deps) {
125809
126192
 
125810
126193
  // src/commands/autosync-skills.ts
125811
126194
  init_esm_shims();
125812
- import { existsSync as existsSync12 } from "fs";
126195
+ import { existsSync as existsSync14 } from "fs";
125813
126196
  import { homedir as homedir12 } from "os";
125814
126197
  import { join as join18 } from "path";
125815
126198
 
125816
126199
  // src/compounds/sync-skills.ts
125817
126200
  init_esm_shims();
125818
126201
  init_paths();
125819
- import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync12, rmSync, writeFileSync as writeFileSync10 } from "fs";
126202
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync13, rmSync, writeFileSync as writeFileSync10 } from "fs";
125820
126203
  import { join as join17 } from "path";
125821
126204
  var MANAGED_MARKER = "<!-- ametyst-managed: sync-skills -->";
125822
126205
  var GITIGNORE_HEADER = "# ametyst-managed: sync-skills \u2014 pointer skills generated for this account; never commit them.";
@@ -125861,7 +126244,7 @@ ${taskRunSection(slug, kind)}`;
125861
126244
  }
125862
126245
  function isManaged(file) {
125863
126246
  try {
125864
- return readFileSync12(file, "utf8").includes(MANAGED_MARKER);
126247
+ return readFileSync13(file, "utf8").includes(MANAGED_MARKER);
125865
126248
  } catch {
125866
126249
  return false;
125867
126250
  }
@@ -125875,8 +126258,8 @@ function buildGitignoreContent(managedSlugs) {
125875
126258
  function maintainGitignore(root2) {
125876
126259
  const file = join17(root2, ".gitignore");
125877
126260
  const managed = listManagedDirs(root2);
125878
- if (existsSync11(file)) {
125879
- const current = readFileSync12(file, "utf8");
126261
+ if (existsSync13(file)) {
126262
+ const current = readFileSync13(file, "utf8");
125880
126263
  const firstLine = current.split(/\r?\n/, 1)[0];
125881
126264
  if (firstLine !== GITIGNORE_HEADER) {
125882
126265
  console.error(
@@ -125939,7 +126322,7 @@ async function syncSkills(opts = {}) {
125939
126322
  const skipped = [];
125940
126323
  for (const [dir, item] of desired) {
125941
126324
  const file = join17(dir, "SKILL.md");
125942
- if (existsSync11(file) && !isManaged(file)) {
126325
+ if (existsSync13(file) && !isManaged(file)) {
125943
126326
  skipped.push(item.slug);
125944
126327
  console.warn(`\u26A0\uFE0F sync-skills: skipping "${item.slug}" \u2014 an unmanaged skill already exists at ${file}`);
125945
126328
  continue;
@@ -125967,7 +126350,7 @@ function isSkillsAutosyncEnabled(env = process.env) {
125967
126350
  return !(raw === "0" || raw === "false" || raw === "off" || raw === "no");
125968
126351
  }
125969
126352
  function detectPresentTargets(deps = {}) {
125970
- const exists = deps.existsSync ?? existsSync12;
126353
+ const exists = deps.existsSync ?? existsSync14;
125971
126354
  const cwd = deps.cwd ?? (() => process.cwd());
125972
126355
  const home = deps.homedir ?? homedir12;
125973
126356
  const targets = [];
@@ -126047,7 +126430,7 @@ async function serveCommand() {
126047
126430
  console.error(
126048
126431
  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
126432
  );
126050
- await startMCPServer(keystore, eoa, { ...config, apiKey }, versionNotice, {
126433
+ await startMCPServer(keystore, eoa, { ...config, apiKey, apiKeySource: resolved.source ?? void 0 }, versionNotice, {
126051
126434
  // Best-effort: materialize local `/`-command pointer skills for every published
126052
126435
  // compound + loop so they're available without a manual `compound sync-skills`.
126053
126436
  // Deferred from boot to the MCP initialize handshake so the sync is CLIENT-AWARE:
@@ -126986,7 +127369,7 @@ function recordFailedLaunch(args) {
126986
127369
  init_esm_shims();
126987
127370
  import { spawn as spawn2 } from "child_process";
126988
127371
  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";
127372
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "fs";
126990
127373
  import { dirname as dirname8, join as join23 } from "path";
126991
127374
 
126992
127375
  // src/loops/claude-binary.ts
@@ -127178,7 +127561,7 @@ async function runLoop(loopId, opts = {}) {
127178
127561
  `Loop ${loop2.slug}: ${est.steps} steps (${est.paidSteps} paid), est \u20AC${est.estCostEur ?? "?"} \u2014 ${capLabel}`
127179
127562
  );
127180
127563
  const statusPath = join23(dir, "STATUS.md");
127181
- const statusBefore = existsSync13(statusPath) ? readFileSync13(statusPath, "utf-8") : "";
127564
+ const statusBefore = existsSync15(statusPath) ? readFileSync14(statusPath, "utf-8") : "";
127182
127565
  writeFileSync12(
127183
127566
  statusPath,
127184
127567
  statusBefore.replace(
@@ -127238,7 +127621,7 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
127238
127621
  async function shipConstraints() {
127239
127622
  try {
127240
127623
  const constraintsPath = join23(dir, "CONSTRAINTS.md");
127241
- const materializedConstraints = existsSync13(constraintsPath) ? readFileSync13(constraintsPath, "utf-8") : void 0;
127624
+ const materializedConstraints = existsSync15(constraintsPath) ? readFileSync14(constraintsPath, "utf-8") : void 0;
127242
127625
  if (materializedConstraints === void 0) return;
127243
127626
  const boot = loop2.constraintsMd ?? "";
127244
127627
  for (let attempt = 1; attempt <= 2; attempt++) {
@@ -127349,7 +127732,7 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
127349
127732
  startedAtEpochMs
127350
127733
  });
127351
127734
  }
127352
- const statusAfter = existsSync13(statusPath) ? readFileSync13(statusPath, "utf-8") : "";
127735
+ const statusAfter = existsSync15(statusPath) ? readFileSync14(statusPath, "utf-8") : "";
127353
127736
  const clean2 = exitCode === 0 && /(vision\s*done|queue\s*drained|status:\s*done)/i.test(statusAfter) && !/(brake|crash|crashed|errored)/i.test(statusAfter);
127354
127737
  await shipBackConstraints();
127355
127738
  process.off("SIGINT", onSignal);
@@ -127387,7 +127770,7 @@ async function showLoop(loopId) {
127387
127770
  // src/loops/schedule.ts
127388
127771
  init_esm_shims();
127389
127772
  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";
127773
+ 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
127774
  import { execFileSync as execFileSync4 } from "child_process";
127392
127775
  import { join as join24, dirname as dirname9 } from "path";
127393
127776
  import { homedir as homedir14 } from "os";
@@ -127467,7 +127850,7 @@ function isLoaded(lbl) {
127467
127850
  }
127468
127851
  function readInteractiveDefaultModel(home) {
127469
127852
  try {
127470
- const raw = readFileSync14(join24(home, ".claude", "settings.json"), "utf-8");
127853
+ const raw = readFileSync15(join24(home, ".claude", "settings.json"), "utf-8");
127471
127854
  const model = JSON.parse(String(raw)).model;
127472
127855
  return typeof model === "string" && model.trim() ? model.trim() : void 0;
127473
127856
  } catch {
@@ -127695,7 +128078,7 @@ function list(kind, opts = {}) {
127695
128078
  const prefix = kind.labelPrefix;
127696
128079
  if (platform === "darwin") {
127697
128080
  const dir = join24(home, "Library", "LaunchAgents");
127698
- if (!existsSync14(dir)) return [];
128081
+ if (!existsSync16(dir)) return [];
127699
128082
  return readdirSync5(dir).filter((f) => f.startsWith(prefix) && f.endsWith(".plist")).map((f) => f.slice(prefix.length, -".plist".length));
127700
128083
  }
127701
128084
  return readCrontab().split("\n").filter((l) => l.includes(`# ${prefix}`)).map((l) => l.trimEnd().slice(l.trimEnd().lastIndexOf(prefix) + prefix.length));
@@ -127757,12 +128140,12 @@ function listEntries(kind, opts = {}) {
127757
128140
  const prefix = kind.labelPrefix;
127758
128141
  if (platform === "darwin") {
127759
128142
  const dir = join24(home, "Library", "LaunchAgents");
127760
- if (!existsSync14(dir)) return [];
128143
+ if (!existsSync16(dir)) return [];
127761
128144
  return readdirSync5(dir).filter((f) => f.startsWith(prefix) && f.endsWith(".plist")).map((f) => {
127762
128145
  const slug = f.slice(prefix.length, -".plist".length);
127763
128146
  let envKeys = [];
127764
128147
  try {
127765
- envKeys = plistEnvKeys(String(readFileSync14(join24(dir, f), "utf-8")));
128148
+ envKeys = plistEnvKeys(String(readFileSync15(join24(dir, f), "utf-8")));
127766
128149
  } catch {
127767
128150
  envKeys = [];
127768
128151
  }