@ametyst/cli 0.3.8 → 0.3.11

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 +1203 -1247
  2. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -99974,7 +99974,7 @@ var require_XMLHttpRequest = __commonJS({
99974
99974
  init_esm_shims();
99975
99975
  var fs = __require("fs");
99976
99976
  var Url = __require("url");
99977
- var spawn4 = __require("child_process").spawn;
99977
+ var spawn3 = __require("child_process").spawn;
99978
99978
  module.exports = XMLHttpRequest3;
99979
99979
  XMLHttpRequest3.XMLHttpRequest = XMLHttpRequest3;
99980
99980
  function XMLHttpRequest3(opts) {
@@ -100270,7 +100270,7 @@ var require_XMLHttpRequest = __commonJS({
100270
100270
  var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
100271
100271
  fs.writeFileSync(syncFile, "", "utf8");
100272
100272
  var execString = "var http = require('http'), https = require('https'), fs = require('fs');var doRequest = http" + (ssl ? "s" : "") + ".request;var options = " + JSON.stringify(options) + ";var responseText = '';var responseData = Buffer.alloc(0);var req = doRequest(options, function(response) {response.on('data', function(chunk) { var data = Buffer.from(chunk); responseText += data.toString('utf8'); responseData = Buffer.concat([responseData, data]);});response.on('end', function() {fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText, data: responseData.toString('base64')}}), 'utf8');fs.unlinkSync('" + syncFile + "');});response.on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});}).on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});" + (data ? "req.write('" + JSON.stringify(data).slice(1, -1).replace(/'/g, "\\'") + "');" : "") + "req.end();";
100273
- var syncProc = spawn4(process.argv[0], ["-e", execString]);
100273
+ var syncProc = spawn3(process.argv[0], ["-e", execString]);
100274
100274
  var statusText;
100275
100275
  while (fs.existsSync(syncFile)) {
100276
100276
  }
@@ -106361,7 +106361,7 @@ var init_dist = __esm({
106361
106361
  // src/config/paths.ts
106362
106362
  import { accessSync, constants, existsSync, mkdirSync } from "fs";
106363
106363
  import { homedir } from "os";
106364
- import { join, parse as parse3, resolve } from "path";
106364
+ import { basename, join, parse as parse3, resolve } from "path";
106365
106365
  function ensureDirectories() {
106366
106366
  if (!existsSync(AMETYST_DIR)) mkdirSync(AMETYST_DIR, { mode: 448 });
106367
106367
  if (!existsSync(WALLETS_DIR)) mkdirSync(WALLETS_DIR, { mode: 448 });
@@ -106407,26 +106407,34 @@ function resolveRunRoot(explicitDir, deps = {}) {
106407
106407
  (deps.mkdirSync ?? mkdirSync)(fallback2, { recursive: true, mode: 448 });
106408
106408
  return { root: fallback2, reason: "fallback", rejected };
106409
106409
  }
106410
- function loopsRoot(runRoot) {
106411
- return join(runRoot ?? resolveRunRoot().root, `.ametyst${ENV_SUFFIX}`, "loops");
106410
+ function tasksRoot(runRoot) {
106411
+ return join(runRoot ?? resolveRunRoot().root, `.ametyst${ENV_SUFFIX}`, RUNS_SEGMENT);
106412
106412
  }
106413
- function loopDir(slug, runRoot) {
106413
+ function runDir(slug, runRoot) {
106414
106414
  const safe = String(slug).toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
106415
- if (!safe) throw new Error(`invalid loop slug: ${JSON.stringify(slug)}`);
106416
- return join(loopsRoot(runRoot), safe);
106415
+ if (!safe) throw new Error(`invalid task slug: ${JSON.stringify(slug)}`);
106416
+ return join(tasksRoot(runRoot), safe);
106417
106417
  }
106418
- function loopFiresRoot(slug, runRoot) {
106419
- return join(loopDir(slug, runRoot), "fires");
106418
+ function legacyRunFolderHint(slug, runRoot, deps = {}) {
106419
+ const exists = deps.existsSync ?? existsSync;
106420
+ const next = runDir(slug, runRoot);
106421
+ const ametystDir = join(runRoot, `.ametyst${ENV_SUFFIX}`);
106422
+ const legacy = join(ametystDir, LEGACY_RUNS_SEGMENT, basename(next));
106423
+ if (exists(next) || !exists(legacy)) return void 0;
106424
+ return `Task ${slug}: found a run folder from an older cli at ${legacy} and nothing yet at ${next} \u2014 move it (\`mv ${join(ametystDir, LEGACY_RUNS_SEGMENT)} ${join(ametystDir, RUNS_SEGMENT)}\`, or \`mv ${legacy} ${next}\` when ${join(ametystDir, RUNS_SEGMENT)} already exists) to keep its .state/ ledger and logs; continuing with ${next}.`;
106420
106425
  }
106421
- function loopFireDir(slug, fireId, runRoot) {
106426
+ function runFiresRoot(slug, runRoot) {
106427
+ return join(runDir(slug, runRoot), "fires");
106428
+ }
106429
+ function runFireDir(slug, fireId, runRoot) {
106422
106430
  const safe = String(fireId).toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
106423
106431
  if (!safe) throw new Error(`invalid fire id: ${JSON.stringify(fireId)}`);
106424
- return join(loopFiresRoot(slug, runRoot), safe);
106432
+ return join(runFiresRoot(slug, runRoot), safe);
106425
106433
  }
106426
106434
  function refusedConstraintsPath(slug, fireId, runRoot) {
106427
106435
  const safe = String(fireId).toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
106428
106436
  if (!safe) throw new Error(`invalid fire id: ${JSON.stringify(fireId)}`);
106429
- return join(loopDir(slug, runRoot), ".state", `constraints-refused-${safe}.md`);
106437
+ return join(runDir(slug, runRoot), ".state", `constraints-refused-${safe}.md`);
106430
106438
  }
106431
106439
  function skillsRoot(target = "claude", global2 = false) {
106432
106440
  return join(global2 ? homedir() : process.cwd(), SKILLS_DIR_BY_TARGET[target], "skills");
@@ -106436,7 +106444,7 @@ function skillDir(slug, target = "claude", global2 = false) {
106436
106444
  if (!safe) throw new Error(`invalid skill slug: ${JSON.stringify(slug)}`);
106437
106445
  return join(skillsRoot(target, global2), safe);
106438
106446
  }
106439
- var ENV_SUFFIX, AMETYST_DIR, CONFIG_PATH, CREDENTIALS_ENC_PATH, WALLETS_DIR, VAULT_PATH, SKILLS_DIR_BY_TARGET;
106447
+ var ENV_SUFFIX, AMETYST_DIR, CONFIG_PATH, CREDENTIALS_ENC_PATH, WALLETS_DIR, VAULT_PATH, RUNS_SEGMENT, LEGACY_RUNS_SEGMENT, SKILLS_DIR_BY_TARGET;
106440
106448
  var init_paths = __esm({
106441
106449
  "src/config/paths.ts"() {
106442
106450
  "use strict";
@@ -106447,6 +106455,8 @@ var init_paths = __esm({
106447
106455
  CREDENTIALS_ENC_PATH = join(AMETYST_DIR, "credentials.enc");
106448
106456
  WALLETS_DIR = join(AMETYST_DIR, "wallets");
106449
106457
  VAULT_PATH = join(AMETYST_DIR, "wallet.json");
106458
+ RUNS_SEGMENT = "tasks";
106459
+ LEGACY_RUNS_SEGMENT = "loops";
106450
106460
  SKILLS_DIR_BY_TARGET = {
106451
106461
  claude: ".claude",
106452
106462
  codex: ".codex"
@@ -111845,7 +111855,7 @@ import {
111845
111855
  writeSync
111846
111856
  } from "fs";
111847
111857
  import { randomBytes as randomBytes3 } from "crypto";
111848
- import { basename as basename3, dirname as dirname5, join as join5 } from "path";
111858
+ import { basename as basename4, dirname as dirname5, join as join5 } from "path";
111849
111859
  function createWallet(passphrase) {
111850
111860
  return native.createWallet(passphrase);
111851
111861
  }
@@ -111887,7 +111897,7 @@ function listWallets() {
111887
111897
  return readdirSync3(WALLETS_DIR).filter((file) => file.endsWith(".json")).map((file) => join5(WALLETS_DIR, file));
111888
111898
  }
111889
111899
  function deleteWallet(walletPath) {
111890
- const name = basename3(walletPath);
111900
+ const name = basename4(walletPath);
111891
111901
  if (!name.endsWith(".json")) {
111892
111902
  throw new Error("Refusing to delete non-wallet file");
111893
111903
  }
@@ -111924,7 +111934,7 @@ function writeVaultFileAtomic(contents) {
111924
111934
  }
111925
111935
  function writeSecretFileAtomic(path2, contents) {
111926
111936
  const dir = dirname5(path2);
111927
- const tmp = join5(dir, `.${basename3(path2)}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`);
111937
+ const tmp = join5(dir, `.${basename4(path2)}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`);
111928
111938
  let fd;
111929
111939
  try {
111930
111940
  fd = openSync(tmp, "wx", 384);
@@ -112477,7 +112487,7 @@ var init_version5 = __esm({
112477
112487
  "src/version.ts"() {
112478
112488
  "use strict";
112479
112489
  init_esm_shims();
112480
- CLI_VERSION = true ? "0.3.8" : "0.0.0-dev";
112490
+ CLI_VERSION = true ? "0.3.11" : "0.0.0-dev";
112481
112491
  }
112482
112492
  });
112483
112493
 
@@ -113961,7 +113971,7 @@ var init_engine_store = __esm({
113961
113971
  BUSY_PRIMARY_ERRNO = /* @__PURE__ */ new Set([5, 6]);
113962
113972
  isBusyErrno = (errno) => Number.isInteger(errno) && BUSY_PRIMARY_ERRNO.has(errno & 255);
113963
113973
  CORRUPT_STORE_ADVICE = `It holds no credentials \u2014 every secret is in your OS keychain \u2014 so deleting that file loses nothing but the engine's own bookkeeping, and it is rebuilt on the next 'ametyst connections' command.`;
113964
- lockedStoreAdvice = (budgetMs) => `Another process is holding it \u2014 a second 'ametyst' command, or a loop running alongside this one. Nothing is damaged and nothing was lost: SQLite is doing its job and serializing the two. This was already retried for up to ${budgetMs}ms and the lock did not clear, so the other process is still working; run the command again once it finishes. Do NOT delete the database to clear this \u2014 that would throw away working state to fix a lock that goes away by itself.`;
113974
+ lockedStoreAdvice = (budgetMs) => `Another process is holding it \u2014 a second 'ametyst' command, or a scheduled task firing alongside this one. Nothing is damaged and nothing was lost: SQLite is doing its job and serializing the two. This was already retried for up to ${budgetMs}ms and the lock did not clear, so the other process is still working; run the command again once it finishes. Do NOT delete the database to clear this \u2014 that would throw away working state to fix a lock that goes away by itself.`;
113965
113975
  ConnectionsStoreUnavailableError = class extends Error {
113966
113976
  constructor(path2, cause, advice = CORRUPT_STORE_ADVICE) {
113967
113977
  super(
@@ -114374,7 +114384,7 @@ import {
114374
114384
  writeFileSync as writeFileSync3
114375
114385
  } from "fs";
114376
114386
  import { homedir as homedir2 } from "os";
114377
- import { basename, dirname as dirname2, join as join2 } from "path";
114387
+ import { basename as basename2, dirname as dirname2, join as join2 } from "path";
114378
114388
  var CLAUDE_CODE_CONFIG_PATH = join2(homedir2(), ".claude.json");
114379
114389
  var AMETYST_MCP_COMMAND = process.argv[1] || "ametyst";
114380
114390
  var AMETYST_MCP_NAME = false ? "ametyst-staging" : "ametyst";
@@ -114397,7 +114407,7 @@ function backupIfExists() {
114397
114407
  const backupPath = `${CLAUDE_CODE_CONFIG_PATH}.backup-${ts}`;
114398
114408
  copyFileSync(CLAUDE_CODE_CONFIG_PATH, backupPath);
114399
114409
  const dir = dirname2(CLAUDE_CODE_CONFIG_PATH);
114400
- const base2 = basename(CLAUDE_CODE_CONFIG_PATH);
114410
+ const base2 = basename2(CLAUDE_CODE_CONFIG_PATH);
114401
114411
  const backups = readdirSync(dir).filter((f) => f.startsWith(`${base2}.backup-`)).sort();
114402
114412
  while (backups.length > MAX_BACKUPS) {
114403
114413
  const oldest = backups.shift();
@@ -114492,7 +114502,7 @@ import {
114492
114502
  writeFileSync as writeFileSync4
114493
114503
  } from "fs";
114494
114504
  import { homedir as homedir3 } from "os";
114495
- import { basename as basename2, dirname as dirname3, join as join3 } from "path";
114505
+ import { basename as basename3, dirname as dirname3, join as join3 } from "path";
114496
114506
  var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
114497
114507
  var MAX_BACKUPS2 = 3;
114498
114508
  var TRAILING = "\\s*(#.*)?$";
@@ -114510,7 +114520,7 @@ function backupIfExists2() {
114510
114520
  } catch {
114511
114521
  }
114512
114522
  const dir = dirname3(CODEX_CONFIG_PATH);
114513
- const base2 = basename2(CODEX_CONFIG_PATH);
114523
+ const base2 = basename3(CODEX_CONFIG_PATH);
114514
114524
  const backups = readdirSync2(dir).filter((f) => f.startsWith(`${base2}.backup-`)).sort();
114515
114525
  while (backups.length > MAX_BACKUPS2) {
114516
114526
  const oldest = backups.shift();
@@ -114617,7 +114627,7 @@ function removeAmetystCodexEntry() {
114617
114627
  }
114618
114628
  function purgeKeyBearingBackups() {
114619
114629
  const dir = dirname3(CODEX_CONFIG_PATH);
114620
- const base2 = basename2(CODEX_CONFIG_PATH);
114630
+ const base2 = basename3(CODEX_CONFIG_PATH);
114621
114631
  let names;
114622
114632
  try {
114623
114633
  names = readdirSync2(dir).filter((f) => f.startsWith(`${base2}.backup-`));
@@ -116610,7 +116620,7 @@ async function startUnlockListener(deps) {
116610
116620
  // src/mcp-server/system-prompt.ts
116611
116621
  init_esm_shims();
116612
116622
 
116613
- // src/loops/memory-model.ts
116623
+ // src/tasks/memory-model.ts
116614
116624
  init_esm_shims();
116615
116625
  init_dist();
116616
116626
  var TASK_MEMORY_MODEL_SECTION = `TASK MEMORY MODEL (the one description \u2014 every taskMemory* tool points here):
@@ -116873,12 +116883,12 @@ your wallet first** \u2014 discover it, don't dead-end on "I can't access that".
116873
116883
  ## Tasks are payable too
116874
116884
 
116875
116885
  Beyond raw merchants, your workspace has reusable **tasks** \u2014 multi-step
116876
- procedures that may spend on merchants as they run. A task is the unified
116877
- card: what used to be published as either a "compound skill" or a "loop" is
116878
- one row now, and one set of tools covers both.
116886
+ procedures that may spend on merchants as they run. A task is one card
116887
+ whether it is a one-shot procedure or a scheduled job with its own memory,
116888
+ and one set of tools covers all of them.
116879
116889
 
116880
116890
  - **\`getTask({ intent })\`** then **\`runTask\`** \u2014 find and run one, whether
116881
- it is a one-shot procedure or an autonomous loop with its own memory.
116891
+ it is a one-shot procedure or a scheduled job with its own memory.
116882
116892
  - **\`createTask\`** \u2014 author or modify one.
116883
116893
 
116884
116894
  These are *your* capabilities the same way the merchants are: available through
@@ -117125,15 +117135,13 @@ function shouldAutoShare(toolCallCount2, isDesignPartner) {
117125
117135
  init_esm_shims();
117126
117136
  function nudgeFlagForCounts(counts) {
117127
117137
  if (!counts) return null;
117128
- if (!Number.isFinite(counts.compounds) || !Number.isFinite(counts.loops)) return null;
117129
- if (counts.compounds < 0 || counts.loops < 0) return null;
117130
- if (counts.compounds === 0) return "skill";
117131
- if (counts.loops === 0) return "loop";
117138
+ if (!Number.isFinite(counts.tasks)) return null;
117139
+ if (counts.tasks < 0) return null;
117140
+ if (counts.tasks === 0) return "task";
117132
117141
  return null;
117133
117142
  }
117134
117143
  var NUDGE_TEXT = {
117135
- skill: "\u2139\uFE0F Ametyst \u2014 this workspace has not created any skill yet. Ask the user: would they like to create their first skill now? A skill (compound) packages a workflow they repeat into one reusable command, instead of re-explaining it every session. If they say yes, walk them through `createTask`. If they say no, drop it and carry on \u2014 do not ask again in this turn. This note is advisory: finish the tool result above first, it is unrelated.",
117136
- loop: "\u2139\uFE0F Ametyst \u2014 this workspace has skills but no loop yet. Ask the user: would they like to turn one of their skills into a loop? A loop runs on a schedule and keeps its own memory between runs, so a skill they fire by hand every day runs itself instead. If they say yes, walk them through `createTask`. If they say no, drop it and carry on \u2014 do not ask again in this turn. This note is advisory: finish the tool result above first, it is unrelated."
117144
+ task: "\u2139\uFE0F Ametyst \u2014 this workspace has not created any task yet. Ask the user: would they like to create their first task now? A task packages a workflow they repeat into one reusable command \u2014 run by hand or on a schedule, with its own memory between runs \u2014 instead of re-explaining it every session. If they say yes, walk them through `createTask`. If they say no, drop it and carry on \u2014 do not ask again in this turn. This note is advisory: finish the tool result above first, it is unrelated."
117137
117145
  };
117138
117146
  function nudgeTextFor(flag) {
117139
117147
  return Object.prototype.hasOwnProperty.call(NUDGE_TEXT, flag) ? NUDGE_TEXT[flag] : null;
@@ -117352,12 +117360,12 @@ init_paths();
117352
117360
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
117353
117361
  import { join as join9 } from "path";
117354
117362
 
117355
- // src/loops/state-docs.ts
117363
+ // src/tasks/state-docs.ts
117356
117364
  init_esm_shims();
117357
117365
  import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
117358
117366
  import { join as join8 } from "path";
117359
117367
 
117360
- // src/loops/shipback.ts
117368
+ // src/tasks/shipback.ts
117361
117369
  init_esm_shims();
117362
117370
  function scanHeadings(text) {
117363
117371
  return scanDoc(text).items;
@@ -117480,7 +117488,7 @@ function verifyLanded(afterWrite, expectedBlocks) {
117480
117488
  return expectedBlocks.filter((t) => !have.has(t));
117481
117489
  }
117482
117490
 
117483
- // src/loops/memory-manifest.ts
117491
+ // src/tasks/memory-manifest.ts
117484
117492
  init_esm_shims();
117485
117493
  var MEMORY_MANIFEST_MAX_DOCS = 64;
117486
117494
  var MEMORY_MANIFEST_MAX_RECORDS = 16;
@@ -117636,7 +117644,7 @@ function formatMemoryManifest(manifest) {
117636
117644
  return lines.length ? lines.join("\n") : "(declared empty: no docs, no records)";
117637
117645
  }
117638
117646
 
117639
- // src/loops/state-docs.ts
117647
+ // src/tasks/state-docs.ts
117640
117648
  var RESERVED_FIRE_FILENAMES = [
117641
117649
  "SKILL.md",
117642
117650
  "VISION.md",
@@ -117862,7 +117870,7 @@ var TASK_DEFINITION_FILES = [
117862
117870
  ["dashboardManifest", "dashboard.manifest.json", "dashboardManifest"]
117863
117871
  ];
117864
117872
  function materializeTask(task, runId, runRoot) {
117865
- const dir = loopFireDir(task.slug, runId, runRoot);
117873
+ const dir = runFireDir(task.slug, runId, runRoot);
117866
117874
  mkdirSync7(dir, { recursive: true, mode: 448 });
117867
117875
  const files = {};
117868
117876
  const skipped = [];
@@ -117923,17 +117931,55 @@ async function materializeMemoryDocs(sdk, apiKey, task, dir) {
117923
117931
  // src/mcp-server/index.ts
117924
117932
  import { existsSync as existsSync13 } from "fs";
117925
117933
 
117926
- // src/loops/dashboard.ts
117934
+ // src/tasks/dashboard.ts
117927
117935
  init_esm_shims();
117928
117936
  import { createServer as createServer2 } from "http";
117929
117937
  import * as realFs from "fs";
117930
117938
  import { spawn as realSpawn } from "child_process";
117931
117939
  import { join as join10 } from "path";
117940
+
117941
+ // src/tasks/env.ts
117942
+ init_esm_shims();
117943
+ var TASK_ENV = Object.freeze({
117944
+ DASHBOARD_PORT: "AMETYST_TASK_DASHBOARD_PORT",
117945
+ MAX_BUDGET_USD: "AMETYST_TASK_MAX_BUDGET_USD",
117946
+ MAX_CONCURRENT_FIRES: "AMETYST_TASK_MAX_CONCURRENT_FIRES",
117947
+ GIT_AUTHOR_NAME: "AMETYST_TASK_GIT_AUTHOR_NAME",
117948
+ GIT_AUTHOR_EMAIL: "AMETYST_TASK_GIT_AUTHOR_EMAIL",
117949
+ SLUG: "AMETYST_TASK_SLUG"
117950
+ });
117951
+ var LEGACY_TASK_ENV = Object.freeze({
117952
+ DASHBOARD_PORT: "AMETYST_LOOP_DASHBOARD_PORT",
117953
+ MAX_BUDGET_USD: "AMETYST_LOOP_MAX_BUDGET_USD",
117954
+ MAX_CONCURRENT_FIRES: "AMETYST_LOOP_MAX_CONCURRENT_FIRES",
117955
+ GIT_AUTHOR_NAME: "AMETYST_LOOP_GIT_AUTHOR_NAME",
117956
+ GIT_AUTHOR_EMAIL: "AMETYST_LOOP_GIT_AUTHOR_EMAIL",
117957
+ SLUG: "AMETYST_LOOP_SLUG"
117958
+ });
117959
+ var warnedLegacyNames = /* @__PURE__ */ new Set();
117960
+ function legacyTaskEnvWarning(key) {
117961
+ return `${LEGACY_TASK_ENV[key]} is deprecated \u2014 rename it to ${TASK_ENV[key]}. Read as ${TASK_ENV[key]} for now; the old name stops being honoured in a later release.`;
117962
+ }
117963
+ function readTaskEnv(key, env = process.env, deps = {}) {
117964
+ const fresh = env[TASK_ENV[key]];
117965
+ if (fresh !== void 0) return fresh;
117966
+ const legacy = env[LEGACY_TASK_ENV[key]];
117967
+ if (legacy === void 0) return void 0;
117968
+ const legacyName = LEGACY_TASK_ENV[key];
117969
+ if (!warnedLegacyNames.has(legacyName)) {
117970
+ warnedLegacyNames.add(legacyName);
117971
+ (deps.warn ?? ((line) => console.warn(line)))(legacyTaskEnvWarning(key));
117972
+ }
117973
+ return legacy;
117974
+ }
117975
+
117976
+ // src/tasks/dashboard.ts
117932
117977
  var DEFAULT_PORT = 4477;
117933
- var DASHBOARD_PORT_ENV = "AMETYST_LOOP_DASHBOARD_PORT";
117978
+ var DASHBOARD_PORT_ENV = TASK_ENV.DASHBOARD_PORT;
117934
117979
  var DASHBOARD_NO_OPEN_ENV = "AMETYST_DASHBOARD_NO_OPEN";
117935
117980
  var NO_DASHBOARD_MESSAGE = "no dashboard on this task \u2014 ask your agent to create one";
117936
117981
  var MAX_PORT_RETRIES = 20;
117982
+ var DASHBOARD_DOC_CACHE_MS = 5e3;
117937
117983
  var SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
117938
117984
  function parseManifestFiles(manifest) {
117939
117985
  if (typeof manifest !== "string" || !manifest.trim()) return [];
@@ -117947,7 +117993,7 @@ function parseManifestFiles(manifest) {
117947
117993
  return list2.filter((f) => typeof f === "string" && SAFE_NAME.test(f));
117948
117994
  }
117949
117995
  function dashboardPort(env = process.env) {
117950
- const raw = Number(env[DASHBOARD_PORT_ENV]);
117996
+ const raw = Number(readTaskEnv("DASHBOARD_PORT", env));
117951
117997
  return Number.isInteger(raw) && raw > 0 && raw < 65536 ? raw : DEFAULT_PORT;
117952
117998
  }
117953
117999
  function openDashboardInBrowser(url2, deps = {}) {
@@ -117958,9 +118004,9 @@ function openDashboardInBrowser(url2, deps = {}) {
117958
118004
  const platform = deps.platform ?? process.platform;
117959
118005
  const cmd = platform === "darwin" ? "open" : platform === "linux" ? "xdg-open" : null;
117960
118006
  if (!cmd) return false;
117961
- const spawn4 = deps.spawn ?? realSpawn;
118007
+ const spawn3 = deps.spawn ?? realSpawn;
117962
118008
  try {
117963
- const child = spawn4(cmd, [url2], { stdio: "ignore", detached: true });
118009
+ const child = spawn3(cmd, [url2], { stdio: "ignore", detached: true });
117964
118010
  child.once?.("error", () => {
117965
118011
  });
117966
118012
  child.unref?.();
@@ -117970,14 +118016,32 @@ function openDashboardInBrowser(url2, deps = {}) {
117970
118016
  }
117971
118017
  }
117972
118018
  function startDashboardServer(args) {
117973
- const html = args.loop.dashboardHtml;
118019
+ const html = args.task.dashboardHtml;
117974
118020
  if (typeof html !== "string" || !html) return Promise.resolve(null);
117975
118021
  const fs = args.deps?.fs ?? realFs;
117976
118022
  const log = args.deps?.log ?? ((line) => console.error(line));
117977
118023
  const make = args.deps?.createServer ?? createServer2;
118024
+ const clock = args.deps?.now ?? Date.now;
117978
118025
  const basePort = args.port ?? dashboardPort();
117979
- const files = parseManifestFiles(args.loop.dashboardManifest);
117980
- const handler = (req, res) => {
118026
+ const files = parseManifestFiles(args.task.dashboardManifest);
118027
+ const readDoc = args.readDoc;
118028
+ const docCache = /* @__PURE__ */ new Map();
118029
+ const resolveDoc = async (name) => {
118030
+ if (!readDoc) return null;
118031
+ const now = clock();
118032
+ const hit = docCache.get(name);
118033
+ if (hit && now - hit.at < DASHBOARD_DOC_CACHE_MS) return hit.value;
118034
+ let value2 = null;
118035
+ try {
118036
+ const got = await readDoc(name);
118037
+ value2 = got && typeof got.content === "string" ? got : null;
118038
+ } catch {
118039
+ value2 = null;
118040
+ }
118041
+ docCache.set(name, { at: now, value: value2 });
118042
+ return value2;
118043
+ };
118044
+ const serve = async (req, res) => {
117981
118045
  try {
117982
118046
  const url2 = (req.url ?? "/").split("?")[0];
117983
118047
  if (req.method === "GET" && url2 === "/") {
@@ -117987,27 +118051,36 @@ function startDashboardServer(args) {
117987
118051
  }
117988
118052
  if (req.method === "GET" && url2 === "/data") {
117989
118053
  const data = {};
118054
+ let newestUpdatedAt;
117990
118055
  for (const name of files) {
118056
+ const doc = await resolveDoc(name);
118057
+ if (doc) {
118058
+ data[name] = doc.content;
118059
+ if (doc.updatedAt && (!newestUpdatedAt || Date.parse(doc.updatedAt) > Date.parse(newestUpdatedAt))) {
118060
+ newestUpdatedAt = doc.updatedAt;
118061
+ }
118062
+ continue;
118063
+ }
117991
118064
  try {
117992
- const p = join10(args.loopDir, name);
118065
+ const p = join10(args.runDir, name);
117993
118066
  if (fs.existsSync(p)) data[name] = fs.readFileSync(p, "utf-8");
117994
118067
  } catch {
117995
118068
  }
117996
118069
  }
117997
118070
  const state = {};
117998
118071
  try {
117999
- const p = join10(args.loopDir, ".state", "fires.jsonl");
118072
+ const p = join10(args.runDir, ".state", "fires.jsonl");
118000
118073
  if (fs.existsSync(p)) state["fires.jsonl"] = fs.readFileSync(p, "utf-8");
118001
118074
  } catch {
118002
118075
  }
118003
118076
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
118004
118077
  res.end(
118005
118078
  JSON.stringify({
118006
- loop: args.loop.slug,
118079
+ loop: args.task.slug,
118007
118080
  files: data,
118008
118081
  state,
118009
118082
  mode: "live",
118010
- asOf: (/* @__PURE__ */ new Date()).toISOString()
118083
+ asOf: newestUpdatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
118011
118084
  })
118012
118085
  );
118013
118086
  return;
@@ -118022,6 +118095,15 @@ function startDashboardServer(args) {
118022
118095
  }
118023
118096
  }
118024
118097
  };
118098
+ const handler = (req, res) => {
118099
+ void serve(req, res).catch(() => {
118100
+ try {
118101
+ res.writeHead(500);
118102
+ res.end();
118103
+ } catch {
118104
+ }
118105
+ });
118106
+ };
118025
118107
  const bind = (port) => new Promise((resolve3) => {
118026
118108
  const server2 = make(handler);
118027
118109
  server2.once("error", (err) => {
@@ -118054,665 +118136,827 @@ function startDashboardServer(args) {
118054
118136
  for (let port = basePort; port <= lastPort; port++) {
118055
118137
  const attempt = await bind(port);
118056
118138
  if (attempt.ok) {
118057
- log(` loop dashboard: http://localhost:${attempt.handle.port}`);
118139
+ log(` task dashboard: http://localhost:${attempt.handle.port}`);
118058
118140
  return attempt.handle;
118059
118141
  }
118060
118142
  if (attempt.err.code !== "EADDRINUSE") {
118061
118143
  log(
118062
- ` (loop dashboard not started on :${port} \u2014 ${attempt.err.code ?? attempt.err.message}; run continues)`
118144
+ ` (task dashboard not started on :${port} \u2014 ${attempt.err.code ?? attempt.err.message}; run continues)`
118063
118145
  );
118064
118146
  return null;
118065
118147
  }
118066
118148
  }
118067
- log(` (loop dashboard not started \u2014 :${basePort}-${lastPort} all in use; run continues)`);
118149
+ log(` (task dashboard not started \u2014 :${basePort}-${lastPort} all in use; run continues)`);
118068
118150
  return null;
118069
118151
  })();
118070
118152
  }
118071
118153
 
118072
- // src/mcp-server/task-run-mode.ts
118154
+ // src/tasks/dashboard-docs.ts
118073
118155
  init_esm_shims();
118074
- var ACCEPTED_RUN_MODES = ["in-chat", "headless"];
118075
- var FRONTMATTER_SCAN_LIMIT = 8192;
118076
- function normalizeRunMode(raw) {
118077
- if (typeof raw !== "string") return void 0;
118078
- const v = raw.trim().toLowerCase();
118079
- if (v === "headless") return "headless";
118080
- if (v === "in-chat" || v === "inchat" || v === "in_chat") return "in-chat";
118081
- return void 0;
118156
+ function memoryDocResolver(sdk, apiKey, slug, manifest) {
118157
+ return async (name) => {
118158
+ const key = docKeyForFilename(name);
118159
+ if (!key) return null;
118160
+ const declared = declaredDocScope(manifest, key);
118161
+ const scopes = declared ? [declared] : ["member", "shared"];
118162
+ for (const scope of scopes) {
118163
+ try {
118164
+ const res = await sdk.loops.memory.getDoc(apiKey, slug, key, { scope });
118165
+ if (res.status === "ok") {
118166
+ return { content: res.doc.content ?? "", ...res.doc.updatedAt ? { updatedAt: res.doc.updatedAt } : {} };
118167
+ }
118168
+ } catch {
118169
+ }
118170
+ }
118171
+ return null;
118172
+ };
118082
118173
  }
118083
- function describeProvided(raw) {
118084
- if (typeof raw === "string") return raw.trim();
118085
- try {
118086
- return JSON.stringify(raw) ?? String(raw);
118087
- } catch {
118088
- return String(raw);
118089
- }
118174
+ function docKeyForFilename(name) {
118175
+ if (typeof name !== "string" || name.length === 0 || name.startsWith(".")) return void 0;
118176
+ if (name.includes("/") || name.includes("\\")) return void 0;
118177
+ const key = name.endsWith(".md") ? name.slice(0, -".md".length) : name;
118178
+ return key.length > 0 ? key : void 0;
118090
118179
  }
118091
- function classifyRunModeArgument(raw) {
118092
- if (raw === void 0 || raw === null) return { kind: "omitted" };
118093
- if (typeof raw === "string" && raw.trim() === "") return { kind: "omitted" };
118094
- const mode2 = normalizeRunMode(raw);
118095
- if (mode2) return { kind: "valid", mode: mode2 };
118096
- return { kind: "invalid", provided: describeProvided(raw) };
118180
+
118181
+ // src/tasks/launch.ts
118182
+ init_esm_shims();
118183
+ import { spawnSync } from "child_process";
118184
+
118185
+ // src/tasks/memory-verbs.ts
118186
+ init_esm_shims();
118187
+ import { readFileSync as readFileSync10 } from "fs";
118188
+
118189
+ // src/tasks/sdk.ts
118190
+ init_esm_shims();
118191
+ init_config();
118192
+ init_resolve();
118193
+ var DEFAULT_URLS = {
118194
+ credentialServerUrl: "http://localhost:3002",
118195
+ buyerServerUrl: "http://localhost:3003",
118196
+ sellerServerUrl: "http://localhost:3003",
118197
+ bundlerPaymasterUrl: "https://rpc.zerodev.app/api/v3/802751ef-4785-4586-873d-687b4c8734a2/chain/84532"
118198
+ };
118199
+ async function getCliSdk() {
118200
+ const config = loadConfig();
118201
+ if (!config) throw new Error("No config found. Run `ametyst login --api-key <KEY>` first.");
118202
+ const resolved = await resolveApiKey(config.credentialStore ?? "keychain");
118203
+ const apiKey = resolved.apiKey;
118204
+ if (!apiKey) {
118205
+ throw new Error(
118206
+ resolved.storeError ? `No API key found \u2014 the credential store is unavailable (${resolved.storeError}). Set ${API_KEY_ENV_VAR} or run \`ametyst login --api-key <KEY>\`.` : `No API key found. Run \`ametyst login --api-key <KEY>\` first, or set ${API_KEY_ENV_VAR}.`
118207
+ );
118208
+ }
118209
+ const { AmetystSDK: AmetystSDK2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
118210
+ const sdk = new AmetystSDK2({
118211
+ bundlerPaymasterUrl: config.bundlerPaymasterUrl || DEFAULT_URLS.bundlerPaymasterUrl,
118212
+ credentialServerUrl: config.credentialServerUrl || DEFAULT_URLS.credentialServerUrl,
118213
+ buyerServerUrl: config.buyerServerUrl || DEFAULT_URLS.buyerServerUrl,
118214
+ sellerServerUrl: config.sellerServerUrl || DEFAULT_URLS.sellerServerUrl
118215
+ });
118216
+ return { sdk, apiKey };
118097
118217
  }
118098
- function unquoteScalar(raw) {
118099
- let v = raw.trim();
118100
- const comment = v.match(/(?:^|\s)#.*$/);
118101
- if (comment) v = v.slice(0, comment.index === 0 ? 0 : comment.index).trim();
118102
- if (v.length >= 2 && (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'"))) {
118103
- v = v.slice(1, -1).trim();
118218
+
118219
+ // src/tasks/memory-verbs.ts
118220
+ function emit(json, payload, human) {
118221
+ if (json) {
118222
+ console.log(JSON.stringify(payload, null, 2));
118223
+ return;
118104
118224
  }
118105
- return v;
118225
+ console.log(human());
118106
118226
  }
118107
- function parseDefaultRunMode(body) {
118108
- if (typeof body !== "string" || !body) return void 0;
118109
- const head = body.replace(/^\uFEFF/, "").slice(0, FRONTMATTER_SCAN_LIMIT);
118110
- const opener = head.match(/^---[ \t]*\r?\n/);
118111
- if (!opener) return void 0;
118112
- const rest = head.slice(opener[0].length);
118113
- const closer = rest.search(/^(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/m);
118114
- if (closer < 0) return void 0;
118115
- const block = rest.slice(0, closer);
118116
- const hit = block.match(/^defaultRunMode[ \t]*:[ \t]*(.*)$/m);
118117
- if (!hit) return void 0;
118118
- return normalizeRunMode(unquoteScalar(hit[1] ?? ""));
118227
+ function fail(res, what) {
118228
+ const code = res.code ? ` (${res.code})` : "";
118229
+ throw new Error(`could not ${what}${code}: ${res.error ?? "unknown error"}`);
118119
118230
  }
118120
- function resolveRunMode(explicit, body) {
118121
- const arg = classifyRunModeArgument(explicit);
118122
- if (arg.kind === "invalid") {
118123
- return { ok: false, error: "invalid_mode", provided: arg.provided, accepted: ACCEPTED_RUN_MODES };
118231
+ function isDocMissing404(res) {
118232
+ if (res.status === "ok" || res.code !== 404) return false;
118233
+ const raw = res.error ?? "";
118234
+ let message = raw;
118235
+ const bodyStart = raw.indexOf("{");
118236
+ if (bodyStart !== -1) {
118237
+ try {
118238
+ const body = JSON.parse(raw.slice(bodyStart));
118239
+ if (body.code === "loop_memory_doc_not_found") return true;
118240
+ if (typeof body.message === "string") message = body.message;
118241
+ } catch {
118242
+ }
118124
118243
  }
118125
- if (arg.kind === "valid") return { ok: true, mode: arg.mode, source: "explicit" };
118126
- const fromBody = parseDefaultRunMode(body);
118127
- if (fromBody) return { ok: true, mode: fromBody, source: "frontmatter" };
118128
- return { ok: true, mode: "in-chat", source: "default" };
118244
+ return /\bNo memory doc\b/.test(message);
118129
118245
  }
118130
-
118131
- // src/loops/estimate.ts
118132
- init_esm_shims();
118133
- function estimateBlastRadius(loop2) {
118134
- const g = loop2.graphJson ?? {};
118135
- const nodes = Array.isArray(g.nodes) ? g.nodes : [];
118136
- const steps = nodes.length;
118137
- const paidSteps = nodes.filter(
118138
- (n) => n?.type === "spend" || n?.data?.paid === true || n?.paid === true
118139
- ).length;
118140
- const costHints = nodes.map((n) => Number(n?.data?.estCostEur ?? n?.estCostEur)).filter((x) => !Number.isNaN(x));
118141
- const estCostEur = costHints.length ? costHints.reduce((a, b) => a + b, 0) : null;
118142
- return { steps, paidSteps, estCostEur };
118246
+ function isNotASeat404(res) {
118247
+ return res.status !== "ok" && res.code === 404 && /not a seat/i.test(res.error ?? "");
118143
118248
  }
118144
-
118145
- // src/loops/dashboard-template.ts
118146
- init_esm_shims();
118147
- var DEFAULT_DASHBOARD_FILES = [
118148
- "VISION.md",
118149
- "CONSTRAINTS.md",
118150
- "QUEUE.md",
118151
- "STATUS.md",
118152
- "README.md",
118153
- "rounds.jsonl"
118154
- ];
118155
- function parseProcessSpec(manifest) {
118156
- if (typeof manifest !== "string" || !manifest.trim()) return null;
118157
- let parsed;
118158
- try {
118159
- parsed = JSON.parse(manifest);
118160
- } catch {
118161
- return null;
118249
+ function parseScopeFlag(raw, allowAll) {
118250
+ if (raw === void 0) return void 0;
118251
+ const s = raw.trim();
118252
+ if (s === "shared" || s === "member") return s;
118253
+ if (allowAll && s === "all") return "all";
118254
+ throw new Error(
118255
+ `--scope must be ${allowAll ? "shared, member or all" : "shared or member"}, got ${JSON.stringify(raw)}`
118256
+ );
118257
+ }
118258
+ function parseKindFlag(raw) {
118259
+ const kind = raw?.trim() || void 0;
118260
+ if (kind !== void 0 && !isRecordKindToken(kind)) {
118261
+ throw new Error(
118262
+ `--kind must be a token matching [a-z0-9-]{1,64} \u2014 a reserved kind (${RESERVED_RECORD_KINDS.join(", ")}) or a free kind the task declares, got ${JSON.stringify(raw)}`
118263
+ );
118162
118264
  }
118163
- const raw = parsed?.processSpec;
118164
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
118165
- const spec = raw;
118166
- const stages = (Array.isArray(spec.stages) ? spec.stages : []).map((s) => {
118167
- if (typeof s === "string" && s.trim()) return { label: s.trim() };
118168
- if (s && typeof s === "object" && !Array.isArray(s)) {
118169
- const o = s;
118170
- if (typeof o.label === "string" && o.label.trim()) {
118171
- return {
118172
- label: o.label.trim(),
118173
- ...typeof o.id === "string" && o.id.trim() ? { id: o.id.trim() } : {},
118174
- ...typeof o.detail === "string" && o.detail.trim() ? { detail: o.detail.trim() } : {}
118175
- };
118176
- }
118177
- }
118178
- return null;
118179
- }).filter((s) => s !== null);
118180
- const strings = (v) => (Array.isArray(v) ? v : []).filter((x) => typeof x === "string" && x.trim() !== "");
118181
- const out = {
118182
- stages,
118183
- inputs: strings(spec.inputs),
118184
- outputs: strings(spec.outputs),
118185
- ...typeof spec.title === "string" && spec.title.trim() ? { title: spec.title.trim() } : {}
118186
- };
118187
- return stages.length || out.inputs.length || out.outputs.length ? out : null;
118265
+ return kind;
118188
118266
  }
118189
- function defaultDashboardManifest() {
118190
- return JSON.stringify({ files: [...DEFAULT_DASHBOARD_FILES] });
118267
+ function parseKeyFlag(raw) {
118268
+ const key = raw?.trim() || void 0;
118269
+ if (key !== void 0 && !isRecordKey(key)) {
118270
+ throw new Error(`--key must match [A-Za-z0-9._:-]{1,200} (no whitespace, no "/"), got ${JSON.stringify(raw)}`);
118271
+ }
118272
+ return key;
118191
118273
  }
118192
- function embedJson(value2) {
118193
- return JSON.stringify(value2).replace(/</g, "\\u003c");
118274
+ async function readDocResolved(sdk, apiKey, slug, key, scope, declared) {
118275
+ if (scope === "shared" || scope === "member") {
118276
+ const res = await sdk.loops.memory.getDoc(apiKey, slug, key, { scope });
118277
+ if (res.status === "ok") return { status: "ok", doc: res.doc, scope };
118278
+ return isDocMissing404(res) ? { status: "missing" } : { status: "nok", res };
118279
+ }
118280
+ if (declared) {
118281
+ const res = await sdk.loops.memory.getDoc(apiKey, slug, key);
118282
+ if (res.status === "ok") return { status: "ok", doc: res.doc, scope: declared };
118283
+ return isDocMissing404(res) ? { status: "missing" } : { status: "nok", res };
118284
+ }
118285
+ const own = await sdk.loops.memory.getDoc(apiKey, slug, key, { scope: "member" });
118286
+ if (own.status === "ok") return { status: "ok", doc: own.doc, scope: "member" };
118287
+ if (!isDocMissing404(own) && !isNotASeat404(own)) return { status: "nok", res: own };
118288
+ const shared = await sdk.loops.memory.getDoc(apiKey, slug, key, { scope: "shared" });
118289
+ if (shared.status === "ok") return { status: "ok", doc: shared.doc, scope: "shared" };
118290
+ return isDocMissing404(shared) ? { status: "missing" } : { status: "nok", res: shared };
118194
118291
  }
118195
- function escapeHtml(s) {
118196
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
118292
+ function renderRecord(r) {
118293
+ const keyPart = r.key ? ` ${r.key}` : "";
118294
+ const archivedPart = r.archived ? ` (archived${r.archivedAt ? ` ${r.archivedAt}` : ""})` : "";
118295
+ if ("content" in r) return `\u2500\u2500 ${r.createdAt} [${r.kind}]${keyPart}${archivedPart}
118296
+ ${r.content}`;
118297
+ return `\u2500\u2500 ${r.createdAt} [${r.kind}]${keyPart}${archivedPart} ${r.summary}`;
118197
118298
  }
118198
- function renderLoopDashboardTemplate(args) {
118199
- const title = escapeHtml(args.processSpec?.title ?? args.slug);
118200
- const seed = embedJson({
118201
- slug: args.slug,
118202
- descriptionShort: args.descriptionShort ?? "",
118203
- processSpec: args.processSpec ?? null
118204
- });
118205
- return `<!doctype html>
118206
- <html lang="en">
118207
- <head>
118208
- <meta charset="utf-8">
118209
- <meta name="viewport" content="width=device-width, initial-scale=1">
118210
- <title>${title} \u2014 loop dashboard</title>
118211
- <style>
118212
- :root { --bg:#f7f7fb; --panel:#fff; --border:#e3e3ee; --muted:#6b6b80; --accent:#5b5bd6; --ok:#1a7f37; --bad:#b42318; }
118213
- * { box-sizing: border-box; }
118214
- body { margin:0; font:14px/1.5 -apple-system, "Segoe UI", Roboto, sans-serif; background:var(--bg); color:#1d1d2b; padding:24px; }
118215
- h1 { font-size:20px; margin:0 0 4px; }
118216
- h2 { font-size:14px; margin:24px 0 8px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
118217
- .sub { color:var(--muted); margin:0 0 16px; }
118218
- .panel { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px; }
118219
- .map { display:flex; flex-wrap:wrap; align-items:stretch; gap:8px; }
118220
- .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; }
118221
- .stage .label { font-weight:600; }
118222
- .stage .detail { color:var(--muted); font-size:12px; margin-top:2px; }
118223
- .arrow { align-self:center; color:var(--muted); }
118224
- .io { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
118225
- ul { margin:6px 0 0; padding-left:18px; }
118226
- table { width:100%; border-collapse:collapse; background:var(--panel); border:1px solid var(--border); border-radius:12px; overflow:hidden; }
118227
- th, td { text-align:left; padding:8px 12px; border-top:1px solid var(--border); font-size:13px; vertical-align:top; }
118228
- thead th { border-top:none; background:var(--bg); color:var(--muted); font-weight:600; }
118229
- .ok { color:var(--ok); } .bad { color:var(--bad); }
118230
- details { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:10px 14px; margin-bottom:8px; }
118231
- summary { cursor:pointer; font-weight:600; }
118232
- pre { overflow-x:auto; font-size:12px; background:var(--bg); border-radius:8px; padding:10px; }
118233
- .empty { color:var(--muted); font-style:italic; }
118234
- #live { font-size:12px; color:var(--muted); float:right; }
118235
- </style>
118236
- </head>
118237
- <body>
118238
- <span id="live">loading\u2026</span>
118239
- <h1 id="title"></h1>
118240
- <p class="sub" id="desc"></p>
118241
-
118242
- <h2>Process</h2>
118243
- <div id="map" class="map panel"></div>
118244
-
118245
- <h2>Inputs &amp; outputs</h2>
118246
- <div class="io">
118247
- <div class="panel"><strong>Inputs</strong><ul id="inputs"></ul></div>
118248
- <div class="panel"><strong>Outputs</strong><ul id="outputs"></ul></div>
118249
- </div>
118250
-
118251
- <h2>Rounds</h2>
118252
- <div id="rounds"></div>
118253
-
118254
- <h2>Files</h2>
118255
- <div id="files"></div>
118256
-
118257
- <script type="application/json" id="loop-seed">${seed}</script>
118258
- <script>
118259
- (function () {
118260
- "use strict";
118261
- var seed = JSON.parse(document.getElementById("loop-seed").textContent);
118262
- document.getElementById("title").textContent = (seed.processSpec && seed.processSpec.title) || seed.slug;
118263
- document.getElementById("desc").textContent = seed.descriptionShort || "";
118264
- document.title = seed.slug + " \u2014 loop dashboard";
118265
-
118266
- function el(tag, cls, text) {
118267
- var e = document.createElement(tag);
118268
- if (cls) e.className = cls;
118269
- if (text !== undefined) e.textContent = text;
118270
- return e;
118299
+ function defaultDocKey(manifest) {
118300
+ if (manifest === void 0) return void 0;
118301
+ return manifest?.docs[0]?.key ?? null;
118302
+ }
118303
+ async function taskMemoryGetVerb(rawSlug, opts) {
118304
+ const slug = rawSlug.trim();
118305
+ const parsedLimitEarly = opts.limit === void 0 ? void 0 : Number(opts.limit);
118306
+ if (opts.limit !== void 0 && (opts.limit.trim() === "" || !Number.isFinite(parsedLimitEarly))) {
118307
+ throw new Error(`--limit must be a number, got ${JSON.stringify(opts.limit)}`);
118271
118308
  }
118272
-
118273
- // \u2500\u2500 Process map + IO from the embedded spec (static \u2014 rendered once). \u2500\u2500
118274
- var map = document.getElementById("map");
118275
- var spec = seed.processSpec;
118276
- if (spec && spec.stages && spec.stages.length) {
118277
- spec.stages.forEach(function (s, i) {
118278
- if (i > 0) map.appendChild(el("div", "arrow", "\\u2192"));
118279
- var box = el("div", "stage");
118280
- box.appendChild(el("div", "label", s.label));
118281
- if (s.detail) box.appendChild(el("div", "detail", s.detail));
118282
- map.appendChild(box);
118283
- });
118284
- } else {
118285
- map.appendChild(el("span", "empty", "No process spec on this loop \\u2014 live files and rounds below."));
118309
+ const scope = parseScopeFlag(opts.scope, true);
118310
+ const kind = parseKindFlag(opts.kind);
118311
+ const key = parseKeyFlag(opts.key);
118312
+ let archived;
118313
+ if (opts.archived !== void 0) {
118314
+ const a = opts.archived.trim().toLowerCase();
118315
+ if (a === "true") archived = true;
118316
+ else if (a === "false") archived = false;
118317
+ else if (a === "all") archived = "all";
118318
+ else throw new Error(`--archived must be false, true or all, got ${JSON.stringify(opts.archived)}`);
118286
118319
  }
118287
- function fillList(id, items) {
118288
- var ul = document.getElementById(id);
118289
- ul.textContent = "";
118290
- if (!items || !items.length) { ul.appendChild(el("li", "empty", "\\u2014")); return; }
118291
- items.forEach(function (x) { ul.appendChild(el("li", null, x)); });
118320
+ const fieldsRaw = opts.fields?.trim();
118321
+ if (fieldsRaw !== void 0 && fieldsRaw !== "full" && fieldsRaw !== "keys") {
118322
+ throw new Error(`--fields must be full or keys, got ${JSON.stringify(opts.fields)}`);
118292
118323
  }
118293
- fillList("inputs", spec && spec.inputs);
118294
- fillList("outputs", spec && spec.outputs);
118295
-
118296
- // \u2500\u2500 Live data: rounds + files, refreshed from GET /data. \u2500\u2500
118297
- function parseJsonl(text) {
118298
- var rows = [];
118299
- (text || "").split("\\n").forEach(function (line) {
118300
- line = line.trim();
118301
- if (!line) return;
118302
- try { rows.push(JSON.parse(line)); } catch (e) { /* skip malformed line */ }
118303
- });
118304
- return rows;
118324
+ const fields = fieldsRaw === "keys" ? "keys" : void 0;
118325
+ const { sdk, apiKey } = await getCliSdk();
118326
+ const json = opts.json === true;
118327
+ const parsedLimit = parsedLimitEarly;
118328
+ const recordScope = scope === "shared" || scope === "member" ? scope : void 0;
118329
+ const keyPrefix = opts.keyPrefix?.trim() || void 0;
118330
+ const since = opts.since?.trim() || void 0;
118331
+ const cursor = opts.cursor?.trim() || void 0;
118332
+ const count = opts.count === true;
118333
+ const hasRecordFilter = kind !== void 0 || keyPrefix !== void 0 || archived !== void 0 || since !== void 0 || fields !== void 0 || count || parsedLimit !== void 0 || cursor !== void 0;
118334
+ const query = {
118335
+ ...kind ? { kind } : {},
118336
+ ...archived !== void 0 ? { archived } : {},
118337
+ // ⛔ THE KEY GOES IN WHENEVER A LISTING RUNS, not only under `--records`.
118338
+ // It used to be gated on `opts.records === true`, which was correct while
118339
+ // `--records` was the ONLY way to reach a listing. Once a filter also routes
118340
+ // here, `--key X --kind pbi` fell between the two: the open-by-key branch was
118341
+ // skipped (a filter was present) and the key never entered the query — so it
118342
+ // silently listed pbi records and printed a DIFFERENT one. That is the same
118343
+ // class of confidently-wrong answer this card exists to remove, reintroduced
118344
+ // on another axis.
118345
+ ...key !== void 0 && (opts.records === true || hasRecordFilter) ? { key } : {},
118346
+ ...keyPrefix ? { keyPrefix } : {},
118347
+ ...since ? { since } : {},
118348
+ ...fields ? { fields } : {},
118349
+ ...count ? { count: true } : {},
118350
+ ...parsedLimit !== void 0 ? { limit: parsedLimit } : {},
118351
+ ...cursor ? { cursor } : {},
118352
+ ...recordScope ? { scope: recordScope } : {}
118353
+ };
118354
+ if (opts.usage === true) {
118355
+ const res = await sdk.loops.memory.usage(apiKey, slug);
118356
+ if (res.status !== "ok") fail(res, "read memory usage");
118357
+ emit(json, { taskSlug: slug, usage: res.usage }, () => JSON.stringify(res.usage, null, 2));
118358
+ return;
118305
118359
  }
118306
-
118307
- function renderRounds(fires, rounds) {
118308
- var host = document.getElementById("rounds");
118309
- host.textContent = "";
118310
- if (!fires.length && !rounds.length) {
118311
- var p = el("div", "panel"); p.appendChild(el("span", "empty", "No rounds yet \\u2014 this fills in as the loop runs."));
118312
- host.appendChild(p);
118360
+ const explicitDoc = opts.doc?.trim();
118361
+ if (explicitDoc) {
118362
+ const manifest2 = scope === void 0 || scope === "all" ? await readTaskManifest(sdk, apiKey, slug) : void 0;
118363
+ const declared = declaredDocScope(manifest2 ?? null, explicitDoc);
118364
+ const read = await readDocResolved(sdk, apiKey, slug, explicitDoc, scope, declared);
118365
+ if (read.status === "ok") {
118366
+ emit(json, { taskSlug: slug, doc: read.doc, scope: read.scope }, () => read.doc.content);
118313
118367
  return;
118314
118368
  }
118315
- var table = document.createElement("table");
118316
- var thead = document.createElement("thead");
118317
- var hr = document.createElement("tr");
118318
- ["when", "duration", "turns", "tokens", "exit", "round detail (rounds.jsonl)"].forEach(function (h) {
118319
- hr.appendChild(el("th", null, h));
118320
- });
118321
- thead.appendChild(hr); table.appendChild(thead);
118322
- var tbody = document.createElement("tbody");
118323
- var n = Math.max(fires.length, rounds.length);
118324
- for (var i = n - 1; i >= 0; i--) { // newest first
118325
- var f = fires[i] || {};
118326
- var r = rounds[i];
118327
- var tr = document.createElement("tr");
118328
- tr.appendChild(el("td", null, f.ts || (r && r.ts) || "\\u2014"));
118329
- tr.appendChild(el("td", null, f.duration_s != null ? f.duration_s + "s" : "\\u2014"));
118330
- tr.appendChild(el("td", null, f.turns != null ? String(f.turns) : "\\u2014"));
118331
- tr.appendChild(el("td", null, f.tokens_total != null ? Number(f.tokens_total).toLocaleString() : "\\u2014"));
118332
- tr.appendChild(el("td", f.exit === 0 ? "ok" : f.exit != null ? "bad" : null, f.exit != null ? String(f.exit) : "\\u2014"));
118333
- var detail = el("td");
118334
- if (f.launch_failed) { detail.textContent = "launch failed \\u2014 " + (f.reason || "unknown reason"); }
118335
- else if (r) { var pre = document.createElement("pre"); pre.textContent = JSON.stringify(r, null, 1); detail.appendChild(pre); }
118336
- else detail.textContent = "\\u2014";
118337
- tr.appendChild(detail);
118338
- tbody.appendChild(tr);
118369
+ if (read.status === "missing") {
118370
+ emit(json, { taskSlug: slug, doc: null, firstRun: true }, () => `No "${explicitDoc}" document yet for ${slug} \u2014 nothing has written one.`);
118371
+ return;
118339
118372
  }
118340
- table.appendChild(tbody);
118341
- host.appendChild(table);
118342
- }
118343
-
118344
- function renderFiles(files) {
118345
- var host = document.getElementById("files");
118346
- host.textContent = "";
118347
- var names = Object.keys(files || {}).filter(function (n) { return n !== "rounds.jsonl"; });
118348
- if (!names.length) { var p = el("div", "panel"); p.appendChild(el("span", "empty", "No files surfaced by the manifest.")); host.appendChild(p); return; }
118349
- names.forEach(function (name) {
118350
- var d = document.createElement("details");
118351
- d.appendChild(el("summary", null, name));
118352
- var pre = document.createElement("pre");
118353
- pre.textContent = files[name];
118354
- d.appendChild(pre);
118355
- host.appendChild(d);
118356
- });
118373
+ fail(read.res, `read the "${explicitDoc}" document`);
118357
118374
  }
118358
-
118359
- function refresh() {
118360
- fetch("/data").then(function (res) { return res.json(); }).then(function (data) {
118361
- var fires = parseJsonl(data.state && data.state["fires.jsonl"]);
118362
- var rounds = parseJsonl(data.files && data.files["rounds.jsonl"]);
118363
- renderRounds(fires, rounds);
118364
- renderFiles(data.files);
118365
- document.getElementById("live").textContent = "updated " + new Date().toLocaleTimeString();
118366
- }).catch(function () {
118367
- document.getElementById("live").textContent = "server stopped";
118368
- });
118375
+ if (key !== void 0 && opts.records !== true && !hasRecordFilter) {
118376
+ const history = opts.history === true;
118377
+ const res = await sdk.loops.memory.getRecordByKey(apiKey, slug, key, history ? { history: true } : void 0);
118378
+ if (res.status !== "ok") fail(res, `open the item "${key}"`);
118379
+ const versions = history ? res.versions ?? [] : void 0;
118380
+ emit(json, { taskSlug: slug, key, record: res.record, ...versions ? { versions } : {} }, () => versions ? [`\u2500\u2500 ${key}: ${versions.length} version(s), oldest first`, ...versions.map(renderRecord)].join("\n\n") : renderRecord(res.record));
118381
+ return;
118369
118382
  }
118370
- refresh();
118371
- setInterval(refresh, 5000);
118372
- })();
118373
- </script>
118374
- </body>
118375
- </html>
118376
- `;
118377
- }
118378
- function injectDefaultDashboard(loop2) {
118379
- const hasHtml = typeof loop2.dashboardHtml === "string" && loop2.dashboardHtml.trim() !== "";
118380
- if (hasHtml) return;
118381
- if (loop2.dashboardManifest && typeof loop2.dashboardManifest === "object") {
118382
- try {
118383
- loop2.dashboardManifest = JSON.stringify(loop2.dashboardManifest);
118384
- } catch {
118383
+ if (opts.records === true || hasRecordFilter) {
118384
+ const res = await sdk.loops.memory.listRecords(apiKey, slug, query);
118385
+ if (res.status !== "ok") fail(res, "read the records");
118386
+ if (count) {
118387
+ emit(json, { taskSlug: slug, count: res.count ?? 0 }, () => String(res.count ?? 0));
118388
+ return;
118385
118389
  }
118390
+ const items2 = res.items ?? [];
118391
+ const nextCursor = res.nextCursor ?? null;
118392
+ emit(json, { taskSlug: slug, records: items2, nextCursor, firstRun: items2.length === 0 && !cursor }, () => items2.length === 0 ? `No records for ${slug}${kind ? ` of kind ${kind}` : ""}${archived === true ? " (archived)" : ""}.` : [
118393
+ ...items2.map(renderRecord),
118394
+ ...nextCursor ? [`\u2500\u2500 more: --cursor ${nextCursor}`] : []
118395
+ ].join("\n\n"));
118396
+ return;
118386
118397
  }
118387
- const manifest = typeof loop2.dashboardManifest === "string" ? loop2.dashboardManifest : void 0;
118388
- loop2.dashboardHtml = renderLoopDashboardTemplate({
118389
- slug: typeof loop2.slug === "string" ? loop2.slug : "loop",
118390
- descriptionShort: typeof loop2.descriptionShort === "string" ? loop2.descriptionShort : void 0,
118391
- processSpec: parseProcessSpec(manifest)
118392
- });
118393
- if (!manifest || !manifest.trim()) loop2.dashboardManifest = defaultDashboardManifest();
118394
- }
118395
-
118396
- // src/loops/memory-verbs.ts
118397
- init_esm_shims();
118398
- import { readFileSync as readFileSync10 } from "fs";
118399
-
118400
- // src/loops/sdk.ts
118401
- init_esm_shims();
118402
- init_config();
118403
- init_resolve();
118404
- var DEFAULT_URLS = {
118405
- credentialServerUrl: "http://localhost:3002",
118406
- buyerServerUrl: "http://localhost:3003",
118407
- sellerServerUrl: "http://localhost:3003",
118408
- bundlerPaymasterUrl: "https://rpc.zerodev.app/api/v3/802751ef-4785-4586-873d-687b4c8734a2/chain/84532"
118409
- };
118410
- async function getCliSdk() {
118411
- const config = loadConfig();
118412
- if (!config) throw new Error("No config found. Run `ametyst login --api-key <KEY>` first.");
118413
- const resolved = await resolveApiKey(config.credentialStore ?? "keychain");
118414
- const apiKey = resolved.apiKey;
118415
- if (!apiKey) {
118416
- throw new Error(
118417
- resolved.storeError ? `No API key found \u2014 the credential store is unavailable (${resolved.storeError}). Set ${API_KEY_ENV_VAR} or run \`ametyst login --api-key <KEY>\`.` : `No API key found. Run \`ametyst login --api-key <KEY>\` first, or set ${API_KEY_ENV_VAR}.`
118398
+ const manifest = scope === void 0 || scope === "all" ? await readTaskManifest(sdk, apiKey, slug) : void 0;
118399
+ const manifestUnreadable = manifest === void 0 && (scope === void 0 || scope === "all");
118400
+ const docKey = defaultDocKey(manifest) ?? void 0;
118401
+ const [doc, records] = await Promise.all([
118402
+ docKey === void 0 ? Promise.resolve({ status: "missing" }) : readDocResolved(sdk, apiKey, slug, docKey, scope, declaredDocScope(manifest ?? null, docKey)),
118403
+ sdk.loops.memory.listRecords(apiKey, slug, query)
118404
+ ]);
118405
+ if (doc.status === "nok") fail(doc.res, `read the "${docKey ?? ""}" document`);
118406
+ if (records.status !== "ok") fail(records, "read the records");
118407
+ const defaultDoc = doc.status === "ok" ? doc.doc : null;
118408
+ const items = records.items ?? [];
118409
+ const firstRun = defaultDoc === null && items.length === 0 && !manifestUnreadable;
118410
+ emit(
118411
+ json,
118412
+ {
118413
+ taskSlug: slug,
118414
+ doc: defaultDoc,
118415
+ docKey: docKey ?? null,
118416
+ manifestUnreadable,
118417
+ records: items,
118418
+ nextCursor: records.nextCursor ?? null,
118419
+ firstRun
118420
+ },
118421
+ () => manifestUnreadable && items.length === 0 ? `Could not read ${slug}'s manifest, so no default document was looked up \u2014 this is NOT "no memory yet". Retry, or name a document with --doc.` : firstRun ? `No memory yet for ${slug} \u2014 nothing has written any.` : [
118422
+ // No doc line at all when the task declares none — an empty `── (none)`
118423
+ // header invites the reader to look for a doc that does not exist.
118424
+ ...docKey === void 0 ? [] : [defaultDoc ? `\u2500\u2500 ${docKey}
118425
+ ${defaultDoc.content}` : `\u2500\u2500 ${docKey}
118426
+ (none)`],
118427
+ `\u2500\u2500 ${items.length} record(s)`,
118428
+ ...items.map(renderRecord)
118429
+ ].join("\n\n")
118430
+ );
118431
+ }
118432
+ async function taskMemoryAppendVerb(rawSlug, opts) {
118433
+ const slug = rawSlug.trim();
118434
+ if (opts.content !== void 0 && opts.file !== void 0) {
118435
+ throw new Error("pass --content or --file, not both \u2014 they are two spellings of one payload");
118436
+ }
118437
+ const docKey = opts.doc?.trim();
118438
+ if (docKey && (opts.kind !== void 0 || opts.key !== void 0 || opts.archived || opts.note !== void 0)) {
118439
+ throw new Error("pass --doc <key> or --kind <kind> [--key --archived --note], not both \u2014 a document is upserted, a record is appended");
118440
+ }
118441
+ const kind = opts.kind?.trim() || "run";
118442
+ if (!docKey) parseKindFlag(kind);
118443
+ const key = parseKeyFlag(opts.key);
118444
+ const archived = opts.archived === true;
118445
+ const note = opts.note?.trim() || void 0;
118446
+ const scope = parseScopeFlag(opts.scope, false);
118447
+ const content = opts.file !== void 0 ? readFileSync10(opts.file, "utf-8") : opts.content ?? "";
118448
+ if (!content) {
118449
+ throw new Error("nothing to store: pass --content <text> or --file <path>");
118450
+ }
118451
+ if (!docKey) {
118452
+ const early = checkRecordWrite({ kind, key, content, archived, declaredKinds: void 0 });
118453
+ if (!early.ok) throw new Error(`${early.code}: ${early.message}`);
118454
+ }
118455
+ const { sdk, apiKey } = await getCliSdk();
118456
+ const manifest = scope && docKey ? void 0 : await readTaskManifest(sdk, apiKey, slug);
118457
+ const warnUndeclared = (what) => {
118458
+ if (scope || manifest === void 0) return;
118459
+ console.warn(
118460
+ `\u26A0\uFE0F ${what} is not in ${slug}'s stateDocs manifest \u2014 written to the seat (member) row. Pass --scope shared|member to choose, or declare it in the manifest.`
118418
118461
  );
118462
+ };
118463
+ if (docKey) {
118464
+ if (!declaredDocScope(manifest ?? null, docKey)) warnUndeclared(`doc "${docKey}"`);
118465
+ const res2 = await sdk.loops.memory.putDoc(apiKey, slug, docKey, { content, ...scope ? { scope } : {} });
118466
+ if (res2.status !== "ok") fail(res2, `save the "${docKey}" state document`);
118467
+ emit(opts.json === true, { taskSlug: slug, docKey, scope: scope ?? null, bytes: Buffer.byteLength(content) }, () => `\u2705 ${slug}: "${docKey}" document saved${scope ? ` (${scope})` : ""} (${Buffer.byteLength(content)} bytes).`);
118468
+ return;
118419
118469
  }
118420
- const { AmetystSDK: AmetystSDK2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
118421
- const sdk = new AmetystSDK2({
118422
- bundlerPaymasterUrl: config.bundlerPaymasterUrl || DEFAULT_URLS.bundlerPaymasterUrl,
118423
- credentialServerUrl: config.credentialServerUrl || DEFAULT_URLS.credentialServerUrl,
118424
- buyerServerUrl: config.buyerServerUrl || DEFAULT_URLS.buyerServerUrl,
118425
- sellerServerUrl: config.sellerServerUrl || DEFAULT_URLS.sellerServerUrl
118470
+ const check = checkRecordWrite({ kind, key, content, archived, declaredKinds: declaredRecordKinds(manifest) });
118471
+ if (!check.ok) throw new Error(`${check.code}: ${check.message}`);
118472
+ if (!declaredRecordScope(manifest ?? null, kind)) warnUndeclared(`record kind "${kind}"`);
118473
+ const res = await sdk.loops.memory.appendRecord(apiKey, slug, {
118474
+ content,
118475
+ kind,
118476
+ ...key !== void 0 ? { key } : {},
118477
+ ...archived ? { archived: true } : {},
118478
+ ...note !== void 0 ? { archiveNote: note } : {},
118479
+ ...scope ? { scope } : {}
118426
118480
  });
118427
- return { sdk, apiKey };
118481
+ if (res.status !== "ok") fail(res, "append the record");
118482
+ const noun = check.shape === "item" ? `${kind} item "${key}"${archived ? " (archived)" : ""}` : `${kind} record`;
118483
+ emit(
118484
+ opts.json === true,
118485
+ { taskSlug: slug, kind, ...key !== void 0 ? { key } : {}, shape: check.shape, archived, scope: scope ?? null, bytes: Buffer.byteLength(content) },
118486
+ () => `\u2705 ${slug}: ${noun} ${check.shape === "item" ? "versioned" : "appended"}${scope ? ` (${scope})` : ""} (${Buffer.byteLength(content)} bytes).`
118487
+ );
118428
118488
  }
118429
-
118430
- // src/loops/memory-verbs.ts
118431
- function emit(json, payload, human) {
118432
- if (json) {
118433
- console.log(JSON.stringify(payload, null, 2));
118434
- return;
118489
+ async function taskMemoryArchiveVerb(rawSlug, opts) {
118490
+ const slug = rawSlug.trim();
118491
+ const key = parseKeyFlag(opts.key);
118492
+ if (key === void 0) throw new Error("which item? pass --key <k> \u2014 the item's key as it was written");
118493
+ const note = opts.note?.trim() || void 0;
118494
+ const { sdk, apiKey } = await getCliSdk();
118495
+ const res = await sdk.loops.memory.archiveRecord(apiKey, slug, key, note ? { note } : void 0);
118496
+ if (res.status !== "ok") {
118497
+ if (res.code === 409) throw new Error(`"${key}" is already archived \u2014 its latest version is closed (409): ${res.error ?? ""}`.trim());
118498
+ fail(res, `archive "${key}"`);
118435
118499
  }
118436
- console.log(human());
118500
+ emit(
118501
+ opts.json === true,
118502
+ { taskSlug: slug, key, kind: res.record.kind, archivedAt: res.record.archivedAt, archiveNote: res.record.archiveNote ?? null },
118503
+ () => `\u2705 ${slug}: "${key}" archived${note ? ` (${note})` : ""} at ${res.record.archivedAt}. History intact.`
118504
+ );
118437
118505
  }
118438
- function fail(res, what) {
118439
- const code = res.code ? ` (${res.code})` : "";
118440
- throw new Error(`could not ${what}${code}: ${res.error ?? "unknown error"}`);
118506
+
118507
+ // src/tasks/launch.ts
118508
+ var TASK_INPUT_ENV = "AMETYST_TASK_INPUT";
118509
+ var LAUNCH_INPUT_HEADER = "LAUNCH INPUT \u2014 the user's arguments for this run; treat them exactly as if the user had typed them in chat:";
118510
+ function launchInputBlock(input) {
118511
+ if (typeof input !== "string" || input.trim() === "") return "";
118512
+ return `${LAUNCH_INPUT_HEADER}
118513
+ ${input}
118514
+
118515
+ `;
118441
118516
  }
118442
- function isDocMissing404(res) {
118443
- if (res.status === "ok" || res.code !== 404) return false;
118444
- const raw = res.error ?? "";
118445
- let message = raw;
118446
- const bodyStart = raw.indexOf("{");
118447
- if (bodyStart !== -1) {
118448
- try {
118449
- const body = JSON.parse(raw.slice(bodyStart));
118450
- if (body.code === "loop_memory_doc_not_found") return true;
118451
- if (typeof body.message === "string") message = body.message;
118452
- } catch {
118453
- }
118517
+ function readGlobalGitConfig(key) {
118518
+ try {
118519
+ const r = spawnSync("git", ["config", "--global", "--get", key], { encoding: "utf-8" });
118520
+ const v = r.status === 0 ? r.stdout.trim() : "";
118521
+ return v || void 0;
118522
+ } catch {
118523
+ return void 0;
118454
118524
  }
118455
- return /\bNo memory doc\b/.test(message);
118456
- }
118457
- function isNotASeat404(res) {
118458
- return res.status !== "ok" && res.code === 404 && /not a seat/i.test(res.error ?? "");
118459
- }
118460
- function parseScopeFlag(raw, allowAll) {
118461
- if (raw === void 0) return void 0;
118462
- const s = raw.trim();
118463
- if (s === "shared" || s === "member") return s;
118464
- if (allowAll && s === "all") return "all";
118465
- throw new Error(
118466
- `--scope must be ${allowAll ? "shared, member or all" : "shared or member"}, got ${JSON.stringify(raw)}`
118467
- );
118468
118525
  }
118469
- function parseKindFlag(raw) {
118470
- const kind = raw?.trim() || void 0;
118471
- if (kind !== void 0 && !isRecordKindToken(kind)) {
118472
- throw new Error(
118473
- `--kind must be a token matching [a-z0-9-]{1,64} \u2014 a reserved kind (${RESERVED_RECORD_KINDS.join(", ")}) or a free kind the task declares, got ${JSON.stringify(raw)}`
118474
- );
118526
+ function deriveGitIdentityEnv(env = process.env, readGitConfig = readGlobalGitConfig) {
118527
+ const name = readTaskEnv("GIT_AUTHOR_NAME", env)?.trim() || readGitConfig("user.name");
118528
+ const email = readTaskEnv("GIT_AUTHOR_EMAIL", env)?.trim() || readGitConfig("user.email");
118529
+ const out = {};
118530
+ if (name) {
118531
+ out.GIT_AUTHOR_NAME = name;
118532
+ out.GIT_COMMITTER_NAME = name;
118475
118533
  }
118476
- return kind;
118477
- }
118478
- function parseKeyFlag(raw) {
118479
- const key = raw?.trim() || void 0;
118480
- if (key !== void 0 && !isRecordKey(key)) {
118481
- throw new Error(`--key must match [A-Za-z0-9._:-]{1,200} (no whitespace, no "/"), got ${JSON.stringify(raw)}`);
118534
+ if (email) {
118535
+ out.GIT_AUTHOR_EMAIL = email;
118536
+ out.GIT_COMMITTER_EMAIL = email;
118482
118537
  }
118483
- return key;
118538
+ return out;
118484
118539
  }
118485
- async function readDocResolved(sdk, apiKey, slug, key, scope, declared) {
118486
- if (scope === "shared" || scope === "member") {
118487
- const res = await sdk.loops.memory.getDoc(apiKey, slug, key, { scope });
118488
- if (res.status === "ok") return { status: "ok", doc: res.doc, scope };
118489
- return isDocMissing404(res) ? { status: "missing" } : { status: "nok", res };
118540
+ function buildLaunchArgs(dir, opts = {}, slug, deps = {}) {
118541
+ const env = deps.env ?? process.env;
118542
+ const maxBudget = resolveMaxBudgetUsd(opts, env);
118543
+ const docKey = defaultDocKey(opts.memoryManifest);
118544
+ const readDocSentence = typeof docKey === "string" ? `taskMemoryGet({ taskSlug: "${slug}", docKey: "${docKey}" }) for your "${docKey}" document \u2014 the FIRST document this task's memory manifest declares, materialized for you at boot as ${filenameForKey(docKey)} \u2014 then ` : docKey === null ? `THIS TASK DECLARES NO MEMORY DOCUMENT \u2014 do not read one and do not create one; your records ARE its memory. Read ` : `no memory document is named here \u2014 this launcher could not resolve the task's manifest, so do NOT assume one exists. Read `;
118545
+ const writeDocSentence = typeof docKey === "string" ? `, and rewrite your "${docKey}" document with taskMemoryAppend({ taskSlug: "${slug}", docKey: "${docKey}", content: "<the state the next fire needs>" }).` : docKey === null ? `. This task declares no memory document, so there is nothing to rewrite \u2014 do not invent one; the run record and your keyed items are what the next fire reads.` : `. If this task declares a memory document, rewrite it with taskMemoryAppend({ taskSlug: "${slug}", docKey: "<the key its manifest declares>", content: "..." }) \u2014 this launcher could not name it for you, so do not guess a key.`;
118546
+ const prompt = `${launchInputBlock(opts.input)}You are running the Ametyst task${slug ? ` "${slug}"` : ""} headless and unattended.
118547
+
118548
+ ${dir} is THIS FIRE'S OWN directory. Other fires of the same task may be running right now, each with its own directory alongside yours \u2014 wherever the task's SKILL says <LOOPDIR> it means exactly ${dir}, never a sibling's directory and never their shared parent. Derive every path the SKILL asks you to create (links, run-state) from ${dir}; never from a path written literally in the SKILL prose.
118549
+
118550
+ Read the task definition files in ${dir}:
118551
+ - SKILL.md \u2014 the driver; follow it.
118552
+ - VISION.md \u2014 the objective / done-condition.
118553
+ - CONSTRAINTS.md \u2014 hard limits; never violate them.
118554
+ - STATUS.md \u2014 your run-state; keep it updated as you progress.
118555
+
118556
+ The QUEUE (the work items) is EXTERNAL \u2014 it is NOT one of these files. SKILL.md tells you WHERE to read the queue from and WHERE to write the results; read your work items from that source.
118557
+ ${slug ? `
118558
+ YOUR DURABLE MEMORY survives this fire, and ${dir} does not \u2014 this directory is deleted when you exit, so anything you want the NEXT fire to know must go into memory, not into a file here. Reach it with the taskMemory* MCP tools, always with taskSlug "${slug}" (also exported as AMETYST_TASK_SLUG). Nothing from it was injected into this fire beyond the materialized docs:
118559
+ - FIRST, before you start work, read what you need with taskMemoryGet: ${readDocSentence}one list per record kind you need \u2014 taskMemoryGet({ taskSlug: "${slug}", records: true, kind: "<kind>" }) \u2014 which returns the latest version per key, archived=false by default. If everything is empty this is your first fire \u2014 say so in your run record.
118560
+ - BEFORE YOU EXIT, on every path including a brake: taskMemoryAppend({ taskSlug: "${slug}", kind: "run", content: "<what you did, what you learned, what the next fire should pick up>" })${writeDocSentence}
118561
+ - Items with an identity (a PBI, a test, a merchant) are keyed records of a free kind: taskMemoryAppend({ taskSlug: "${slug}", kind: "<kind>", key: "<id>", content }) writes or versions one; taskMemoryArchive({ taskSlug: "${slug}", key: "<id>", note }) closes it.
118562
+ Memory is quota-bounded per workspace \u2014 an over-limit write is REJECTED and tells you the limit, never silently truncated. If a write is refused, shorten it and write again; do not skip the run record.
118563
+
118564
+ ${TASK_MEMORY_MODEL_SECTION}
118565
+ ` : ""}
118566
+ Execute the task until VISION is met or the queue is drained. For any step that costs money, use the Ametyst \`spend\` MCP tool (the on-chain policy enforces the budget) \u2014 do NOT invent another payment path.
118567
+
118568
+ When you finish cleanly (VISION met / queue drained), write "status: done" and "queue drained" into ${dir}/STATUS.md. If you must stop early (a brake/constraint was hit or an unrecoverable error occurred), write "brake: <reason>" into ${dir}/STATUS.md and exit. Never exceed the constraints.`;
118569
+ const args = [
118570
+ "-p",
118571
+ prompt,
118572
+ "--dangerously-skip-permissions",
118573
+ // The WRAPPER (`ametyst task run`, see runTask) owns shipping the task's learned
118574
+ // CONSTRAINTS back to Ametyst on exit. The headless agent must NEVER rewrite its own
118575
+ // stored task record, so deny the upsert tool even though `--dangerously-skip-permissions`
118576
+ // otherwise grants every tool. (STATUS/QUEUE stay local run-state and are never shipped.)
118577
+ // ⛔ THE LIST IS DERIVED, NOT TYPED, on both axes — a deny that names the wrong string is
118578
+ // indistinguishable from no deny at all:
118579
+ // - TOOL: `createTask` is what this server exposes now; `createLoop`, its retired alias,
118580
+ // is kept because the fire connects to whichever `ametyst serve` the host's MCP config
118581
+ // points at, which may still be an older build offering it. Denying only the retired
118582
+ // name is what the retirement would otherwise leave behind — an inert deny.
118583
+ // - ENTRY NAME: this line used to hardcode the `ametyst-staging` prefix, but a PROD build
118584
+ // registers as `ametyst` (AMETYST_MCP_NAME), so the guard was silently inert in prod.
118585
+ // AMETYST_MCP_NAMES is the repo's own list of every entry name this CLI family writes.
118586
+ // `--disallowedTools` is variadic and comma-or-space separated, so one arg carries them all.
118587
+ "--disallowedTools",
118588
+ AMETYST_MCP_NAMES.flatMap((n) => [`mcp__${n}__createTask`, `mcp__${n}__createLoop`]).join(","),
118589
+ "--add-dir",
118590
+ dir
118591
+ ];
118592
+ if (maxBudget !== void 0) {
118593
+ args.push("--max-budget-usd", String(maxBudget));
118490
118594
  }
118491
- if (declared) {
118492
- const res = await sdk.loops.memory.getDoc(apiKey, slug, key);
118493
- if (res.status === "ok") return { status: "ok", doc: res.doc, scope: declared };
118494
- return isDocMissing404(res) ? { status: "missing" } : { status: "nok", res };
118595
+ if (opts.sessionId) {
118596
+ args.push("--session-id", opts.sessionId);
118495
118597
  }
118496
- const own = await sdk.loops.memory.getDoc(apiKey, slug, key, { scope: "member" });
118497
- if (own.status === "ok") return { status: "ok", doc: own.doc, scope: "member" };
118498
- if (!isDocMissing404(own) && !isNotASeat404(own)) return { status: "nok", res: own };
118499
- const shared = await sdk.loops.memory.getDoc(apiKey, slug, key, { scope: "shared" });
118500
- if (shared.status === "ok") return { status: "ok", doc: shared.doc, scope: "shared" };
118501
- return isDocMissing404(shared) ? { status: "missing" } : { status: "nok", res: shared };
118502
- }
118503
- function renderRecord(r) {
118504
- const keyPart = r.key ? ` ${r.key}` : "";
118505
- const archivedPart = r.archived ? ` (archived${r.archivedAt ? ` ${r.archivedAt}` : ""})` : "";
118506
- if ("content" in r) return `\u2500\u2500 ${r.createdAt} [${r.kind}]${keyPart}${archivedPart}
118507
- ${r.content}`;
118508
- return `\u2500\u2500 ${r.createdAt} [${r.kind}]${keyPart}${archivedPart} ${r.summary}`;
118509
- }
118510
- function defaultDocKey(manifest) {
118511
- if (manifest === void 0) return void 0;
118512
- return manifest?.docs[0]?.key ?? null;
118598
+ args.push(
118599
+ // Eager-load MCP tools: with tool search enabled, MCP tools (including Ametyst's) are
118600
+ // deferred behind a ToolSearch step that smaller orchestrator models (e.g. Haiku) never
118601
+ // perform the run ends its turn without the tools (BUG-15). Eager loading is safe for
118602
+ // all models, so this is universal rather than model-gated.
118603
+ "--settings",
118604
+ '{"env":{"ENABLE_TOOL_SEARCH":"false"}}'
118605
+ );
118606
+ return {
118607
+ cmd: "claude",
118608
+ args,
118609
+ cwd: opts.cwd ?? process.cwd(),
118610
+ env: {
118611
+ ...deriveGitIdentityEnv(env, deps.readGitConfig ?? readGlobalGitConfig),
118612
+ // Task memory is addressed by SLUG, and the fire context carries no task
118613
+ // identity today which is why the taskMemory* tools take an explicit
118614
+ // `taskSlug`. Exporting it here is the convenience half: the prompt names
118615
+ // the slug literally, and this lets any shell step in the task reach the
118616
+ // same value without re-deriving it. Env only, never argv — the launch
118617
+ // argv is pinned byte-for-byte by launch.test.ts, and widening it would
118618
+ // be a change to the command rather than to the child's environment.
118619
+ // Exported under BOTH spellings for this version: a task SKILL written against an
118620
+ // older cli may still read `AMETYST_LOOP_SLUG`, and a fire must not lose its memory
118621
+ // address because the launcher was upgraded underneath it.
118622
+ ...slug ? { [TASK_ENV.SLUG]: slug, [LEGACY_TASK_ENV.SLUG]: slug } : {},
118623
+ // The user's `--input` text, env only for the same reason as the slug above. Only when
118624
+ // set: a run without arguments must not export an empty variable the SKILL could mistake
118625
+ // for "the user said nothing" when the truth is "nobody asked".
118626
+ ...launchInputBlock(opts.input) ? { [TASK_INPUT_ENV]: opts.input } : {}
118627
+ }
118628
+ };
118513
118629
  }
118514
- async function taskMemoryGetVerb(rawSlug, opts) {
118515
- const slug = rawSlug.trim();
118516
- const parsedLimitEarly = opts.limit === void 0 ? void 0 : Number(opts.limit);
118517
- if (opts.limit !== void 0 && (opts.limit.trim() === "" || !Number.isFinite(parsedLimitEarly))) {
118518
- throw new Error(`--limit must be a number, got ${JSON.stringify(opts.limit)}`);
118630
+ function resolveMaxBudgetUsd(opts, env = process.env) {
118631
+ if (opts.maxBudgetUsd !== void 0) return opts.maxBudgetUsd;
118632
+ const raw = readTaskEnv("MAX_BUDGET_USD", env);
118633
+ if (raw === void 0 || raw.trim() === "") return void 0;
118634
+ const n = Number(raw);
118635
+ return Number.isFinite(n) ? n : void 0;
118636
+ }
118637
+
118638
+ // src/mcp-server/task-run-mode.ts
118639
+ init_esm_shims();
118640
+ var ACCEPTED_RUN_MODES = ["in-chat", "headless"];
118641
+ var FRONTMATTER_SCAN_LIMIT = 8192;
118642
+ function normalizeRunMode(raw) {
118643
+ if (typeof raw !== "string") return void 0;
118644
+ const v = raw.trim().toLowerCase();
118645
+ if (v === "headless") return "headless";
118646
+ if (v === "in-chat" || v === "inchat" || v === "in_chat") return "in-chat";
118647
+ return void 0;
118648
+ }
118649
+ function describeProvided(raw) {
118650
+ if (typeof raw === "string") return raw.trim();
118651
+ try {
118652
+ return JSON.stringify(raw) ?? String(raw);
118653
+ } catch {
118654
+ return String(raw);
118519
118655
  }
118520
- const scope = parseScopeFlag(opts.scope, true);
118521
- const kind = parseKindFlag(opts.kind);
118522
- const key = parseKeyFlag(opts.key);
118523
- let archived;
118524
- if (opts.archived !== void 0) {
118525
- const a = opts.archived.trim().toLowerCase();
118526
- if (a === "true") archived = true;
118527
- else if (a === "false") archived = false;
118528
- else if (a === "all") archived = "all";
118529
- else throw new Error(`--archived must be false, true or all, got ${JSON.stringify(opts.archived)}`);
118656
+ }
118657
+ function classifyRunModeArgument(raw) {
118658
+ if (raw === void 0 || raw === null) return { kind: "omitted" };
118659
+ if (typeof raw === "string" && raw.trim() === "") return { kind: "omitted" };
118660
+ const mode2 = normalizeRunMode(raw);
118661
+ if (mode2) return { kind: "valid", mode: mode2 };
118662
+ return { kind: "invalid", provided: describeProvided(raw) };
118663
+ }
118664
+ function unquoteScalar(raw) {
118665
+ let v = raw.trim();
118666
+ const comment = v.match(/(?:^|\s)#.*$/);
118667
+ if (comment) v = v.slice(0, comment.index === 0 ? 0 : comment.index).trim();
118668
+ if (v.length >= 2 && (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'"))) {
118669
+ v = v.slice(1, -1).trim();
118530
118670
  }
118531
- const fieldsRaw = opts.fields?.trim();
118532
- if (fieldsRaw !== void 0 && fieldsRaw !== "full" && fieldsRaw !== "keys") {
118533
- throw new Error(`--fields must be full or keys, got ${JSON.stringify(opts.fields)}`);
118671
+ return v;
118672
+ }
118673
+ function parseDefaultRunMode(body) {
118674
+ if (typeof body !== "string" || !body) return void 0;
118675
+ const head = body.replace(/^\uFEFF/, "").slice(0, FRONTMATTER_SCAN_LIMIT);
118676
+ const opener = head.match(/^---[ \t]*\r?\n/);
118677
+ if (!opener) return void 0;
118678
+ const rest = head.slice(opener[0].length);
118679
+ const closer = rest.search(/^(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/m);
118680
+ if (closer < 0) return void 0;
118681
+ const block = rest.slice(0, closer);
118682
+ const hit = block.match(/^defaultRunMode[ \t]*:[ \t]*(.*)$/m);
118683
+ if (!hit) return void 0;
118684
+ return normalizeRunMode(unquoteScalar(hit[1] ?? ""));
118685
+ }
118686
+ function resolveRunMode(explicit, body) {
118687
+ const arg = classifyRunModeArgument(explicit);
118688
+ if (arg.kind === "invalid") {
118689
+ return { ok: false, error: "invalid_mode", provided: arg.provided, accepted: ACCEPTED_RUN_MODES };
118534
118690
  }
118535
- const fields = fieldsRaw === "keys" ? "keys" : void 0;
118536
- const { sdk, apiKey } = await getCliSdk();
118537
- const json = opts.json === true;
118538
- const parsedLimit = parsedLimitEarly;
118539
- const recordScope = scope === "shared" || scope === "member" ? scope : void 0;
118540
- const keyPrefix = opts.keyPrefix?.trim() || void 0;
118541
- const since = opts.since?.trim() || void 0;
118542
- const cursor = opts.cursor?.trim() || void 0;
118543
- const count = opts.count === true;
118544
- const hasRecordFilter = kind !== void 0 || keyPrefix !== void 0 || archived !== void 0 || since !== void 0 || fields !== void 0 || count || parsedLimit !== void 0 || cursor !== void 0;
118545
- const query = {
118546
- ...kind ? { kind } : {},
118547
- ...archived !== void 0 ? { archived } : {},
118548
- // THE KEY GOES IN WHENEVER A LISTING RUNS, not only under `--records`.
118549
- // It used to be gated on `opts.records === true`, which was correct while
118550
- // `--records` was the ONLY way to reach a listing. Once a filter also routes
118551
- // here, `--key X --kind pbi` fell between the two: the open-by-key branch was
118552
- // skipped (a filter was present) and the key never entered the query — so it
118553
- // silently listed pbi records and printed a DIFFERENT one. That is the same
118554
- // class of confidently-wrong answer this card exists to remove, reintroduced
118555
- // on another axis.
118556
- ...key !== void 0 && (opts.records === true || hasRecordFilter) ? { key } : {},
118557
- ...keyPrefix ? { keyPrefix } : {},
118558
- ...since ? { since } : {},
118559
- ...fields ? { fields } : {},
118560
- ...count ? { count: true } : {},
118561
- ...parsedLimit !== void 0 ? { limit: parsedLimit } : {},
118562
- ...cursor ? { cursor } : {},
118563
- ...recordScope ? { scope: recordScope } : {}
118691
+ if (arg.kind === "valid") return { ok: true, mode: arg.mode, source: "explicit" };
118692
+ const fromBody = parseDefaultRunMode(body);
118693
+ if (fromBody) return { ok: true, mode: fromBody, source: "frontmatter" };
118694
+ return { ok: true, mode: "in-chat", source: "default" };
118695
+ }
118696
+
118697
+ // src/tasks/estimate.ts
118698
+ init_esm_shims();
118699
+ function estimateBlastRadius(task) {
118700
+ const g = task.graphJson ?? {};
118701
+ const nodes = Array.isArray(g.nodes) ? g.nodes : [];
118702
+ const steps = nodes.length;
118703
+ const paidSteps = nodes.filter(
118704
+ (n) => n?.type === "spend" || n?.data?.paid === true || n?.paid === true
118705
+ ).length;
118706
+ const costHints = nodes.map((n) => Number(n?.data?.estCostEur ?? n?.estCostEur)).filter((x) => !Number.isNaN(x));
118707
+ const estCostEur = costHints.length ? costHints.reduce((a, b) => a + b, 0) : null;
118708
+ return { steps, paidSteps, estCostEur };
118709
+ }
118710
+
118711
+ // src/tasks/dashboard-template.ts
118712
+ init_esm_shims();
118713
+ var DEFAULT_DASHBOARD_FILES = [
118714
+ "VISION.md",
118715
+ "CONSTRAINTS.md",
118716
+ "QUEUE.md",
118717
+ "STATUS.md",
118718
+ "README.md",
118719
+ "rounds.jsonl"
118720
+ ];
118721
+ function parseProcessSpec(manifest) {
118722
+ if (typeof manifest !== "string" || !manifest.trim()) return null;
118723
+ let parsed;
118724
+ try {
118725
+ parsed = JSON.parse(manifest);
118726
+ } catch {
118727
+ return null;
118728
+ }
118729
+ const raw = parsed?.processSpec;
118730
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
118731
+ const spec = raw;
118732
+ const stages = (Array.isArray(spec.stages) ? spec.stages : []).map((s) => {
118733
+ if (typeof s === "string" && s.trim()) return { label: s.trim() };
118734
+ if (s && typeof s === "object" && !Array.isArray(s)) {
118735
+ const o = s;
118736
+ if (typeof o.label === "string" && o.label.trim()) {
118737
+ return {
118738
+ label: o.label.trim(),
118739
+ ...typeof o.id === "string" && o.id.trim() ? { id: o.id.trim() } : {},
118740
+ ...typeof o.detail === "string" && o.detail.trim() ? { detail: o.detail.trim() } : {}
118741
+ };
118742
+ }
118743
+ }
118744
+ return null;
118745
+ }).filter((s) => s !== null);
118746
+ const strings = (v) => (Array.isArray(v) ? v : []).filter((x) => typeof x === "string" && x.trim() !== "");
118747
+ const out = {
118748
+ stages,
118749
+ inputs: strings(spec.inputs),
118750
+ outputs: strings(spec.outputs),
118751
+ ...typeof spec.title === "string" && spec.title.trim() ? { title: spec.title.trim() } : {}
118564
118752
  };
118565
- if (opts.usage === true) {
118566
- const res = await sdk.loops.memory.usage(apiKey, slug);
118567
- if (res.status !== "ok") fail(res, "read memory usage");
118568
- emit(json, { taskSlug: slug, usage: res.usage }, () => JSON.stringify(res.usage, null, 2));
118569
- return;
118753
+ return stages.length || out.inputs.length || out.outputs.length ? out : null;
118754
+ }
118755
+ function defaultDashboardManifest() {
118756
+ return JSON.stringify({ files: [...DEFAULT_DASHBOARD_FILES] });
118757
+ }
118758
+ function embedJson(value2) {
118759
+ return JSON.stringify(value2).replace(/</g, "\\u003c");
118760
+ }
118761
+ function escapeHtml(s) {
118762
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
118763
+ }
118764
+ function renderTaskDashboardTemplate(args) {
118765
+ const title = escapeHtml(args.processSpec?.title ?? args.slug);
118766
+ const seed = embedJson({
118767
+ slug: args.slug,
118768
+ descriptionShort: args.descriptionShort ?? "",
118769
+ processSpec: args.processSpec ?? null
118770
+ });
118771
+ return `<!doctype html>
118772
+ <html lang="en">
118773
+ <head>
118774
+ <meta charset="utf-8">
118775
+ <meta name="viewport" content="width=device-width, initial-scale=1">
118776
+ <title>${title} \u2014 task dashboard</title>
118777
+ <style>
118778
+ :root { --bg:#f7f7fb; --panel:#fff; --border:#e3e3ee; --muted:#6b6b80; --accent:#5b5bd6; --ok:#1a7f37; --bad:#b42318; }
118779
+ * { box-sizing: border-box; }
118780
+ body { margin:0; font:14px/1.5 -apple-system, "Segoe UI", Roboto, sans-serif; background:var(--bg); color:#1d1d2b; padding:24px; }
118781
+ h1 { font-size:20px; margin:0 0 4px; }
118782
+ h2 { font-size:14px; margin:24px 0 8px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
118783
+ .sub { color:var(--muted); margin:0 0 16px; }
118784
+ .panel { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px; }
118785
+ .map { display:flex; flex-wrap:wrap; align-items:stretch; gap:8px; }
118786
+ .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; }
118787
+ .stage .label { font-weight:600; }
118788
+ .stage .detail { color:var(--muted); font-size:12px; margin-top:2px; }
118789
+ .arrow { align-self:center; color:var(--muted); }
118790
+ .io { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
118791
+ ul { margin:6px 0 0; padding-left:18px; }
118792
+ table { width:100%; border-collapse:collapse; background:var(--panel); border:1px solid var(--border); border-radius:12px; overflow:hidden; }
118793
+ th, td { text-align:left; padding:8px 12px; border-top:1px solid var(--border); font-size:13px; vertical-align:top; }
118794
+ thead th { border-top:none; background:var(--bg); color:var(--muted); font-weight:600; }
118795
+ .ok { color:var(--ok); } .bad { color:var(--bad); }
118796
+ details { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:10px 14px; margin-bottom:8px; }
118797
+ summary { cursor:pointer; font-weight:600; }
118798
+ pre { overflow-x:auto; font-size:12px; background:var(--bg); border-radius:8px; padding:10px; }
118799
+ .empty { color:var(--muted); font-style:italic; }
118800
+ #live { font-size:12px; color:var(--muted); float:right; }
118801
+ </style>
118802
+ </head>
118803
+ <body>
118804
+ <span id="live">loading\u2026</span>
118805
+ <h1 id="title"></h1>
118806
+ <p class="sub" id="desc"></p>
118807
+
118808
+ <h2>Process</h2>
118809
+ <div id="map" class="map panel"></div>
118810
+
118811
+ <h2>Inputs &amp; outputs</h2>
118812
+ <div class="io">
118813
+ <div class="panel"><strong>Inputs</strong><ul id="inputs"></ul></div>
118814
+ <div class="panel"><strong>Outputs</strong><ul id="outputs"></ul></div>
118815
+ </div>
118816
+
118817
+ <h2>Rounds</h2>
118818
+ <div id="rounds"></div>
118819
+
118820
+ <h2>Files</h2>
118821
+ <div id="files"></div>
118822
+
118823
+ <script type="application/json" id="loop-seed">${seed}</script>
118824
+ <script>
118825
+ (function () {
118826
+ "use strict";
118827
+ var seed = JSON.parse(document.getElementById("loop-seed").textContent);
118828
+ document.getElementById("title").textContent = (seed.processSpec && seed.processSpec.title) || seed.slug;
118829
+ document.getElementById("desc").textContent = seed.descriptionShort || "";
118830
+ document.title = seed.slug + " \u2014 task dashboard";
118831
+
118832
+ function el(tag, cls, text) {
118833
+ var e = document.createElement(tag);
118834
+ if (cls) e.className = cls;
118835
+ if (text !== undefined) e.textContent = text;
118836
+ return e;
118837
+ }
118838
+
118839
+ // \u2500\u2500 Process map + IO from the embedded spec (static \u2014 rendered once). \u2500\u2500
118840
+ var map = document.getElementById("map");
118841
+ var spec = seed.processSpec;
118842
+ if (spec && spec.stages && spec.stages.length) {
118843
+ spec.stages.forEach(function (s, i) {
118844
+ if (i > 0) map.appendChild(el("div", "arrow", "\\u2192"));
118845
+ var box = el("div", "stage");
118846
+ box.appendChild(el("div", "label", s.label));
118847
+ if (s.detail) box.appendChild(el("div", "detail", s.detail));
118848
+ map.appendChild(box);
118849
+ });
118850
+ } else {
118851
+ map.appendChild(el("span", "empty", "No process spec on this task \\u2014 live files and rounds below."));
118570
118852
  }
118571
- const explicitDoc = opts.doc?.trim();
118572
- if (explicitDoc) {
118573
- const manifest2 = scope === void 0 || scope === "all" ? await readTaskManifest(sdk, apiKey, slug) : void 0;
118574
- const declared = declaredDocScope(manifest2 ?? null, explicitDoc);
118575
- const read = await readDocResolved(sdk, apiKey, slug, explicitDoc, scope, declared);
118576
- if (read.status === "ok") {
118577
- emit(json, { taskSlug: slug, doc: read.doc, scope: read.scope }, () => read.doc.content);
118578
- return;
118579
- }
118580
- if (read.status === "missing") {
118581
- emit(json, { taskSlug: slug, doc: null, firstRun: true }, () => `No "${explicitDoc}" document yet for ${slug} \u2014 nothing has written one.`);
118582
- return;
118583
- }
118584
- fail(read.res, `read the "${explicitDoc}" document`);
118853
+ function fillList(id, items) {
118854
+ var ul = document.getElementById(id);
118855
+ ul.textContent = "";
118856
+ if (!items || !items.length) { ul.appendChild(el("li", "empty", "\\u2014")); return; }
118857
+ items.forEach(function (x) { ul.appendChild(el("li", null, x)); });
118585
118858
  }
118586
- if (key !== void 0 && opts.records !== true && !hasRecordFilter) {
118587
- const history = opts.history === true;
118588
- const res = await sdk.loops.memory.getRecordByKey(apiKey, slug, key, history ? { history: true } : void 0);
118589
- if (res.status !== "ok") fail(res, `open the item "${key}"`);
118590
- const versions = history ? res.versions ?? [] : void 0;
118591
- emit(json, { taskSlug: slug, key, record: res.record, ...versions ? { versions } : {} }, () => versions ? [`\u2500\u2500 ${key}: ${versions.length} version(s), oldest first`, ...versions.map(renderRecord)].join("\n\n") : renderRecord(res.record));
118592
- return;
118859
+ fillList("inputs", spec && spec.inputs);
118860
+ fillList("outputs", spec && spec.outputs);
118861
+
118862
+ // \u2500\u2500 Live data: rounds + files, refreshed from GET /data. \u2500\u2500
118863
+ function parseJsonl(text) {
118864
+ var rows = [];
118865
+ (text || "").split("\\n").forEach(function (line) {
118866
+ line = line.trim();
118867
+ if (!line) return;
118868
+ try { rows.push(JSON.parse(line)); } catch (e) { /* skip malformed line */ }
118869
+ });
118870
+ return rows;
118593
118871
  }
118594
- if (opts.records === true || hasRecordFilter) {
118595
- const res = await sdk.loops.memory.listRecords(apiKey, slug, query);
118596
- if (res.status !== "ok") fail(res, "read the records");
118597
- if (count) {
118598
- emit(json, { taskSlug: slug, count: res.count ?? 0 }, () => String(res.count ?? 0));
118872
+
118873
+ function renderRounds(fires, rounds) {
118874
+ var host = document.getElementById("rounds");
118875
+ host.textContent = "";
118876
+ if (!fires.length && !rounds.length) {
118877
+ var p = el("div", "panel"); p.appendChild(el("span", "empty", "No rounds yet \\u2014 this fills in as the task runs."));
118878
+ host.appendChild(p);
118599
118879
  return;
118600
118880
  }
118601
- const items2 = res.items ?? [];
118602
- const nextCursor = res.nextCursor ?? null;
118603
- emit(json, { taskSlug: slug, records: items2, nextCursor, firstRun: items2.length === 0 && !cursor }, () => items2.length === 0 ? `No records for ${slug}${kind ? ` of kind ${kind}` : ""}${archived === true ? " (archived)" : ""}.` : [
118604
- ...items2.map(renderRecord),
118605
- ...nextCursor ? [`\u2500\u2500 more: --cursor ${nextCursor}`] : []
118606
- ].join("\n\n"));
118607
- return;
118608
- }
118609
- const manifest = scope === void 0 || scope === "all" ? await readTaskManifest(sdk, apiKey, slug) : void 0;
118610
- const manifestUnreadable = manifest === void 0 && (scope === void 0 || scope === "all");
118611
- const docKey = defaultDocKey(manifest) ?? void 0;
118612
- const [doc, records] = await Promise.all([
118613
- docKey === void 0 ? Promise.resolve({ status: "missing" }) : readDocResolved(sdk, apiKey, slug, docKey, scope, declaredDocScope(manifest ?? null, docKey)),
118614
- sdk.loops.memory.listRecords(apiKey, slug, query)
118615
- ]);
118616
- if (doc.status === "nok") fail(doc.res, `read the "${docKey ?? ""}" document`);
118617
- if (records.status !== "ok") fail(records, "read the records");
118618
- const defaultDoc = doc.status === "ok" ? doc.doc : null;
118619
- const items = records.items ?? [];
118620
- const firstRun = defaultDoc === null && items.length === 0 && !manifestUnreadable;
118621
- emit(
118622
- json,
118623
- {
118624
- taskSlug: slug,
118625
- doc: defaultDoc,
118626
- docKey: docKey ?? null,
118627
- manifestUnreadable,
118628
- records: items,
118629
- nextCursor: records.nextCursor ?? null,
118630
- firstRun
118631
- },
118632
- () => manifestUnreadable && items.length === 0 ? `Could not read ${slug}'s manifest, so no default document was looked up \u2014 this is NOT "no memory yet". Retry, or name a document with --doc.` : firstRun ? `No memory yet for ${slug} \u2014 nothing has written any.` : [
118633
- // No doc line at all when the task declares none — an empty `── (none)`
118634
- // header invites the reader to look for a doc that does not exist.
118635
- ...docKey === void 0 ? [] : [defaultDoc ? `\u2500\u2500 ${docKey}
118636
- ${defaultDoc.content}` : `\u2500\u2500 ${docKey}
118637
- (none)`],
118638
- `\u2500\u2500 ${items.length} record(s)`,
118639
- ...items.map(renderRecord)
118640
- ].join("\n\n")
118641
- );
118642
- }
118643
- async function taskMemoryAppendVerb(rawSlug, opts) {
118644
- const slug = rawSlug.trim();
118645
- if (opts.content !== void 0 && opts.file !== void 0) {
118646
- throw new Error("pass --content or --file, not both \u2014 they are two spellings of one payload");
118647
- }
118648
- const docKey = opts.doc?.trim();
118649
- if (docKey && (opts.kind !== void 0 || opts.key !== void 0 || opts.archived || opts.note !== void 0)) {
118650
- throw new Error("pass --doc <key> or --kind <kind> [--key --archived --note], not both \u2014 a document is upserted, a record is appended");
118651
- }
118652
- const kind = opts.kind?.trim() || "run";
118653
- if (!docKey) parseKindFlag(kind);
118654
- const key = parseKeyFlag(opts.key);
118655
- const archived = opts.archived === true;
118656
- const note = opts.note?.trim() || void 0;
118657
- const scope = parseScopeFlag(opts.scope, false);
118658
- const content = opts.file !== void 0 ? readFileSync10(opts.file, "utf-8") : opts.content ?? "";
118659
- if (!content) {
118660
- throw new Error("nothing to store: pass --content <text> or --file <path>");
118881
+ var table = document.createElement("table");
118882
+ var thead = document.createElement("thead");
118883
+ var hr = document.createElement("tr");
118884
+ ["when", "duration", "turns", "tokens", "exit", "round detail (rounds.jsonl)"].forEach(function (h) {
118885
+ hr.appendChild(el("th", null, h));
118886
+ });
118887
+ thead.appendChild(hr); table.appendChild(thead);
118888
+ var tbody = document.createElement("tbody");
118889
+ var n = Math.max(fires.length, rounds.length);
118890
+ for (var i = n - 1; i >= 0; i--) { // newest first
118891
+ var f = fires[i] || {};
118892
+ var r = rounds[i];
118893
+ var tr = document.createElement("tr");
118894
+ tr.appendChild(el("td", null, f.ts || (r && r.ts) || "\\u2014"));
118895
+ tr.appendChild(el("td", null, f.duration_s != null ? f.duration_s + "s" : "\\u2014"));
118896
+ tr.appendChild(el("td", null, f.turns != null ? String(f.turns) : "\\u2014"));
118897
+ tr.appendChild(el("td", null, f.tokens_total != null ? Number(f.tokens_total).toLocaleString() : "\\u2014"));
118898
+ tr.appendChild(el("td", f.exit === 0 ? "ok" : f.exit != null ? "bad" : null, f.exit != null ? String(f.exit) : "\\u2014"));
118899
+ var detail = el("td");
118900
+ if (f.launch_failed) { detail.textContent = "launch failed \\u2014 " + (f.reason || "unknown reason"); }
118901
+ else if (r) { var pre = document.createElement("pre"); pre.textContent = JSON.stringify(r, null, 1); detail.appendChild(pre); }
118902
+ else detail.textContent = "\\u2014";
118903
+ tr.appendChild(detail);
118904
+ tbody.appendChild(tr);
118905
+ }
118906
+ table.appendChild(tbody);
118907
+ host.appendChild(table);
118661
118908
  }
118662
- if (!docKey) {
118663
- const early = checkRecordWrite({ kind, key, content, archived, declaredKinds: void 0 });
118664
- if (!early.ok) throw new Error(`${early.code}: ${early.message}`);
118909
+
118910
+ function renderFiles(files) {
118911
+ var host = document.getElementById("files");
118912
+ host.textContent = "";
118913
+ var names = Object.keys(files || {}).filter(function (n) { return n !== "rounds.jsonl"; });
118914
+ if (!names.length) { var p = el("div", "panel"); p.appendChild(el("span", "empty", "No files surfaced by the manifest.")); host.appendChild(p); return; }
118915
+ names.forEach(function (name) {
118916
+ var d = document.createElement("details");
118917
+ d.appendChild(el("summary", null, name));
118918
+ var pre = document.createElement("pre");
118919
+ pre.textContent = files[name];
118920
+ d.appendChild(pre);
118921
+ host.appendChild(d);
118922
+ });
118665
118923
  }
118666
- const { sdk, apiKey } = await getCliSdk();
118667
- const manifest = scope && docKey ? void 0 : await readTaskManifest(sdk, apiKey, slug);
118668
- const warnUndeclared = (what) => {
118669
- if (scope || manifest === void 0) return;
118670
- console.warn(
118671
- `\u26A0\uFE0F ${what} is not in ${slug}'s stateDocs manifest \u2014 written to the seat (member) row. Pass --scope shared|member to choose, or declare it in the manifest.`
118672
- );
118673
- };
118674
- if (docKey) {
118675
- if (!declaredDocScope(manifest ?? null, docKey)) warnUndeclared(`doc "${docKey}"`);
118676
- const res2 = await sdk.loops.memory.putDoc(apiKey, slug, docKey, { content, ...scope ? { scope } : {} });
118677
- if (res2.status !== "ok") fail(res2, `save the "${docKey}" state document`);
118678
- emit(opts.json === true, { taskSlug: slug, docKey, scope: scope ?? null, bytes: Buffer.byteLength(content) }, () => `\u2705 ${slug}: "${docKey}" document saved${scope ? ` (${scope})` : ""} (${Buffer.byteLength(content)} bytes).`);
118679
- return;
118924
+
118925
+ function refresh() {
118926
+ fetch("/data").then(function (res) { return res.json(); }).then(function (data) {
118927
+ var fires = parseJsonl(data.state && data.state["fires.jsonl"]);
118928
+ var rounds = parseJsonl(data.files && data.files["rounds.jsonl"]);
118929
+ renderRounds(fires, rounds);
118930
+ renderFiles(data.files);
118931
+ document.getElementById("live").textContent = "updated " + new Date().toLocaleTimeString();
118932
+ }).catch(function () {
118933
+ document.getElementById("live").textContent = "server stopped";
118934
+ });
118680
118935
  }
118681
- const check = checkRecordWrite({ kind, key, content, archived, declaredKinds: declaredRecordKinds(manifest) });
118682
- if (!check.ok) throw new Error(`${check.code}: ${check.message}`);
118683
- if (!declaredRecordScope(manifest ?? null, kind)) warnUndeclared(`record kind "${kind}"`);
118684
- const res = await sdk.loops.memory.appendRecord(apiKey, slug, {
118685
- content,
118686
- kind,
118687
- ...key !== void 0 ? { key } : {},
118688
- ...archived ? { archived: true } : {},
118689
- ...note !== void 0 ? { archiveNote: note } : {},
118690
- ...scope ? { scope } : {}
118691
- });
118692
- if (res.status !== "ok") fail(res, "append the record");
118693
- const noun = check.shape === "item" ? `${kind} item "${key}"${archived ? " (archived)" : ""}` : `${kind} record`;
118694
- emit(
118695
- opts.json === true,
118696
- { taskSlug: slug, kind, ...key !== void 0 ? { key } : {}, shape: check.shape, archived, scope: scope ?? null, bytes: Buffer.byteLength(content) },
118697
- () => `\u2705 ${slug}: ${noun} ${check.shape === "item" ? "versioned" : "appended"}${scope ? ` (${scope})` : ""} (${Buffer.byteLength(content)} bytes).`
118698
- );
118936
+ refresh();
118937
+ setInterval(refresh, 5000);
118938
+ })();
118939
+ </script>
118940
+ </body>
118941
+ </html>
118942
+ `;
118699
118943
  }
118700
- async function taskMemoryArchiveVerb(rawSlug, opts) {
118701
- const slug = rawSlug.trim();
118702
- const key = parseKeyFlag(opts.key);
118703
- if (key === void 0) throw new Error("which item? pass --key <k> \u2014 the item's key as it was written");
118704
- const note = opts.note?.trim() || void 0;
118705
- const { sdk, apiKey } = await getCliSdk();
118706
- const res = await sdk.loops.memory.archiveRecord(apiKey, slug, key, note ? { note } : void 0);
118707
- if (res.status !== "ok") {
118708
- if (res.code === 409) throw new Error(`"${key}" is already archived \u2014 its latest version is closed (409): ${res.error ?? ""}`.trim());
118709
- fail(res, `archive "${key}"`);
118944
+ function injectDefaultDashboard(task) {
118945
+ const hasHtml = typeof task.dashboardHtml === "string" && task.dashboardHtml.trim() !== "";
118946
+ if (hasHtml) return;
118947
+ if (task.dashboardManifest && typeof task.dashboardManifest === "object") {
118948
+ try {
118949
+ task.dashboardManifest = JSON.stringify(task.dashboardManifest);
118950
+ } catch {
118951
+ }
118710
118952
  }
118711
- emit(
118712
- opts.json === true,
118713
- { taskSlug: slug, key, kind: res.record.kind, archivedAt: res.record.archivedAt, archiveNote: res.record.archiveNote ?? null },
118714
- () => `\u2705 ${slug}: "${key}" archived${note ? ` (${note})` : ""} at ${res.record.archivedAt}. History intact.`
118715
- );
118953
+ const manifest = typeof task.dashboardManifest === "string" ? task.dashboardManifest : void 0;
118954
+ task.dashboardHtml = renderTaskDashboardTemplate({
118955
+ slug: typeof task.slug === "string" ? task.slug : "task",
118956
+ descriptionShort: typeof task.descriptionShort === "string" ? task.descriptionShort : void 0,
118957
+ processSpec: parseProcessSpec(manifest)
118958
+ });
118959
+ if (!manifest || !manifest.trim()) task.dashboardManifest = defaultDashboardManifest();
118716
118960
  }
118717
118961
 
118718
118962
  // src/mcp-server/memory-scope.ts
@@ -118741,51 +118985,41 @@ init_esm_shims();
118741
118985
 
118742
118986
  // src/tasks/task-run-section.ts
118743
118987
  init_esm_shims();
118744
- var LOOP_IN_CHAT_EXCLUDED = "**NOT AVAILABLE for a loop** \u2014 a loop must fire as a fresh headless process under the CLI wrapper that owns the fire lock, the heartbeat, `fires.jsonl` accounting, the dashboard and the CONSTRAINTS ship-back, all of which an in-chat run bypasses.";
118745
- function cadenceWarning(kind) {
118746
- return kind === "loop" ? "Never pick a default silently; a loop armed on a schedule the user did not choose keeps costing money and merging code on its own." : "Never pick a default silently; a compound armed on a schedule the user did not choose keeps costing money on its own.";
118747
- }
118748
- function defaultModeHighlight(kind, mode2) {
118988
+ var CADENCE_WARNING = "Never pick a default silently; a task armed on a schedule the user did not choose keeps costing money on its own.";
118989
+ function defaultModeHighlight(mode2) {
118749
118990
  if (!mode2) return "";
118750
- if (kind === "loop" && mode2 === "in-chat") {
118751
- return `
118752
- The author's \`defaultRunMode: in-chat\` cannot apply to a loop (see option 1) \u2014 ask anyway, and mention that its stated default is unavailable.
118753
- `;
118754
- }
118755
118991
  return `
118756
118992
  The author suggests \`${mode2}\` (\`defaultRunMode:\` in the task's frontmatter). Highlight it when you ask \u2014 it is a suggestion, never a reason to skip the question.
118757
118993
  `;
118758
118994
  }
118759
- function taskRunSection(slug, kind, opts = {}) {
118760
- const inChat = kind === "loop" ? LOOP_IN_CHAT_EXCLUDED : `call the \`runTask\` MCP tool with task="${slug}" and mode="in-chat" (present the match and get confirmation first, per the getTask stop-gate convention).`;
118995
+ function taskRunSection(slug, opts = {}) {
118761
118996
  return `Before running it, ASK the user HOW to run it, and wait for an answer \u2014 always ask, never pick one silently:
118762
118997
 
118763
- 1. **one-time, in this chat, followed here** \u2014 ${inChat}
118998
+ 1. **one-time, in this chat, followed here** \u2014 call the \`runTask\` MCP tool with task="${slug}" and mode="in-chat" (present the match and get confirmation first, per the getTask stop-gate convention).
118764
118999
  2. **one-time, headless in its own process** \u2014 \`ametyst task run ${slug}\`
118765
- 3. **scheduled, recurring** \u2014 \`ametyst task schedule ${slug} --every <dur>\` (e.g. \`--every 30m\`, \`--every 1h\`), or \`ametyst task schedule ${slug} --at HH:MM\` for a fixed daily time. ASK which cadence. ${cadenceWarning(kind)}
118766
- ${defaultModeHighlight(kind, opts.defaultRunMode)}
119000
+ 3. **scheduled, recurring** \u2014 \`ametyst task schedule ${slug} --every <dur>\` (e.g. \`--every 30m\`, \`--every 1h\`), or \`ametyst task schedule ${slug} --at HH:MM\` for a fixed daily time. ASK which cadence. ${CADENCE_WARNING}
119001
+ ${defaultModeHighlight(opts.defaultRunMode)}
118767
119002
  Stop or inspect a schedule: \`ametyst task unschedule ${slug}\` \xB7 \`ametyst task schedules\`.
118768
119003
  To READ it without executing: \`ametyst task show ${slug}\`.
118769
119004
  `;
118770
119005
  }
118771
119006
 
118772
119007
  // src/mcp-server/prompt-descriptors.ts
118773
- function promptName(kind, slug) {
118774
- return `${kind}__${slug}`;
119008
+ function promptName(slug) {
119009
+ return `task__${slug}`;
118775
119010
  }
118776
- function buildPromptEntry(kind, summary) {
119011
+ function buildPromptEntry(summary) {
118777
119012
  const cat = summary.category ? ` [${summary.category}]` : "";
118778
- const label2 = kind === "compound" ? "Compound skill" : "Loop";
118779
119013
  return {
118780
- name: promptName(kind, summary.slug),
118781
- description: `${label2}: ${summary.descriptionShort ?? summary.slug}${cat}`
119014
+ name: promptName(summary.slug),
119015
+ description: `Task: ${summary.descriptionShort ?? summary.slug}${cat}`
118782
119016
  };
118783
119017
  }
118784
- function buildPromptContent(kind, full) {
118785
- const text = `This is the ${kind} "${full.slug}".
119018
+ function buildPromptContent(full) {
119019
+ const text = `This is the task "${full.slug}".
118786
119020
 
118787
- ${taskRunSection(full.slug, kind, { defaultRunMode: parseDefaultRunMode(full.markdownBody) })}
118788
- --- ${kind} body ---
119021
+ ${taskRunSection(full.slug, { defaultRunMode: parseDefaultRunMode(full.markdownBody) })}
119022
+ --- task body ---
118789
119023
  ${full.markdownBody ?? ""}`;
118790
119024
  return { messages: [{ role: "user", content: { type: "text", text } }] };
118791
119025
  }
@@ -118813,9 +119047,9 @@ function resolveBodyFromInlineOrFile(inline, filePath) {
118813
119047
  return void 0;
118814
119048
  }
118815
119049
 
118816
- // src/mcp-server/loop-upsert-summary.ts
119050
+ // src/mcp-server/task-upsert-summary.ts
118817
119051
  init_esm_shims();
118818
- var LOOP_CONTENT_FIELDS = [
119052
+ var TASK_CONTENT_FIELDS = [
118819
119053
  "markdownBody",
118820
119054
  "visionMd",
118821
119055
  "constraintsMd",
@@ -118918,7 +119152,7 @@ var BRANCH_MATCHERS = [
118918
119152
  ];
118919
119153
  function allLinesOf(content) {
118920
119154
  const out = [];
118921
- for (const f of LOOP_CONTENT_FIELDS) {
119155
+ for (const f of TASK_CONTENT_FIELDS) {
118922
119156
  const v = content[f];
118923
119157
  if (typeof v === "string" && v) out.push(...v.split(/\r?\n/));
118924
119158
  }
@@ -118927,7 +119161,7 @@ function allLinesOf(content) {
118927
119161
  function mergeEffective(sent, previous) {
118928
119162
  const base2 = previous && previous.available ? { ...previous.content } : {};
118929
119163
  const provenance = [];
118930
- const keys = [...LOOP_CONTENT_FIELDS, "descriptionShort"];
119164
+ const keys = [...TASK_CONTENT_FIELDS, "descriptionShort"];
118931
119165
  const content = { ...base2 };
118932
119166
  for (const k of keys) {
118933
119167
  const sentVal = sent[k];
@@ -118990,7 +119224,7 @@ function buildReceipt(input) {
118990
119224
  if (prevVal === void 0) return "changed";
118991
119225
  return String(sentVal) === String(prevVal) ? "unchanged" : "changed";
118992
119226
  };
118993
- for (const f of LOOP_CONTENT_FIELDS) {
119227
+ for (const f of TASK_CONTENT_FIELDS) {
118994
119228
  const sentVal = sent[f];
118995
119229
  const wasSent = typeof sentVal === "string" && sentVal.length > 0;
118996
119230
  if (!wasSent && mode2 === "created") continue;
@@ -119030,7 +119264,7 @@ function deriveChanges(input, receipt) {
119030
119264
  if (mode2 === "created") {
119031
119265
  return {
119032
119266
  title: "WHAT CHANGED",
119033
- lines: ["new loop \u2014 there is no previous version to compare against"]
119267
+ lines: ["new task \u2014 there is no previous version to compare against"]
119034
119268
  };
119035
119269
  }
119036
119270
  if (!previous || !previous.available) {
@@ -119063,7 +119297,7 @@ function deriveChanges(input, receipt) {
119063
119297
  function renderSections(sections2) {
119064
119298
  return sections2.map((s) => [s.title, ...s.lines.map((l) => ` - ${l}`)].join("\n")).join("\n\n");
119065
119299
  }
119066
- function buildLoopUpsertSummary(input) {
119300
+ function buildTaskUpsertSummary(input) {
119067
119301
  const { content, provenance } = mergeEffective(input.sent, input.previous);
119068
119302
  const sections2 = deriveNarrative(content);
119069
119303
  const receipt = buildReceipt(input);
@@ -119086,9 +119320,9 @@ var IDENTITY_KEYS = [
119086
119320
  "createdAt",
119087
119321
  "updatedAt"
119088
119322
  ];
119089
- function projectLoopIdentity(loop2) {
119090
- if (!loop2 || typeof loop2 !== "object") return {};
119091
- const src = loop2;
119323
+ function projectTaskIdentity(task) {
119324
+ if (!task || typeof task !== "object") return {};
119325
+ const src = task;
119092
119326
  const out = {};
119093
119327
  for (const k of IDENTITY_KEYS) {
119094
119328
  const v = src[k];
@@ -119871,7 +120105,7 @@ init_paths();
119871
120105
  import * as nodeFs4 from "fs";
119872
120106
  import { execFileSync as execFileSync2 } from "child_process";
119873
120107
  import { homedir as homedir8 } from "os";
119874
- import { basename as basename4, join as join14 } from "path";
120108
+ import { basename as basename5, join as join14 } from "path";
119875
120109
  var DELEGATED_CHILD_ENV_VAR = "AMETYST_DELEGATED";
119876
120110
  var SPEND_GRANT_TOKEN_ENV_VAR = "AMETYST_DELEGATE_SPEND_TOKEN";
119877
120111
  var SPEND_KILL_SWITCH_ENV_VAR = "AMETYST_DELEGATE_NO_SPEND";
@@ -119913,7 +120147,7 @@ function defaultProcessTable() {
119913
120147
  function isOurOpencodeBinary(argv0, home = homedir8()) {
119914
120148
  if (!argv0) return false;
119915
120149
  if (argv0 === opencodeBinaryPath(home)) return true;
119916
- return /^opencode-\d/.test(basename4(argv0));
120150
+ return /^opencode-\d/.test(basename5(argv0));
119917
120151
  }
119918
120152
  var ancestryMemo;
119919
120153
  function classifyAncestry(deps = {}) {
@@ -121710,7 +121944,7 @@ function registerDelegateTools(deps) {
121710
121944
  return { registered: [...defs.keys()], parentDeathHook: isProductionStore, mcpServers };
121711
121945
  }
121712
121946
 
121713
- // src/mcp-server/write-compound-body.ts
121947
+ // src/mcp-server/write-task-body.ts
121714
121948
  init_esm_shims();
121715
121949
  import { tmpdir as tmpdir2 } from "os";
121716
121950
  import { join as joinPath } from "path";
@@ -122195,8 +122429,6 @@ var allowlistCache = null;
122195
122429
  var capabilityIndexCache = null;
122196
122430
  var firstMcpSessionSeen = false;
122197
122431
  var firstSessionGuideEmitted = false;
122198
- var compoundIndexCache = null;
122199
- var loopIndexCache = null;
122200
122432
  var taskIndexCache = null;
122201
122433
  var cardCounts = null;
122202
122434
  var lastNudgeAtCall = null;
@@ -122328,8 +122560,6 @@ function dropIdentityScopedCaches() {
122328
122560
  transactionsCache = null;
122329
122561
  allowlistCache = null;
122330
122562
  capabilityIndexCache = null;
122331
- compoundIndexCache = null;
122332
- loopIndexCache = null;
122333
122563
  taskIndexCache = null;
122334
122564
  cardCounts = null;
122335
122565
  servicesDiscovered = null;
@@ -122877,7 +123107,7 @@ ${lines}${footer}${buildPersonaProposalSection(index2.persona)}`;
122877
123107
  }
122878
123108
  var INDEX_MAX_CATEGORIES = 12;
122879
123109
  var INDEX_MAX_RECENT = 5;
122880
- function buildCompoundLoopIndexSection(items, noun) {
123110
+ function buildTaskIndexSection(items, noun) {
122881
123111
  if (!items || items.length === 0) return "";
122882
123112
  const counts = /* @__PURE__ */ new Map();
122883
123113
  for (const it of items) {
@@ -122901,9 +123131,9 @@ Your workspace has ${total} ${noun}${total === 1 ? "" : "s"} across ${catCount}
122901
123131
  ${catLines}${moreCats}${recentLine}
122902
123132
  Call with an intent or category to pull the full body of the one you want.`;
122903
123133
  }
122904
- var GET_TASK_BASE_DESCRIPTION = "Search the workspace's TASKS by intent and RETURN the matching task(s) as markdown. 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 searches both. Like getAllowlist, it does NOT execute anything \u2014 present the matched task(s) to the user and get their explicit confirmation before running one with `runTask`. Input: intent (natural-language description of what the user wants), optional category. On an unambiguous single match it also returns the raw `markdownBody`, spilled to a file path when large, so you can edit the task cross-session without hand-stripping the display prefix \u2014 and the task's `stateDocs` memory manifest ({docs:[{key,scope}],records:[{kind,scope}]}, scope shared|member) when it declares one. To create, publish/import or audit a task (including local skills), fetch the `task-architect` task first and follow it \u2014 `createTask` is only the final upsert it performs.";
123134
+ var GET_TASK_BASE_DESCRIPTION = "Search the workspace's TASKS by intent and RETURN the matching task(s) as markdown. A task is the workspace's reusable card \u2014 a one-shot procedure and a scheduled, memory-carrying job are the same kind of row, so this ONE tool searches all of them. Like getAllowlist, it does NOT execute anything \u2014 present the matched task(s) to the user and get their explicit confirmation before running one with `runTask`. Input: intent (natural-language description of what the user wants), optional category. On an unambiguous single match it also returns the raw `markdownBody`, spilled to a file path when large, so you can edit the task cross-session without hand-stripping the display prefix \u2014 and the task's `stateDocs` memory manifest ({docs:[{key,scope}],records:[{kind,scope}]}, scope shared|member) when it declares one. To create, publish/import or audit a task (including local skills), fetch the `task-architect` task first and follow it \u2014 `createTask` is only the final upsert it performs.";
122905
123135
  function buildGetTaskDescription(items) {
122906
- return `${GET_TASK_BASE_DESCRIPTION}${buildCompoundLoopIndexSection(items, "task")}`;
123136
+ return `${GET_TASK_BASE_DESCRIPTION}${buildTaskIndexSection(items, "task")}`;
122907
123137
  }
122908
123138
  var refreshGetAllowlistDescriptionInFlight = false;
122909
123139
  var pendingRefreshTimer = null;
@@ -122982,19 +123212,14 @@ async function refreshDynamicPrompts() {
122982
123212
  const sdk = await getSDK();
122983
123213
  const nativeServer = server.nativeServer;
122984
123214
  if (!nativeServer?.registerPrompt) return;
122985
- const [compRes, loopRes] = await Promise.all([
122986
- sdk.compoundedSkills.list(apiKey).catch(() => ({ status: "nok" })),
122987
- sdk.loops.list(apiKey).catch(() => ({ status: "nok" }))
122988
- ]);
122989
- const compounds = compRes?.status === "ok" ? compRes.items.filter((c) => c.draft === false) : [];
122990
- const loops2 = loopRes?.status === "ok" ? loopRes.items.filter((l) => l.draft === false) : [];
122991
- if (compRes?.status === "ok" && loopRes?.status === "ok" && Array.isArray(compRes.items) && Array.isArray(loopRes.items)) {
122992
- cardCounts = { compounds: compRes.items.length, loops: loopRes.items.length };
123215
+ const listRes = await sdk.loops.list(apiKey).catch(() => ({ status: "nok" }));
123216
+ const tasks2 = listRes?.status === "ok" ? listRes.items.filter((l) => l.draft === false) : [];
123217
+ if (listRes?.status === "ok" && Array.isArray(listRes.items)) {
123218
+ cardCounts = { tasks: listRes.items.length };
122993
123219
  }
122994
- compoundIndexCache = compounds.map((c) => ({ slug: c.slug, category: c.category, updatedAt: c.updatedAt }));
122995
- loopIndexCache = loops2.map((l) => ({ slug: l.slug, category: l.category, updatedAt: l.updatedAt }));
122996
123220
  const taskBySlug = /* @__PURE__ */ new Map();
122997
- for (const it of [...compoundIndexCache ?? [], ...loopIndexCache ?? []]) {
123221
+ for (const l of tasks2) {
123222
+ const it = { slug: l.slug, category: l.category, updatedAt: l.updatedAt };
122998
123223
  const existing = taskBySlug.get(it.slug);
122999
123224
  if (!existing || String(it.updatedAt ?? "") > String(existing.updatedAt ?? "")) taskBySlug.set(it.slug, it);
123000
123225
  }
@@ -123009,8 +123234,7 @@ async function refreshDynamicPrompts() {
123009
123234
  } catch {
123010
123235
  }
123011
123236
  const desired = /* @__PURE__ */ new Map();
123012
- for (const c of compounds) desired.set(promptName("compound", c.slug), { kind: "compound", id: c.id, summary: c });
123013
- for (const l of loops2) desired.set(promptName("loop", l.slug), { kind: "loop", id: l.id, summary: l });
123237
+ for (const l of tasks2) if (!desired.has(promptName(l.slug))) desired.set(promptName(l.slug), { id: l.id, summary: l });
123014
123238
  for (const [name, handle] of dynamicPromptHandles) {
123015
123239
  if (!desired.has(name)) {
123016
123240
  try {
@@ -123020,19 +123244,13 @@ async function refreshDynamicPrompts() {
123020
123244
  dynamicPromptHandles.delete(name);
123021
123245
  }
123022
123246
  }
123023
- for (const [name, { kind, id, summary }] of desired) {
123247
+ for (const [name, { id, summary }] of desired) {
123024
123248
  if (dynamicPromptHandles.has(name)) continue;
123025
- const entry = buildPromptEntry(kind, summary);
123249
+ const entry = buildPromptEntry(summary);
123026
123250
  const cb = async () => {
123027
- let full;
123028
- if (kind === "compound") {
123029
- const got = await sdk.compoundedSkills.get(apiKey, id);
123030
- full = got?.status === "ok" ? got.skill : void 0;
123031
- } else {
123032
- const got = await sdk.loops.get(apiKey, id);
123033
- full = got?.status === "ok" ? got.loop : void 0;
123034
- }
123035
- return buildPromptContent(kind, full ?? { slug: summary.slug, markdownBody: "" });
123251
+ const got = await sdk.loops.get(apiKey, id);
123252
+ const full = got?.status === "ok" ? got.loop : void 0;
123253
+ return buildPromptContent(full ?? { slug: summary.slug, markdownBody: "" });
123036
123254
  };
123037
123255
  try {
123038
123256
  const handle = nativeServer.registerPrompt(name, { description: entry.description, argsSchema: void 0 }, cb);
@@ -124460,19 +124678,17 @@ function suggestedCategoryFromSlug(slug) {
124460
124678
  const prefix = String(slug ?? "").trim().split(/[-_]/)[0]?.trim();
124461
124679
  return prefix ? prefix.toLowerCase() : void 0;
124462
124680
  }
124463
- function categoryGateResponse(params, slug, kind) {
124681
+ function categoryGateResponse(params, slug) {
124464
124682
  if (typeof params.category === "string" && params.category.trim()) return void 0;
124465
124683
  const suggestion = suggestedCategoryFromSlug(slug);
124466
124684
  const suggestionText = suggestion ? ` A reasonable default (derived from the slug prefix) is "${suggestion}", but confirm it with the user.` : "";
124467
- const noun = kind === "compound" ? "compound" : kind === "loop" ? "loop" : "task";
124468
- const toolSuffix = kind === "compound" ? "Compound" : kind === "loop" ? "Loop" : "Task";
124469
124685
  return {
124470
124686
  content: [{ type: "text", text: JSON.stringify({
124471
124687
  success: false,
124472
124688
  error: "category_required",
124473
124689
  guidance: {
124474
- say_to_user: `Before I publish this ${noun}, which category should it go under?${suggestionText}`,
124475
- next_action: `Ask the user for the category, then call create${toolSuffix} again with an explicit \`category\`.`,
124690
+ say_to_user: `Before I publish this task, which category should it go under?${suggestionText}`,
124691
+ next_action: `Ask the user for the category, then call createTask again with an explicit \`category\`.`,
124476
124692
  stop: true
124477
124693
  },
124478
124694
  ...suggestion ? { suggestedCategory: suggestion } : {}
@@ -124534,12 +124750,27 @@ server.tool(
124534
124750
  resolve: (sdk, apiKey, intent, category) => sdk.tasks.resolve(apiKey, intent, category),
124535
124751
  // Same wording as the slash-prompt directive (src/tasks/task-run-section.ts): after the pick,
124536
124752
  // ASK HOW to run it — a run mode is a spend decision, so no surface picks one silently.
124537
- sayToUser: "Present these matching tasks to the user, briefly explain them, and ask which one to run before proceeding. Then, before running the one they pick, ASK HOW to run it and wait for an answer \u2014 always ask, never pick one silently: (1) one-time, in this chat, followed here; (2) one-time, headless in its own process via `ametyst task run <slug>`; (3) scheduled, recurring via `ametyst task schedule <slug> --every <dur>` (ask which cadence). Only then call runTask with the chosen mode (`in-chat` for 1, `headless` for 2) \u2014 for 3, hand the user the schedule command instead.",
124753
+ sayToUser: "Present these matching tasks to the user, briefly explain them, and ask which one to run before proceeding. Then, before running the one they pick, ASK HOW to run it and wait for an answer \u2014 always ask, never pick one silently: (1) one-time, in this chat, followed here; (2) one-time, headless in its own process via `ametyst task run <slug>`; (3) scheduled, recurring via `ametyst task schedule <slug> --every <dur>` (ask which cadence). Only then call runTask with the chosen mode (`in-chat` for 1, `headless` for 2) \u2014 for 3, hand the user the schedule command instead. When the user gave arguments for the run (a company, a cap, a list), pass them to runTask as `input` \u2014 they reach the task exactly as if the user had typed them in chat.",
124538
124754
  resolverFailedSayToUser: "Couldn't search tasks right now.",
124539
124755
  surfaceRawBodies: true,
124540
124756
  fetchManifest: (sdk, apiKey, slug) => readTaskManifest(sdk, apiKey, slug)
124541
124757
  })
124542
124758
  );
124759
+ function shellQuote2(s) {
124760
+ return `'${s.replace(/'/g, `'\\''`)}'`;
124761
+ }
124762
+ async function resolveTaskOwnership(sdk, apiKey, entity) {
124763
+ try {
124764
+ const createdBy = typeof entity?.createdBy === "string" ? entity.createdBy.trim() : "";
124765
+ if (!createdBy) return { resolved: false };
124766
+ const res = await sdk.compoundedSkills.getSyncSelection(apiKey);
124767
+ const name = res?.status === "ok" && typeof res.self?.name === "string" ? res.self.name.trim() : "";
124768
+ if (!name) return { resolved: false };
124769
+ return { resolved: true, isOwner: name === createdBy, createdBy };
124770
+ } catch {
124771
+ return { resolved: false };
124772
+ }
124773
+ }
124543
124774
  function taskMemoryBlock(slug, docKey) {
124544
124775
  const listRecords = `taskMemoryGet({ taskSlug: "${slug}", records: true, kind: "<kind>" })`;
124545
124776
  return {
@@ -124562,7 +124793,7 @@ function closeAllLiveDashboards() {
124562
124793
  liveDashboards.clear();
124563
124794
  }
124564
124795
  process.once("exit", closeAllLiveDashboards);
124565
- async function startLiveDashboard(entity, dir) {
124796
+ async function startLiveDashboard(entity, dir, sdk, apiKey) {
124566
124797
  const slug = typeof entity?.slug === "string" ? entity.slug : "";
124567
124798
  const html = entity?.dashboardHtml;
124568
124799
  if (typeof html !== "string" || !html) return null;
@@ -124573,8 +124804,11 @@ async function startLiveDashboard(entity, dir) {
124573
124804
  previous.close();
124574
124805
  }
124575
124806
  const handle = await startDashboardServer({
124576
- loop: { slug, dashboardHtml: html, dashboardManifest: entity?.dashboardManifest },
124577
- loopDir: dir,
124807
+ task: { slug, dashboardHtml: html, dashboardManifest: entity?.dashboardManifest },
124808
+ runDir: dir,
124809
+ // `/data` reads the manifest-named docs from Ametyst MEMORY first — the run writes there
124810
+ // through taskMemoryAppend and touches the local file only at exit — then the file.
124811
+ readDoc: memoryDocResolver(sdk, apiKey, slug, normalizeMemoryManifest(entity?.stateDocs ?? null)),
124578
124812
  deps: { log: (line) => console.error(line) }
124579
124813
  });
124580
124814
  if (!handle) return null;
@@ -124617,16 +124851,20 @@ async function runTaskCore(params, flavor) {
124617
124851
  guidance: { say_to_user: flavor.missingRefSayToUser, next_action: flavor.missingRefNextAction }
124618
124852
  }) }] };
124619
124853
  }
124620
- const headless = (modeSource2) => ({ content: [{ type: "text", text: JSON.stringify({
124621
- success: true,
124622
- mode: "headless",
124623
- ...modeSource2 ? { modeSource: modeSource2 } : {},
124624
- command: flavor.headlessCommand(ref),
124625
- guidance: {
124626
- say_to_user: flavor.headlessSayToUser(ref),
124627
- next_action: "Tell the user to run the command in a shell."
124628
- }
124629
- }) }] });
124854
+ const input = typeof params.input === "string" && params.input.trim() !== "" ? params.input : void 0;
124855
+ const headless = (modeSource2) => {
124856
+ const command = flavor.headlessCommand(ref, input);
124857
+ return { content: [{ type: "text", text: JSON.stringify({
124858
+ success: true,
124859
+ mode: "headless",
124860
+ ...modeSource2 ? { modeSource: modeSource2 } : {},
124861
+ command,
124862
+ guidance: {
124863
+ say_to_user: flavor.headlessSayToUser(ref, command),
124864
+ next_action: "Tell the user to run the command in a shell."
124865
+ }
124866
+ }) }] };
124867
+ };
124630
124868
  const invalidMode = (provided) => ({ content: [{ type: "text", text: JSON.stringify({
124631
124869
  success: false,
124632
124870
  error: "invalid_mode",
@@ -124664,13 +124902,15 @@ async function runTaskCore(params, flavor) {
124664
124902
  `(${entity.slug}: run folder anchored on the fallback ${runRoot.root} \u2014 ${(runRoot.rejected ?? []).map((r) => `${r.dir}: ${r.why}`).join("; ")})`
124665
124903
  );
124666
124904
  }
124905
+ const legacyHint = legacyRunFolderHint(entity.slug, runRoot.root);
124906
+ if (legacyHint) console.error(legacyHint);
124667
124907
  const materialized = flavor.materialize(entity, randomUUID4(), runRoot.root);
124668
124908
  const docBoot = await materializeMemoryDocs(sdk, apiKey, entity, materialized.dir);
124669
124909
  for (const note of docBoot.notes) console.error(`(${entity.slug} memory docs: ${note})`);
124670
124910
  const runFiles = { ...materialized.files, ...docBoot.files };
124671
124911
  const est = estimateBlastRadius(entity);
124672
- const shipBack = flavor.buildShipBack({ dir: materialized.dir, entity });
124673
- const dashboardUrl = await startLiveDashboard(entity, materialized.dir);
124912
+ const shipBack = await flavor.buildShipBack({ dir: materialized.dir, entity, sdk, apiKey });
124913
+ const dashboardUrl = await startLiveDashboard(entity, materialized.dir, sdk, apiKey);
124674
124914
  const dashboardLine = dashboardUrl ? `Live dashboard: ${dashboardUrl}` : NO_DASHBOARD_MESSAGE;
124675
124915
  return { content: [{ type: "text", text: JSON.stringify({
124676
124916
  success: true,
@@ -124690,7 +124930,9 @@ async function runTaskCore(params, flavor) {
124690
124930
  // the slug because the run context carries no task identity — which is exactly why
124691
124931
  // the taskMemory* tools take an explicit `taskSlug`.
124692
124932
  memory: taskMemoryBlock(entity.slug, memoryDocKey),
124693
- directive: flavor.buildDirective({ dir: materialized.dir, slug: entity.slug, files: runFiles, docKey: memoryDocKey, runRoot }),
124933
+ // The user's arguments lead the directive, in the same LAUNCH INPUT block the headless
124934
+ // launcher prepends to its prompt — so both surfaces hand them over in the same words.
124935
+ directive: launchInputBlock(input) + flavor.buildDirective({ dir: materialized.dir, slug: entity.slug, files: runFiles, docKey: memoryDocKey, runRoot }),
124694
124936
  ...shipBack ? { shipBack } : {}
124695
124937
  }) }, { type: "text", text: dashboardLine }] };
124696
124938
  } catch (error) {
@@ -124701,11 +124943,12 @@ async function runTaskCore(params, flavor) {
124701
124943
  server.tool(
124702
124944
  {
124703
124945
  name: "runTask",
124704
- description: "Run a workspace TASK after the user has confirmed it (getTask already presented + stopped). A task is the unified card \u2014 what used to be a 'compound skill' or a 'loop' is one row now, and this ONE tool runs either. Two modes: `in-chat` (default) materializes the task's definition files locally and returns them plus a directive so YOU execute it inline in this session, paying for any step via the `spend` tool; `headless` returns the shell command to run it unattended in its own process. Only the NON-EMPTY definition files are materialized \u2014 a compound-shaped task (no VISION/CONSTRAINTS) gets just SKILL.md (+README.md) and NO constraints ship-back, instead of empty files that look checked but say nothing. DEFAULT RUN MODE: when you OMIT `mode` (or pass it empty), the task body's YAML frontmatter is consulted for a `defaultRunMode: headless|in-chat` key and that is used; an explicit `mode` wins over it. The ONLY accepted values are `in-chat` and `headless` \u2014 anything else (a typo like `in-chatt`) is REJECTED with `invalid_mode` and nothing runs: a value you passed is never quietly ignored, never coerced, and never falls back to the frontmatter, because that would answer an explicit `in-chat` with an unattended headless run. The response reports `modeSource` (explicit / frontmatter / default) so you can see which one decided.",
124946
+ description: "Run a workspace TASK after the user has confirmed it (getTask already presented + stopped). A task is the workspace's reusable card \u2014 a one-shot procedure and a scheduled, memory-carrying job are the same kind of row, and this ONE tool runs either. Two modes: `in-chat` (default) materializes the task's definition files locally and returns them plus a directive so YOU execute it inline in this session, paying for any step via the `spend` tool; `headless` returns the shell command to run it unattended in its own process. Only the NON-EMPTY definition files are materialized \u2014 a task with no VISION/CONSTRAINTS gets just SKILL.md (+README.md) and NO constraints ship-back, instead of empty files that look checked but say nothing. DEFAULT RUN MODE: when you OMIT `mode` (or pass it empty), the task body's YAML frontmatter is consulted for a `defaultRunMode: headless|in-chat` key and that is used; an explicit `mode` wins over it. The ONLY accepted values are `in-chat` and `headless` \u2014 anything else (a typo like `in-chatt`) is REJECTED with `invalid_mode` and nothing runs: a value you passed is never quietly ignored, never coerced, and never falls back to the frontmatter, because that would answer an explicit `in-chat` with an unattended headless run. The response reports `modeSource` (explicit / frontmatter / default) so you can see which one decided.",
124705
124947
  inputs: [
124706
124948
  { name: "task", type: "string", required: true, description: "Slug or id of the task to run (as returned by getTask)." },
124707
124949
  { name: "mode", type: "string", required: false, description: 'Exactly "in-chat" (you run it inline now) or "headless" (run unattended via `ametyst task run`) \u2014 any other value is rejected with `invalid_mode` rather than coerced or ignored. Omit it (or pass empty) to use the task body\'s `defaultRunMode` frontmatter, falling back to "in-chat". An explicit value wins over the frontmatter.' },
124708
- { name: "dir", type: "string", required: false, description: "Directory to materialize and run in; defaults to the current project folder, falling back to a writable per-user location when there is none. The response reports the chosen root as `runRoot` and why as `runRootReason` (explicit | cwd | fallback)." }
124950
+ { name: "dir", type: "string", required: false, description: "Directory to materialize and run in; defaults to the current project folder, falling back to a writable per-user location when there is none. The response reports the chosen root as `runRoot` and why as `runRootReason` (explicit | cwd | fallback)." },
124951
+ { name: "input", type: "string", required: false, description: 'The user\'s arguments for THIS run, exactly as they would type them in chat \u2014 a company, a cap, a list (e.g. "run it on skyfire.com, cap $5"). A parameterised task run without them silently runs in its no-argument mode. Headless: the returned command carries them as `--input`. In-chat: the directive opens with a LAUNCH INPUT block holding them. Omit when the user gave no arguments.' }
124709
124952
  ]
124710
124953
  },
124711
124954
  async (params) => runTaskCore(params, {
@@ -124717,8 +124960,8 @@ server.tool(
124717
124960
  notFoundError: "task_not_found",
124718
124961
  fetch: (sdk, apiKey, ref) => sdk.tasks.get(apiKey, ref),
124719
124962
  pick: (res) => res.task,
124720
- headlessCommand: (ref) => `ametyst task run ${ref}`,
124721
- headlessSayToUser: (ref) => `Run \`ametyst task run ${ref}\` in a terminal \u2014 it materializes the task, runs it unattended, and ships back improvements on a clean finish. (\`task run\` is the unattended runner for every task row, whatever the task was originally authored as.) Paid steps go through your on-chain policy.`,
124963
+ headlessCommand: (ref, input) => `ametyst task run ${ref}${input === void 0 ? "" : ` --input ${shellQuote2(input)}`}`,
124964
+ headlessSayToUser: (_ref, command) => `Run \`${command}\` in a terminal \u2014 it materializes the task, runs it unattended, and ships back improvements on a clean finish. (\`task run\` is the unattended runner for every task row, whatever the task was originally authored as.) Paid steps go through your on-chain policy.`,
124722
124965
  honorFrontmatterDefault: true,
124723
124966
  materialize: (task, runId, runRoot) => materializeTask(task, runId, runRoot),
124724
124967
  buildDirective: ({ dir, slug, files, docKey, runRoot }) => {
@@ -124726,7 +124969,7 @@ server.tool(
124726
124969
  const whereNote = runRoot.reason === "fallback" ? `NOTE: this run's folder lives under the per-user fallback ${runRoot.root}, NOT under the current project, because the current folder was not writable (${(runRoot.rejected ?? []).map((r) => `${r.dir}: ${r.why}`).join("; ")}). Read the files from ${dir} exactly as given \u2014 do not go looking for them in the project. ` : "";
124727
124970
  const readDoc = typeof docKey === "string" ? `taskMemoryGet({ taskSlug: "${slug}", docKey: "${docKey}" }) for the "${docKey}" document (the first one this task declares), then ` : `this task declares NO memory document \u2014 do not read or create one; read `;
124728
124971
  const writeDoc = typeof docKey === "string" ? `, and rewrite the "${docKey}" document with taskMemoryAppend({ taskSlug: "${slug}", docKey: "${docKey}", content: "..." })` : ` \u2014 and NO document upsert, because this task declares none`;
124729
- return `${whereNote}Read the task files in ${dir} \u2014 the ONLY files materialized are: ${present.join(", ")} (plus STATUS.md, this run's own state). SKILL.md drives${files.vision ? "; VISION.md = done-condition" : ""}${files.constraints ? "; CONSTRAINTS.md = hard limits" : ""}. Any definition file NOT listed was empty on the task and was deliberately not written \u2014 do not go looking for it, and do not assume limits you cannot read. If the task has a QUEUE of work items it is EXTERNAL \u2014 NOT one of these files; SKILL.md tells you WHERE to read the queue from and WHERE to write the results. Execute the task here, in this session, until it is done (or, if it is a looping task, until VISION is met / the queue is drained). Pay for any step ONLY via the spend tool (the on-chain policy is the budget). Keep ${dir}/STATUS.md updated. CLEAN UP AFTER YOURSELF: on a CLEAN finish (done / queue drained, no brake), DELETE the ${dir} folder \u2014 it is this run's scratch space, not a record, and every run gets its own, so leaving them behind piles up orphans. On a dirty stop, leave ${dir} in place so the run can be resumed.
124972
+ return `${whereNote}Read the task files in ${dir} \u2014 the ONLY files materialized are: ${present.join(", ")} (plus STATUS.md, this run's own state). SKILL.md drives${files.vision ? "; VISION.md = done-condition" : ""}${files.constraints ? "; CONSTRAINTS.md = hard limits" : ""}. Any definition file NOT listed was empty on the task and was deliberately not written \u2014 do not go looking for it, and do not assume limits you cannot read. If the task has a QUEUE of work items it is EXTERNAL \u2014 NOT one of these files; SKILL.md tells you WHERE to read the queue from and WHERE to write the results. Execute the task here, in this session, until it is done (or, if it carries a VISION or a queue, until VISION is met / the queue is drained). Pay for any step ONLY via the spend tool (the on-chain policy is the budget). Keep ${dir}/STATUS.md updated. CLEAN UP AFTER YOURSELF: on a CLEAN finish (done / queue drained, no brake), DELETE the ${dir} folder \u2014 it is this run's scratch space, not a record, and every run gets its own, so leaving them behind piles up orphans. On a dirty stop, leave ${dir} in place so the run can be resumed.
124730
124973
 
124731
124974
  Your durable memory is available via the taskMemory* tools with taskSlug "${slug}" \u2014 and it, not ${dir}, is what survives this run. Nothing from it was injected into this run: READ WHAT YOU NEED FIRST with taskMemoryGet \u2014 ${readDoc}one list per record kind you need, taskMemoryGet({ taskSlug: "${slug}", records: true, kind: "<kind>" }) (latest version per key, archived=false by default); empty means this is the first run. WRITE A RUN RECORD BEFORE EXITING, on every path including a brake: taskMemoryAppend({ taskSlug: "${slug}", kind: "run", content: "<what you did, what you learned, what the next run should pick up>" })${writeDoc}.
124732
124975
 
@@ -124737,7 +124980,22 @@ ${TASK_MEMORY_MODEL_SECTION}`;
124737
124980
  // The `dir` cleanup does NOT live here: it is owed by EVERY in-chat run, and a task
124738
124981
  // with no constraints gets no ship-back at all — so it is stated once, in the
124739
124982
  // directive. This sentence only orders the two (ship back, then clean up).
124740
- buildShipBack: ({ entity }) => typeof entity?.constraintsMd === "string" && entity.constraintsMd.length > 0 ? `On a CLEAN finish (done / VISION met / queue drained, no brake), call createTask with id="${entity.id}" and the updated constraintsMd to ship improvements back \u2014 patch ONLY constraintsMd (F11: never the queue). Do that BEFORE the directive's clean-up step.` : void 0
124983
+ //
124984
+ // ⛔ ONLY THE OWNER CAN SHIP BACK. buyer-api lets the row's creator or the workspace admin
124985
+ // modify a task; a member running an admin-owned task was told to `createTask` anyway,
124986
+ // tripped the category gate first and then got a 403. When ownership resolves and the
124987
+ // caller is not the owner, the directive says where the learnings go instead. When the
124988
+ // caller IS the owner, the call names the task's existing `category` so a MODIFY does not
124989
+ // trip `category_required`. Unresolvable → today's directive.
124990
+ buildShipBack: async ({ entity, sdk, apiKey }) => {
124991
+ if (!(typeof entity?.constraintsMd === "string" && entity.constraintsMd.length > 0)) return void 0;
124992
+ const owner = await resolveTaskOwnership(sdk, apiKey, entity);
124993
+ if (owner.resolved && !owner.isOwner) {
124994
+ return `SHIP-BACK: none. This task is owned by ${owner.createdBy}; do NOT patch its CONSTRAINTS (you would get 403). Your learnings belong in your run diary record, and in the task's member-scoped "learnings" document when its manifest declares one (taskMemoryAppend docKey "learnings"). To propose a change to the shared CONSTRAINTS, tell the user to open a card proposal from the web app.`;
124995
+ }
124996
+ const category = typeof entity?.category === "string" && entity.category.trim() ? `, category="${entity.category}"` : "";
124997
+ return `On a CLEAN finish (done / VISION met / queue drained, no brake), call createTask with id="${entity.id}"${category} and the updated constraintsMd to ship improvements back \u2014 patch ONLY constraintsMd (F11: never the queue). Do that BEFORE the directive's clean-up step.`;
124998
+ }
124741
124999
  })
124742
125000
  );
124743
125001
  function taskMemoryContext(params) {
@@ -124757,7 +125015,7 @@ function taskMemoryContext(params) {
124757
125015
  error: "taskSlug_required",
124758
125016
  guidance: {
124759
125017
  say_to_user: "I need to know which task's memory to use.",
124760
- next_action: "Pass taskSlug explicitly \u2014 it is the slug of the task you are running (also in AMETYST_LOOP_SLUG)."
125018
+ next_action: "Pass taskSlug explicitly \u2014 it is the slug of the task you are running (also in AMETYST_TASK_SLUG)."
124761
125019
  }
124762
125020
  }) }] }
124763
125021
  };
@@ -124854,7 +125112,7 @@ server.tool(
124854
125112
  name: "taskMemoryAppend",
124855
125113
  description: "Write to a task's durable memory \u2014 the state that survives the run (the task's materialized directory does not outlive a clean exit). See the TASK MEMORY MODEL in your instructions for what docs and records are. Pass `content` WITH `docKey` to upsert a state document in place; pass `content` with `kind` to append a record: a RESERVED diary kind (run | decision | error | import) takes NO `key`; a FREE item kind (declared in the task's manifest) REQUIRES `key`, and every append with the same key is a new VERSION of that item (`archived: true` writes a closed version directly). The kind/key/size rules are checked here before the write and named on refusal; the server's own refusals are relayed verbatim. NAMESPACE: omit `scope` for a key/kind the task's `stateDocs` manifest declares (the server resolves it; a disagreeing scope is refused with `memory_scope_mismatch`); for an undeclared doc key pass `scope` explicitly \u2014 omitted, it lands on the seat and the response carries `warning: \"not in manifest\"`.",
124856
125114
  inputs: [
124857
- { name: "taskSlug", type: "string", required: true, description: "Slug of the task whose memory to write. Required and explicit \u2014 the fire context carries no task identity. Also exported as AMETYST_LOOP_SLUG." },
125115
+ { name: "taskSlug", type: "string", required: true, description: "Slug of the task whose memory to write. Required and explicit \u2014 the fire context carries no task identity. Also exported as AMETYST_TASK_SLUG." },
124858
125116
  { name: "content", type: "string", required: true, description: "What to store. For a run record: what you did, what you learned, what the next fire should pick up. \u2264 32 KB on a diary kind, \u2264 256 KB on an item kind or a doc." },
124859
125117
  { name: "docKey", type: "string", required: false, description: `Set this to upsert a STATE DOCUMENT under that key instead of appending a record \u2014 the key the task's OWN manifest declares (e.g. "board"); there is no universal default key, and upserting an undeclared one mints a document nobody reads. Upserts replace in place, so rewriting a doc at a steady size costs nothing against the quota.` },
124860
125118
  { name: "kind", type: "string", required: false, description: `Record kind when appending \u2014 a reserved diary kind ("run", "decision", "error", "import") or a free item kind the task's manifest declares (a token [a-z0-9-]{1,64}, e.g. "pbi"). Defaults to "run". Ignored when docKey is set.` },
@@ -124963,7 +125221,7 @@ server.tool(
124963
125221
  name: "taskMemoryGet",
124964
125222
  description: 'Read a task\'s durable memory \u2014 what past runs left behind. See the TASK MEMORY MODEL in your instructions for what docs and records are and when to read which. Selectors: `docKey` for one state document; `records: true` for a record LISTING (filters: `kind`, `archived` false|true|"all" \u2014 default false, i.e. live items plus the diary; `key`, `keyPrefix`, `since`; `fields: "keys"` for identities + one-line summaries with no bodies; `count: true` for a number only; `limit` + `cursor` for keyset paging \u2014 the response carries `nextCursor`); `key` ALONE (no `records`, no filter) opens one item by key \u2014 its latest version, live or archived \u2014 with `history: true` for every version oldest-first; `usage: true` for the quota footprint. ANY record filter (`kind`, `keyPrefix`, `archived`, `since`, `fields`, `count`, `limit`, `cursor`, or `key` with `records`) selects the LISTING and reads no document, whether or not you also pass `records: true`. With no selector it returns the task\'s FIRST DECLARED document plus the default listing \u2014 a task declaring none gets records only, and no document is invented for it. Keyed items always collapse to their latest version per key. NAMESPACES: a DECLARED doc key is read from its manifest namespace by the server; an UNDECLARED one from this seat\'s row first, then the shared row. An empty result means no run has written yet.',
124965
125223
  inputs: [
124966
- { name: "taskSlug", type: "string", required: true, description: "Slug of the task whose memory to read. Required and explicit \u2014 the fire context carries no task identity. Also exported as AMETYST_LOOP_SLUG." },
125224
+ { name: "taskSlug", type: "string", required: true, description: "Slug of the task whose memory to read. Required and explicit \u2014 the fire context carries no task identity. Also exported as AMETYST_TASK_SLUG." },
124967
125225
  { name: "docKey", type: "string", required: false, description: `Read one state document by key \u2014 the key the task's own manifest declares (e.g. "board"); there is no universal default key. Declared in the manifest \u2192 its declared namespace; undeclared \u2192 own row, then shared.` },
124968
125226
  { name: "records", type: "boolean", required: false, description: "Read a record LISTING instead of the default read \u2014 the explicit spelling; any filter below already implies it." },
124969
125227
  { name: "key", type: "string", required: false, description: "With `records: true`: narrow the listing to this one key. ALONE: open the item by key (latest version, live or archived; `record_not_found` if never written)." },
@@ -125157,7 +125415,7 @@ server.tool(
125157
125415
  name: "taskMemoryArchive",
125158
125416
  description: "CLOSE a keyed item in a task's durable memory \u2014 see the TASK MEMORY MODEL in your instructions. Appends one more version of the item with `archived: true` (archivedAt stamped by the server), copying the latest live version's content; the history stays intact and nothing is deleted or edited. Archived items drop out of default listings (archived=false) and stay readable by key or with archived: true|\"all\". `record_not_found` if the key was never written; `already_archived` if its latest version is already closed.",
125159
125417
  inputs: [
125160
- { name: "taskSlug", type: "string", required: true, description: "Slug of the task whose item to close. Required and explicit \u2014 the fire context carries no task identity. Also exported as AMETYST_LOOP_SLUG." },
125418
+ { name: "taskSlug", type: "string", required: true, description: "Slug of the task whose item to close. Required and explicit \u2014 the fire context carries no task identity. Also exported as AMETYST_TASK_SLUG." },
125161
125419
  { name: "key", type: "string", required: true, description: "The item's key, exactly as written (case-sensitive)." },
125162
125420
  { name: "note", type: "string", required: false, description: 'Optional archive note \u2014 why it is closed ("shipped in #212", "superseded by pbi-9").' }
125163
125421
  ]
@@ -125235,7 +125493,7 @@ async function upsertTaskCore(params, flavor) {
125235
125493
  void refreshDynamicPrompts();
125236
125494
  const mode2 = id ? "modified" : "created";
125237
125495
  const effectiveManifest = "stateDocs" in entity ? entity.stateDocs : !id ? null : previous && previous.available ? previous.content.stateDocs ?? null : void 0;
125238
- const summary = buildLoopUpsertSummary({
125496
+ const summary = buildTaskUpsertSummary({
125239
125497
  mode: mode2,
125240
125498
  sent: entity,
125241
125499
  previous,
@@ -125244,7 +125502,7 @@ async function upsertTaskCore(params, flavor) {
125244
125502
  const payload = enforceResponseCap({
125245
125503
  success: true,
125246
125504
  mode: mode2,
125247
- [flavor.responseKey]: projectLoopIdentity(flavor.pick(res)),
125505
+ [flavor.responseKey]: projectTaskIdentity(flavor.pick(res)),
125248
125506
  // WHERE IT LANDED, on the receipt itself. The 2026-08-31 incident put a
125249
125507
  // task into the wrong (admin) workspace and the success payload named
125250
125508
  // no workspace at all, so neither the agent nor the human reading the
@@ -125298,7 +125556,7 @@ async function upsertTaskCore(params, flavor) {
125298
125556
  ["visionMd", "visionFilePath"],
125299
125557
  ["constraintsMd", "constraintsFilePath"],
125300
125558
  ["readmeMd", "readmeFilePath"],
125301
- // Per-loop dashboard (loop-run D12): same inline-or-file verbatim plumbing as the
125559
+ // Per-task dashboard (loop-run D12): same inline-or-file verbatim plumbing as the
125302
125560
  // *Md files — the bytes reach the SDK CreateLoopInput unchanged.
125303
125561
  ["dashboardHtml", "dashboardHtmlFilePath"],
125304
125562
  ["dashboardManifest", "dashboardManifestFilePath"]
@@ -125331,7 +125589,7 @@ async function upsertTaskCore(params, flavor) {
125331
125589
  }) }] };
125332
125590
  }
125333
125591
  }
125334
- const gate = categoryGateResponse(params, slug, flavor.gateKind);
125592
+ const gate = categoryGateResponse(params, slug);
125335
125593
  if (gate) return gate;
125336
125594
  let graphJson;
125337
125595
  if (graphJsonProvided) {
@@ -125384,10 +125642,10 @@ async function upsertTaskCore(params, flavor) {
125384
125642
  return { content: [{ type: "text", text: JSON.stringify({
125385
125643
  success: false,
125386
125644
  redirect: "task-architect",
125387
- reason: "a new loop needs the qualification gate, brakes, status vocabulary and dashboard that the architect interview produces",
125645
+ reason: "a new task needs the qualification gate, brakes, status vocabulary and dashboard that the architect interview produces",
125388
125646
  guidance: {
125389
- say_to_user: "I can't spin up a loop from a one-liner \u2014 a new loop needs the qualification gate, brakes, status vocabulary and dashboard that the architect interview produces. Run the `task-architect` task instead; it interviews you and then publishes the finished loop.",
125390
- next_action: 'Call getTask({ intent: "create a new loop" }) to fetch the `task-architect` task, present it, and run it once the user confirms. When you do, make sure the loop it designs reads its memory at boot and writes a run record before exiting (see this tool\'s description) \u2014 a loop that skips either one restarts from zero on every fire.',
125647
+ say_to_user: "I can't spin up a task from a one-liner \u2014 a new task needs the qualification gate, brakes, status vocabulary and dashboard that the architect interview produces. Run the `task-architect` task instead; it interviews you and then publishes the finished task.",
125648
+ next_action: 'Call getTask({ intent: "create a new task" }) to fetch the `task-architect` task, present it, and run it once the user confirms. When you do, make sure the task it designs reads its memory at boot and writes a run record before exiting (see this tool\'s description) \u2014 a task that skips either one restarts from zero on every fire.',
125391
125649
  stop: true
125392
125650
  }
125393
125651
  }) }] };
@@ -125401,7 +125659,7 @@ async function upsertTaskCore(params, flavor) {
125401
125659
  server.tool(
125402
125660
  {
125403
125661
  name: "createTask",
125404
- 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.',
125662
+ description: "Create OR modify a workspace TASK (upsert). A task is the workspace's reusable card, and this ONE tool authors every shape of it: a task with only a `markdownBody` is a one-shot procedure, a task that also carries visionMd/constraintsMd/readmeMd is a scheduled job with a done-condition, hard limits and its own memory. 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.',
125405
125663
  inputs: [
125406
125664
  { name: "slug", type: "string", required: false, description: "URL-safe unique slug for the task within the workspace. Required on CREATE." },
125407
125665
  { name: "descriptionShort", type: "string", required: false, description: "One-line description of what the task does. Required on CREATE." },
@@ -125427,7 +125685,6 @@ server.tool(
125427
125685
  async (params) => upsertTaskCore(params, {
125428
125686
  toolName: "createTask",
125429
125687
  responseKey: "task",
125430
- gateKind: "task",
125431
125688
  get: (sdk, apiKey, id) => sdk.tasks.get(apiKey, id),
125432
125689
  // `body` is assembled field-by-field in `upsertTaskCore` precisely so MODIFY stays a
125433
125690
  // no-clobber PATCH, so it cannot statically satisfy `CreateTaskInput`'s required
@@ -126800,7 +127057,7 @@ import { existsSync as existsSync15 } from "fs";
126800
127057
  import { homedir as homedir12 } from "os";
126801
127058
  import { join as join19 } from "path";
126802
127059
 
126803
- // src/compounds/sync-skills.ts
127060
+ // src/tasks/sync-skills.ts
126804
127061
  init_esm_shims();
126805
127062
  init_paths();
126806
127063
  import { accessSync as accessSync3, constants as constants3, existsSync as existsSync14, mkdirSync as mkdirSync9, readdirSync as readdirSync5, readFileSync as readFileSync14, rmSync, writeFileSync as writeFileSync10 } from "fs";
@@ -126834,7 +127091,7 @@ async function fetchSelectionContext(sdk, apiKey) {
126834
127091
  }
126835
127092
  }
126836
127093
  function buildStubContent(item) {
126837
- const { kind, slug, descriptionShort } = item;
127094
+ const { slug, descriptionShort } = item;
126838
127095
  return `---
126839
127096
  name: ${slug}
126840
127097
  description: ${JSON.stringify(descriptionShort)}
@@ -126842,9 +127099,9 @@ description: ${JSON.stringify(descriptionShort)}
126842
127099
 
126843
127100
  ${MANAGED_MARKER}
126844
127101
 
126845
- This is a pointer to the Ametyst ${kind} \`${slug}\` \u2014 the real ${kind} lives in the Ametyst workspace, not in this file.
127102
+ This is a pointer to the Ametyst task \`${slug}\` \u2014 the real task lives in the Ametyst workspace, not in this file.
126846
127103
 
126847
- ${taskRunSection(slug, kind)}`;
127104
+ ${taskRunSection(slug)}`;
126848
127105
  }
126849
127106
  function isManaged(file) {
126850
127107
  try {
@@ -126925,27 +127182,15 @@ async function syncSkills(opts = {}) {
126925
127182
  const global2 = scope.global;
126926
127183
  const fallback2 = "fallback" in scope ? scope.fallback : void 0;
126927
127184
  const { sdk, apiKey } = await getCliSdk();
126928
- const [compRes, loopRes] = await Promise.all([sdk.compoundedSkills.list(apiKey), sdk.loops.list(apiKey)]);
126929
- if (compRes?.status !== "ok") throw new Error(`failed to list compounds: ${compRes?.error ?? "unknown error"}`);
126930
- if (loopRes?.status !== "ok") throw new Error(`failed to list loops: ${loopRes?.error ?? "unknown error"}`);
126931
- const allItems = [
126932
- ...compRes.items.filter((c) => c.draft === false).map((c) => ({
126933
- kind: "compound",
126934
- slug: c.slug,
126935
- descriptionShort: c.descriptionShort,
126936
- id: c.id ?? "",
126937
- category: c.category ?? null,
126938
- createdBy: c.createdBy ?? ""
126939
- })),
126940
- ...loopRes.items.filter((l) => l.draft === false).map((l) => ({
126941
- kind: "loop",
126942
- slug: l.slug,
126943
- descriptionShort: l.descriptionShort,
126944
- id: l.id ?? "",
126945
- category: l.category ?? null,
126946
- createdBy: l.createdBy ?? ""
126947
- }))
126948
- ];
127185
+ const listRes = await sdk.loops.list(apiKey);
127186
+ if (listRes?.status !== "ok") throw new Error(`failed to list tasks: ${listRes?.error ?? "unknown error"}`);
127187
+ const allItems = listRes.items.filter((l) => l.draft === false).map((l) => ({
127188
+ slug: l.slug,
127189
+ descriptionShort: l.descriptionShort,
127190
+ id: l.id ?? "",
127191
+ category: l.category ?? null,
127192
+ createdBy: l.createdBy ?? ""
127193
+ }));
126949
127194
  const items = applySyncSelection(allItems, await fetchSelectionContext(sdk, apiKey));
126950
127195
  const desired = /* @__PURE__ */ new Map();
126951
127196
  for (const item of items) {
@@ -127073,7 +127318,7 @@ async function serveCommand() {
127073
127318
  );
127074
127319
  await startMCPServer(keystore, eoa, { ...config, apiKey, apiKeySource: resolved.source ?? void 0 }, versionNotice, {
127075
127320
  // Best-effort: materialize local `/`-command pointer skills for every published
127076
- // compound + loop so they're available without a manual `compound sync-skills`.
127321
+ // task so they're available without a manual `ametyst task sync-skills`.
127077
127322
  // Deferred from boot to the MCP initialize handshake so the sync is CLIENT-AWARE:
127078
127323
  // clientInfo.name tells us whether the host reads `.claude/skills` (Claude) or
127079
127324
  // `.codex/skills` (Codex); when it doesn't, presence detection picks the roots.
@@ -127680,53 +127925,53 @@ connectionsCommand.command("remove <provider>").description("Delete a stored con
127680
127925
  // src/commands/task.ts
127681
127926
  init_esm_shims();
127682
127927
 
127683
- // src/loops/index.ts
127928
+ // src/tasks/index.ts
127684
127929
  init_esm_shims();
127685
127930
 
127686
- // src/loops/materialize.ts
127931
+ // src/tasks/materialize.ts
127687
127932
  init_esm_shims();
127688
127933
  init_paths();
127689
127934
  import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync11 } from "fs";
127690
127935
  import { join as join20 } from "path";
127691
127936
  var MIRRORED_DEFINITION_FILES = ["SKILL.md", "VISION.md", "README.md"];
127692
- function materialize(loop2, fireId, stateDocs = [], runRoot) {
127693
- const dir = loopFireDir(loop2.slug, fireId, runRoot);
127937
+ function materialize(task, fireId, stateDocs = [], runRoot) {
127938
+ const dir = runFireDir(task.slug, fireId, runRoot);
127694
127939
  mkdirSync10(dir, { recursive: true, mode: 448 });
127695
127940
  for (const doc of stateDocs) {
127696
127941
  writeFileSync11(join20(dir, doc.filename), doc.body, { mode: 384 });
127697
127942
  }
127698
127943
  const files = {
127699
- "SKILL.md": loop2.markdownBody ?? "",
127700
- "VISION.md": loop2.visionMd ?? "",
127701
- "CONSTRAINTS.md": loop2.constraintsMd ?? "",
127702
- "README.md": loop2.readmeMd ?? ""
127944
+ "SKILL.md": task.markdownBody ?? "",
127945
+ "VISION.md": task.visionMd ?? "",
127946
+ "CONSTRAINTS.md": task.constraintsMd ?? "",
127947
+ "README.md": task.readmeMd ?? ""
127703
127948
  };
127704
- if (typeof loop2.dashboardHtml === "string" && loop2.dashboardHtml) {
127705
- files["dashboard.html"] = loop2.dashboardHtml;
127949
+ if (typeof task.dashboardHtml === "string" && task.dashboardHtml) {
127950
+ files["dashboard.html"] = task.dashboardHtml;
127706
127951
  }
127707
- if (typeof loop2.dashboardManifest === "string" && loop2.dashboardManifest) {
127708
- files["dashboard.manifest.json"] = loop2.dashboardManifest;
127952
+ if (typeof task.dashboardManifest === "string" && task.dashboardManifest) {
127953
+ files["dashboard.manifest.json"] = task.dashboardManifest;
127709
127954
  }
127710
127955
  for (const [name, body] of Object.entries(files)) {
127711
127956
  writeFileSync11(join20(dir, name), body, { mode: 384 });
127712
127957
  }
127713
127958
  writeFileSync11(
127714
127959
  join20(dir, "STATUS.md"),
127715
- `# STATUS \u2014 ${loop2.slug}
127960
+ `# STATUS \u2014 ${task.slug}
127716
127961
 
127717
- loop_id: ${loop2.id}
127962
+ loop_id: ${task.id}
127718
127963
  fire_id: ${fireId}
127719
127964
  started: pending
127720
127965
  queue: not started
127721
127966
  `,
127722
127967
  { mode: 384 }
127723
127968
  );
127724
- mirrorDefinitionFiles(loop2.slug, files, runRoot);
127969
+ mirrorDefinitionFiles(task.slug, files, runRoot);
127725
127970
  return dir;
127726
127971
  }
127727
127972
  function mirrorDefinitionFiles(slug, files, runRoot) {
127728
127973
  try {
127729
- const root2 = loopDir(slug, runRoot);
127974
+ const root2 = runDir(slug, runRoot);
127730
127975
  mkdirSync10(root2, { recursive: true, mode: 448 });
127731
127976
  for (const name of [...MIRRORED_DEFINITION_FILES, "dashboard.html", "dashboard.manifest.json"]) {
127732
127977
  const body = files[name];
@@ -127737,134 +127982,17 @@ function mirrorDefinitionFiles(slug, files, runRoot) {
127737
127982
  }
127738
127983
  }
127739
127984
 
127740
- // src/loops/launch.ts
127741
- init_esm_shims();
127742
- import { spawnSync } from "child_process";
127743
- function readGlobalGitConfig(key) {
127744
- try {
127745
- const r = spawnSync("git", ["config", "--global", "--get", key], { encoding: "utf-8" });
127746
- const v = r.status === 0 ? r.stdout.trim() : "";
127747
- return v || void 0;
127748
- } catch {
127749
- return void 0;
127750
- }
127751
- }
127752
- function deriveGitIdentityEnv(env = process.env, readGitConfig = readGlobalGitConfig) {
127753
- const name = env.AMETYST_LOOP_GIT_AUTHOR_NAME?.trim() || readGitConfig("user.name");
127754
- const email = env.AMETYST_LOOP_GIT_AUTHOR_EMAIL?.trim() || readGitConfig("user.email");
127755
- const out = {};
127756
- if (name) {
127757
- out.GIT_AUTHOR_NAME = name;
127758
- out.GIT_COMMITTER_NAME = name;
127759
- }
127760
- if (email) {
127761
- out.GIT_AUTHOR_EMAIL = email;
127762
- out.GIT_COMMITTER_EMAIL = email;
127763
- }
127764
- return out;
127765
- }
127766
- function buildLaunchArgs(dir, opts = {}, slug, deps = {}) {
127767
- const env = deps.env ?? process.env;
127768
- const maxBudget = resolveMaxBudgetUsd(opts, env);
127769
- const docKey = defaultDocKey(opts.memoryManifest);
127770
- const readDocSentence = typeof docKey === "string" ? `taskMemoryGet({ taskSlug: "${slug}", docKey: "${docKey}" }) for your "${docKey}" document \u2014 the FIRST document this task's memory manifest declares, materialized for you at boot as ${filenameForKey(docKey)} \u2014 then ` : docKey === null ? `THIS TASK DECLARES NO MEMORY DOCUMENT \u2014 do not read one and do not create one; your records ARE its memory. Read ` : `no memory document is named here \u2014 this launcher could not resolve the task's manifest, so do NOT assume one exists. Read `;
127771
- const writeDocSentence = typeof docKey === "string" ? `, and rewrite your "${docKey}" document with taskMemoryAppend({ taskSlug: "${slug}", docKey: "${docKey}", content: "<the state the next fire needs>" }).` : docKey === null ? `. This task declares no memory document, so there is nothing to rewrite \u2014 do not invent one; the run record and your keyed items are what the next fire reads.` : `. If this task declares a memory document, rewrite it with taskMemoryAppend({ taskSlug: "${slug}", docKey: "<the key its manifest declares>", content: "..." }) \u2014 this launcher could not name it for you, so do not guess a key.`;
127772
- const prompt = `You are running the Ametyst loop${slug ? ` "${slug}"` : ""} headless and unattended.
127773
-
127774
- ${dir} is THIS FIRE'S OWN directory. Other fires of the same loop may be running right now, each with its own directory alongside yours \u2014 wherever the loop's SKILL says <LOOPDIR> it means exactly ${dir}, never a sibling's directory and never their shared parent. Derive every path the SKILL asks you to create (links, run-state) from ${dir}; never from a path written literally in the SKILL prose.
127775
-
127776
- Read the loop definition files in ${dir}:
127777
- - SKILL.md \u2014 the driver; follow it.
127778
- - VISION.md \u2014 the objective / done-condition.
127779
- - CONSTRAINTS.md \u2014 hard limits; never violate them.
127780
- - STATUS.md \u2014 your run-state; keep it updated as you progress.
127781
-
127782
- The QUEUE (the work items) is EXTERNAL \u2014 it is NOT one of these files. SKILL.md tells you WHERE to read the queue from and WHERE to write the results; read your work items from that source.
127783
- ${slug ? `
127784
- YOUR DURABLE MEMORY survives this fire, and ${dir} does not \u2014 this directory is deleted when you exit, so anything you want the NEXT fire to know must go into memory, not into a file here. Reach it with the taskMemory* MCP tools, always with taskSlug "${slug}" (also exported as AMETYST_LOOP_SLUG). Nothing from it was injected into this fire beyond the materialized docs:
127785
- - FIRST, before you start work, read what you need with taskMemoryGet: ${readDocSentence}one list per record kind you need \u2014 taskMemoryGet({ taskSlug: "${slug}", records: true, kind: "<kind>" }) \u2014 which returns the latest version per key, archived=false by default. If everything is empty this is your first fire \u2014 say so in your run record.
127786
- - BEFORE YOU EXIT, on every path including a brake: taskMemoryAppend({ taskSlug: "${slug}", kind: "run", content: "<what you did, what you learned, what the next fire should pick up>" })${writeDocSentence}
127787
- - Items with an identity (a PBI, a test, a merchant) are keyed records of a free kind: taskMemoryAppend({ taskSlug: "${slug}", kind: "<kind>", key: "<id>", content }) writes or versions one; taskMemoryArchive({ taskSlug: "${slug}", key: "<id>", note }) closes it.
127788
- Memory is quota-bounded per workspace \u2014 an over-limit write is REJECTED and tells you the limit, never silently truncated. If a write is refused, shorten it and write again; do not skip the run record.
127789
-
127790
- ${TASK_MEMORY_MODEL_SECTION}
127791
- ` : ""}
127792
- Execute the loop until VISION is met or the queue is drained. For any step that costs money, use the Ametyst \`spend\` MCP tool (the on-chain policy enforces the budget) \u2014 do NOT invent another payment path.
127793
-
127794
- When you finish cleanly (VISION met / queue drained), write "status: done" and "queue drained" into ${dir}/STATUS.md. If you must stop early (a brake/constraint was hit or an unrecoverable error occurred), write "brake: <reason>" into ${dir}/STATUS.md and exit. Never exceed the constraints.`;
127795
- const args = [
127796
- "-p",
127797
- prompt,
127798
- "--dangerously-skip-permissions",
127799
- // The WRAPPER (`ametyst task run`, see runTask) owns shipping the loop's learned
127800
- // CONSTRAINTS back to Ametyst on exit. The headless agent must NEVER rewrite its own
127801
- // stored task record, so deny the upsert tool even though `--dangerously-skip-permissions`
127802
- // otherwise grants every tool. (STATUS/QUEUE stay local run-state and are never shipped.)
127803
- // ⛔ THE LIST IS DERIVED, NOT TYPED, on both axes — a deny that names the wrong string is
127804
- // indistinguishable from no deny at all:
127805
- // - TOOL: `createTask` is what this server exposes now; `createLoop`, its retired alias,
127806
- // is kept because the fire connects to whichever `ametyst serve` the host's MCP config
127807
- // points at, which may still be an older build offering it. Denying only the retired
127808
- // name is what the retirement would otherwise leave behind — an inert deny.
127809
- // - ENTRY NAME: this line used to hardcode the `ametyst-staging` prefix, but a PROD build
127810
- // registers as `ametyst` (AMETYST_MCP_NAME), so the guard was silently inert in prod.
127811
- // AMETYST_MCP_NAMES is the repo's own list of every entry name this CLI family writes.
127812
- // `--disallowedTools` is variadic and comma-or-space separated, so one arg carries them all.
127813
- "--disallowedTools",
127814
- AMETYST_MCP_NAMES.flatMap((n) => [`mcp__${n}__createTask`, `mcp__${n}__createLoop`]).join(","),
127815
- "--add-dir",
127816
- dir
127817
- ];
127818
- if (maxBudget !== void 0) {
127819
- args.push("--max-budget-usd", String(maxBudget));
127820
- }
127821
- if (opts.sessionId) {
127822
- args.push("--session-id", opts.sessionId);
127823
- }
127824
- args.push(
127825
- // Eager-load MCP tools: with tool search enabled, MCP tools (including Ametyst's) are
127826
- // deferred behind a ToolSearch step that smaller orchestrator models (e.g. Haiku) never
127827
- // perform — the run ends its turn without the tools (BUG-15). Eager loading is safe for
127828
- // all models, so this is universal rather than model-gated.
127829
- "--settings",
127830
- '{"env":{"ENABLE_TOOL_SEARCH":"false"}}'
127831
- );
127832
- return {
127833
- cmd: "claude",
127834
- args,
127835
- cwd: opts.cwd ?? process.cwd(),
127836
- env: {
127837
- ...deriveGitIdentityEnv(env, deps.readGitConfig ?? readGlobalGitConfig),
127838
- // Loop memory is addressed by SLUG, and the fire context carries no loop
127839
- // identity today — which is why the taskMemory* tools take an explicit
127840
- // `taskSlug`. Exporting it here is the convenience half: the prompt names
127841
- // the slug literally, and this lets any shell step in the loop reach the
127842
- // same value without re-deriving it. Env only, never argv — the launch
127843
- // argv is pinned byte-for-byte by launch.test.ts, and widening it would
127844
- // be a change to the command rather than to the child's environment.
127845
- ...slug ? { AMETYST_LOOP_SLUG: slug } : {}
127846
- }
127847
- };
127848
- }
127849
- function resolveMaxBudgetUsd(opts, env = process.env) {
127850
- if (opts.maxBudgetUsd !== void 0) return opts.maxBudgetUsd;
127851
- const raw = env.AMETYST_LOOP_MAX_BUDGET_USD;
127852
- if (raw === void 0 || raw.trim() === "") return void 0;
127853
- const n = Number(raw);
127854
- return Number.isFinite(n) ? n : void 0;
127855
- }
127856
-
127857
- // src/loops/heartbeat.ts
127985
+ // src/tasks/heartbeat.ts
127858
127986
  init_esm_shims();
127859
127987
  import * as realFs2 from "fs";
127860
127988
  import { join as join21 } from "path";
127861
- function startHeartbeat(loopDir2, info, deps = {}) {
127989
+ function startHeartbeat(runDir2, info, deps = {}) {
127862
127990
  const fs = deps.fs ?? realFs2;
127863
127991
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
127864
127992
  const setI = deps.setInterval ?? globalThis.setInterval;
127865
127993
  const clearI = deps.clearInterval ?? globalThis.clearInterval;
127866
127994
  const intervalMs = deps.intervalMs ?? 6e4;
127867
- const stateDir = join21(loopDir2, ".state");
127995
+ const stateDir = join21(runDir2, ".state");
127868
127996
  const path2 = join21(stateDir, "fire.running");
127869
127997
  try {
127870
127998
  fs.mkdirSync(stateDir, { recursive: true, mode: 448 });
@@ -127901,7 +128029,7 @@ ${info.sessionId ? `session=${info.sessionId}
127901
128029
  };
127902
128030
  }
127903
128031
 
127904
- // src/loops/accounting.ts
128032
+ // src/tasks/accounting.ts
127905
128033
  init_esm_shims();
127906
128034
  import * as realFs3 from "fs";
127907
128035
  import { homedir as homedir13 } from "os";
@@ -127965,21 +128093,21 @@ function recordFireAccounting(args) {
127965
128093
  const stats = parseTranscriptStats(fs.readFileSync(transcript, "utf-8"));
127966
128094
  const rec = {
127967
128095
  ts: new Date(args.startedAtEpochMs).toISOString(),
127968
- loop: args.loopSlug,
128096
+ loop: args.taskSlug,
127969
128097
  session: args.sessionId,
127970
128098
  exit: args.exitCode,
127971
128099
  duration_s: Math.max(0, Math.round((now().getTime() - args.startedAtEpochMs) / 1e3)),
127972
128100
  ...stats
127973
128101
  };
127974
- appendFireLine(fs, args.loopDir, rec);
128102
+ appendFireLine(fs, args.runDir, rec);
127975
128103
  return rec;
127976
128104
  } catch (err) {
127977
128105
  log(` (accounting failed \u2014 fire itself unaffected: ${err instanceof Error ? err.message : String(err)})`);
127978
128106
  return null;
127979
128107
  }
127980
128108
  }
127981
- function appendFireLine(fs, loopDir2, rec) {
127982
- const stateDir = join22(loopDir2, ".state");
128109
+ function appendFireLine(fs, runDir2, rec) {
128110
+ const stateDir = join22(runDir2, ".state");
127983
128111
  fs.mkdirSync(stateDir, { recursive: true, mode: 448 });
127984
128112
  fs.appendFileSync(join22(stateDir, "fires.jsonl"), JSON.stringify(rec) + "\n", { mode: 384 });
127985
128113
  }
@@ -127990,7 +128118,7 @@ function recordFailedLaunch(args) {
127990
128118
  try {
127991
128119
  const rec = {
127992
128120
  ts: new Date(args.startedAtEpochMs).toISOString(),
127993
- loop: args.loopSlug,
128121
+ loop: args.taskSlug,
127994
128122
  session: args.sessionId,
127995
128123
  exit: args.exitCode ?? 1,
127996
128124
  duration_s: Math.max(0, Math.round((now().getTime() - args.startedAtEpochMs) / 1e3)),
@@ -127998,7 +128126,7 @@ function recordFailedLaunch(args) {
127998
128126
  launch_failed: true,
127999
128127
  reason: args.reason
128000
128128
  };
128001
- appendFireLine(fs, args.loopDir, rec);
128129
+ appendFireLine(fs, args.runDir, rec);
128002
128130
  return rec;
128003
128131
  } catch (err) {
128004
128132
  log(` (failed-launch record could not be written: ${err instanceof Error ? err.message : String(err)})`);
@@ -128006,14 +128134,14 @@ function recordFailedLaunch(args) {
128006
128134
  }
128007
128135
  }
128008
128136
 
128009
- // src/loops/run.ts
128137
+ // src/tasks/run.ts
128010
128138
  init_esm_shims();
128011
128139
  import { spawn as spawn2 } from "child_process";
128012
128140
  import { randomUUID as randomUUID5 } from "crypto";
128013
128141
  import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync15, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "fs";
128014
128142
  import { dirname as dirname9, join as join24 } from "path";
128015
128143
 
128016
- // src/loops/claude-binary.ts
128144
+ // src/tasks/claude-binary.ts
128017
128145
  init_esm_shims();
128018
128146
  import { spawnSync as spawnSync2 } from "child_process";
128019
128147
  var defaultRun = (cmd, args) => {
@@ -128060,7 +128188,7 @@ function ensureClaudeBinary(deps = {}) {
128060
128188
  return { status: "healed" };
128061
128189
  }
128062
128190
 
128063
- // src/loops/concurrency.ts
128191
+ // src/tasks/concurrency.ts
128064
128192
  init_esm_shims();
128065
128193
  import * as realFs4 from "fs";
128066
128194
  import { join as join23 } from "path";
@@ -128080,21 +128208,21 @@ function parseHeartbeatPid(body) {
128080
128208
  const pid = Number(m[1]);
128081
128209
  return Number.isInteger(pid) && pid > 0 ? pid : void 0;
128082
128210
  }
128083
- function liveFires(loopRoot, deps = {}) {
128211
+ function liveFires(taskRoot, deps = {}) {
128084
128212
  const fs = deps.fs ?? realFs4;
128085
128213
  const isAlive = deps.isAlive ?? pidIsAlive;
128086
128214
  const now = deps.now ?? Date.now;
128087
128215
  const staleMs = deps.staleMs ?? DEFAULT_HEARTBEAT_STALE_MS;
128088
128216
  let entries;
128089
128217
  try {
128090
- entries = fs.readdirSync(join23(loopRoot, "fires"));
128218
+ entries = fs.readdirSync(join23(taskRoot, "fires"));
128091
128219
  } catch {
128092
128220
  return [];
128093
128221
  }
128094
128222
  const out = [];
128095
128223
  for (const entry of entries) {
128096
128224
  try {
128097
- const beat = join23(loopRoot, "fires", String(entry), ".state", "fire.running");
128225
+ const beat = join23(taskRoot, "fires", String(entry), ".state", "fire.running");
128098
128226
  const st = fs.statSync(beat);
128099
128227
  const heartbeatAgeMs = now() - Number(st.mtimeMs);
128100
128228
  if (!(heartbeatAgeMs <= staleMs)) continue;
@@ -128109,16 +128237,20 @@ function liveFires(loopRoot, deps = {}) {
128109
128237
  }
128110
128238
  function resolveMaxConcurrentFires(opts = {}, env = process.env) {
128111
128239
  const explicit = opts.maxConcurrentFires;
128112
- const raw = env.AMETYST_LOOP_MAX_CONCURRENT_FIRES;
128240
+ const raw = readTaskEnv("MAX_CONCURRENT_FIRES", env);
128113
128241
  const fromEnv = raw !== void 0 && raw.trim() !== "" ? Number(raw) : void 0;
128114
128242
  const picked = explicit !== void 0 && Number.isFinite(explicit) ? explicit : fromEnv !== void 0 && Number.isFinite(fromEnv) ? fromEnv : DEFAULT_MAX_CONCURRENT_FIRES;
128115
128243
  if (picked <= 0) return Infinity;
128116
128244
  return Math.floor(picked);
128117
128245
  }
128118
128246
 
128119
- // src/loops/run.ts
128247
+ // src/tasks/run.ts
128120
128248
  init_paths();
128121
128249
  var SHIPBACK_SIGNAL_TIMEOUT_MS = 8e3;
128250
+ function isForbidden(res) {
128251
+ if (res.code === 403) return true;
128252
+ return /\bHTTP 403\b|forbidden|only the creator/i.test(res.error ?? "");
128253
+ }
128122
128254
  function preserveRefusedConstraints(slug, fireId, body, runRoot) {
128123
128255
  try {
128124
128256
  const path2 = refusedConstraintsPath(slug, fireId, runRoot);
@@ -128141,72 +128273,74 @@ function killTree(pid) {
128141
128273
  }
128142
128274
  }, 2e3).unref?.();
128143
128275
  }
128144
- async function runLoop(loopId, opts = {}) {
128276
+ async function runTask(taskId, opts = {}) {
128145
128277
  const { sdk, apiKey } = await getCliSdk();
128146
- const got = await sdk.loops.get(apiKey, loopId);
128147
- if (got.status !== "ok") throw new Error(`loop not found: ${got.error}`);
128148
- const loop2 = got.loop;
128278
+ const got = await sdk.loops.get(apiKey, taskId);
128279
+ if (got.status !== "ok") throw new Error(`task not found: ${got.error}`);
128280
+ const task = got.loop;
128149
128281
  const runRoot = resolveRunRoot();
128150
128282
  if (runRoot.reason === "fallback") {
128151
128283
  const why = runRoot.rejected?.map((r) => `${r.dir}: ${r.why}`).join("; ") ?? "no usable cwd";
128152
- console.log(`Loop ${loop2.slug}: running under ${runRoot.root} \u2014 the current folder cannot host a run (${why}).`);
128284
+ console.log(`Task ${task.slug}: running under ${runRoot.root} \u2014 the current folder cannot host a run (${why}).`);
128153
128285
  }
128154
- const loopRoot = loopDir(loop2.slug, runRoot.root);
128286
+ const taskRoot = runDir(task.slug, runRoot.root);
128287
+ const legacyHint = legacyRunFolderHint(task.slug, runRoot.root);
128288
+ if (legacyHint) console.log(legacyHint);
128155
128289
  const maxConcurrent = resolveMaxConcurrentFires(opts, process.env);
128156
- const inFlight = liveFires(loopRoot);
128290
+ const inFlight = liveFires(taskRoot);
128157
128291
  if (inFlight.length >= maxConcurrent) {
128158
128292
  console.log(
128159
- `Loop ${loop2.slug}: standing down \u2014 ${inFlight.length} fire(s) already in flight (pids ${inFlight.map((f) => f.pid).join(", ")}), ceiling ${maxConcurrent}. Raise it with --max-concurrent-fires / AMETYST_LOOP_MAX_CONCURRENT_FIRES (0 = unlimited).`
128293
+ `Task ${task.slug}: standing down \u2014 ${inFlight.length} fire(s) already in flight (pids ${inFlight.map((f) => f.pid).join(", ")}), ceiling ${maxConcurrent}. Raise it with --max-concurrent-fires / AMETYST_TASK_MAX_CONCURRENT_FIRES (0 = unlimited).`
128160
128294
  );
128161
- return { status: "skipped", dir: loopRoot, shipBack: { outcome: "nothing" } };
128295
+ return { status: "skipped", dir: taskRoot, shipBack: { outcome: "nothing" } };
128162
128296
  }
128163
- const est = estimateBlastRadius(loop2);
128297
+ const est = estimateBlastRadius(task);
128164
128298
  const sessionId2 = opts.sessionId ?? randomUUID5();
128165
- const discovered = await discoverMemoryDocs(sdk, apiKey, loop2.slug);
128299
+ const discovered = await discoverMemoryDocs(sdk, apiKey, task.slug);
128166
128300
  for (const scope of discovered.failed) {
128167
128301
  console.error(
128168
- `Loop ${loop2.slug}: could not list the ${scope === "member" ? "seat's own" : "shared"} memory docs \u2014 any undeclared doc that lives only there is NOT materialized this fire (declared docs are still resolved individually from the manifest), and nothing is overwritten.`
128302
+ `Task ${task.slug}: could not list the ${scope === "member" ? "seat's own" : "shared"} memory docs \u2014 any undeclared doc that lives only there is NOT materialized this fire (declared docs are still resolved individually from the manifest), and nothing is overwritten.`
128169
128303
  );
128170
128304
  }
128171
128305
  for (const scope of discovered.truncated) {
128172
128306
  console.error(
128173
- `Loop ${loop2.slug}: the ${scope === "member" ? "seat's own" : "shared"} memory holds at least ${(scope === "member" ? discovered.own : discovered.shared).length} docs and the listing has no cursor \u2014 undeclared docs past the first page are NOT materialized this fire.`
128307
+ `Task ${task.slug}: the ${scope === "member" ? "seat's own" : "shared"} memory holds at least ${(scope === "member" ? discovered.own : discovered.shared).length} docs and the listing has no cursor \u2014 undeclared docs past the first page are NOT materialized this fire.`
128174
128308
  );
128175
128309
  }
128176
- const plan = planStateDocs(loop2.stateDocs, discovered);
128310
+ const plan = planStateDocs(task.stateDocs, discovered);
128177
128311
  if (plan.skipped.length > 0) {
128178
128312
  console.error(
128179
- `Loop ${loop2.slug}: IGNORING ${plan.skipped.length} memory doc(s) (${plan.skipped.map((d) => `${d.key}: ${d.reason}`).join("; ")}). They are NOT materialized and NOT shipped back \u2014 anything this fire writes to those filenames is the wrapper's own, and would overwrite the record if sent up.`
128313
+ `Task ${task.slug}: IGNORING ${plan.skipped.length} memory doc(s) (${plan.skipped.map((d) => `${d.key}: ${d.reason}`).join("; ")}). They are NOT materialized and NOT shipped back \u2014 anything this fire writes to those filenames is the wrapper's own, and would overwrite the record if sent up.`
128180
128314
  );
128181
128315
  }
128182
- const { fetched: stateDocBodies, failed: stateDocFailures } = plan.docs.length > 0 ? await fetchStateDocs(sdk, apiKey, loop2.slug, plan.docs) : { fetched: [], failed: [] };
128316
+ const { fetched: stateDocBodies, failed: stateDocFailures } = plan.docs.length > 0 ? await fetchStateDocs(sdk, apiKey, task.slug, plan.docs) : { fetched: [], failed: [] };
128183
128317
  if (stateDocFailures.length > 0) {
128184
128318
  console.error(
128185
- `Loop ${loop2.slug}: could not read or create ${stateDocFailures.length} memory doc(s) (${stateDocFailures.map((f) => `${f.key}: ${f.reason}`).join("; ")}) \u2014 they are NOT materialized this fire, and their records are left untouched rather than overwritten with an empty file.`
128319
+ `Task ${task.slug}: could not read or create ${stateDocFailures.length} memory doc(s) (${stateDocFailures.map((f) => `${f.key}: ${f.reason}`).join("; ")}) \u2014 they are NOT materialized this fire, and their records are left untouched rather than overwritten with an empty file.`
128186
128320
  );
128187
128321
  }
128188
128322
  if (stateDocBodies.length > 0) {
128189
- console.log(`Loop ${loop2.slug}: state docs \u2192 ${stateDocBodies.map(describeSource).join(", ")}`);
128323
+ console.log(`Task ${task.slug}: state docs \u2192 ${stateDocBodies.map(describeSource).join(", ")}`);
128190
128324
  }
128191
128325
  const undeclared = stateDocBodies.filter((d) => !d.declared);
128192
128326
  if (undeclared.length > 0) {
128193
128327
  console.log(
128194
- `Loop ${loop2.slug}: ${undeclared.length} memory doc(s) not in manifest (${undeclared.map((d) => `${d.key} \u2190 ${d.scope}`).join(", ")}) \u2014 materialized anyway, and shipped back to the namespace each came from. Declare them in the task's stateDocs manifest to make that explicit.`
128328
+ `Task ${task.slug}: ${undeclared.length} memory doc(s) not in manifest (${undeclared.map((d) => `${d.key} \u2190 ${d.scope}`).join(", ")}) \u2014 materialized anyway, and shipped back to the namespace each came from. Declare them in the task's stateDocs manifest to make that explicit.`
128195
128329
  );
128196
128330
  }
128197
- const dir = materialize(loop2, sessionId2, stateDocBodies, runRoot.root);
128198
- console.log(`Loop ${loop2.slug}: fire ${sessionId2} \u2192 ${dir}`);
128331
+ const dir = materialize(task, sessionId2, stateDocBodies, runRoot.root);
128332
+ console.log(`Task ${task.slug}: fire ${sessionId2} \u2192 ${dir}`);
128199
128333
  const launch = buildLaunchArgs(
128200
128334
  dir,
128201
128335
  // The child runs IN the resolved root: for a project cwd that is the cwd it always was; under
128202
128336
  // the fallback it is the writable folder rather than the `/` launchd handed us.
128203
- { ...opts, sessionId: sessionId2, cwd: opts.cwd ?? runRoot.root, memoryManifest: normalizeMemoryManifest(loop2.stateDocs) },
128204
- loop2.slug
128337
+ { ...opts, sessionId: sessionId2, cwd: opts.cwd ?? runRoot.root, memoryManifest: normalizeMemoryManifest(task.stateDocs) },
128338
+ task.slug
128205
128339
  );
128206
128340
  const budget = resolveMaxBudgetUsd(opts);
128207
128341
  const capLabel = budget !== void 0 ? `capped at \u20AC${budget}` : "no external budget cap";
128208
128342
  console.log(
128209
- `Loop ${loop2.slug}: ${est.steps} steps (${est.paidSteps} paid), est \u20AC${est.estCostEur ?? "?"} \u2014 ${capLabel}`
128343
+ `Task ${task.slug}: ${est.steps} steps (${est.paidSteps} paid), est \u20AC${est.estCostEur ?? "?"} \u2014 ${capLabel}`
128210
128344
  );
128211
128345
  const statusPath = join24(dir, "STATUS.md");
128212
128346
  const statusBefore = existsSync16(statusPath) ? readFileSync15(statusPath, "utf-8") : "";
@@ -128220,10 +128354,14 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
128220
128354
  { mode: 384 }
128221
128355
  );
128222
128356
  const heartbeat = startHeartbeat(dir, { pid: process.pid, sessionId: sessionId2 });
128223
- const dashboard = await startDashboardServer({ loop: loop2, loopDir: dir });
128357
+ const dashboard = await startDashboardServer({
128358
+ task,
128359
+ runDir: dir,
128360
+ readDoc: memoryDocResolver(sdk, apiKey, task.slug, normalizeMemoryManifest(task.stateDocs))
128361
+ });
128224
128362
  if (dashboard) {
128225
128363
  openDashboardInBrowser(`http://localhost:${dashboard.port}`, { isTTY: Boolean(process.stdout.isTTY) });
128226
- } else if (typeof loop2.dashboardHtml !== "string" || !loop2.dashboardHtml) {
128364
+ } else if (typeof task.dashboardHtml !== "string" || !task.dashboardHtml) {
128227
128365
  console.log(NO_DASHBOARD_MESSAGE);
128228
128366
  }
128229
128367
  const startedAtEpochMs = Date.now();
@@ -128231,7 +128369,7 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
128231
128369
  ensureClaudeBinary();
128232
128370
  } catch (err) {
128233
128371
  const reason = err instanceof Error ? err.message : String(err);
128234
- recordFailedLaunch({ loopDir: dir, loopSlug: loop2.slug, sessionId: sessionId2, reason, startedAtEpochMs });
128372
+ recordFailedLaunch({ runDir: dir, taskSlug: task.slug, sessionId: sessionId2, reason, startedAtEpochMs });
128235
128373
  heartbeat.stop();
128236
128374
  dashboard?.close();
128237
128375
  throw err;
@@ -128252,17 +128390,17 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
128252
128390
  async function shipDocs() {
128253
128391
  if (stateDocBodies.length === 0) return;
128254
128392
  try {
128255
- const outcomes = await shipBackStateDocs(sdk, apiKey, loop2.slug, dir, stateDocBodies);
128393
+ const outcomes = await shipBackStateDocs(sdk, apiKey, task.slug, dir, stateDocBodies);
128256
128394
  for (const o of outcomes) {
128257
128395
  if (o.outcome === "shipped") {
128258
- console.log(`Loop ${loop2.slug}: shipped back state doc '${o.key}'.`);
128396
+ console.log(`Task ${task.slug}: shipped back state doc '${o.key}'.`);
128259
128397
  } else if (o.outcome === "refused" || o.outcome === "failed") {
128260
- console.error(`Loop ${loop2.slug}: state doc '${o.key}' ${o.outcome} \u2014 ${o.detail}`);
128398
+ console.error(`Task ${task.slug}: state doc '${o.key}' ${o.outcome} \u2014 ${o.detail}`);
128261
128399
  }
128262
128400
  }
128263
128401
  } catch (err) {
128264
128402
  console.error(
128265
- `Loop ${loop2.slug}: state-doc ship-back failed: ${err instanceof Error ? err.message : String(err)}`
128403
+ `Task ${task.slug}: state-doc ship-back failed: ${err instanceof Error ? err.message : String(err)}`
128266
128404
  );
128267
128405
  }
128268
128406
  }
@@ -128271,36 +128409,51 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
128271
128409
  const constraintsPath = join24(dir, "CONSTRAINTS.md");
128272
128410
  const materializedConstraints = existsSync16(constraintsPath) ? readFileSync15(constraintsPath, "utf-8") : void 0;
128273
128411
  if (materializedConstraints === void 0) return;
128274
- const boot = loop2.constraintsMd ?? "";
128412
+ const boot = task.constraintsMd ?? "";
128275
128413
  for (let attempt = 1; attempt <= 2; attempt++) {
128276
- const reread = await sdk.loops.get(apiKey, loopId);
128414
+ const reread = await sdk.loops.get(apiKey, taskId);
128277
128415
  if (reread.status !== "ok") {
128278
128416
  constraintsReport = { outcome: "failed" };
128279
128417
  console.error(
128280
- `Loop ${loop2.slug}: ship-back ABORTED \u2014 could not re-read the record (${reread.error}). Refusing to merge against the stale boot snapshot; this fire's CONSTRAINTS.md is kept at ${constraintsPath} for recovery.`
128418
+ `Task ${task.slug}: ship-back ABORTED \u2014 could not re-read the record (${reread.error}). Refusing to merge against the stale boot snapshot; this fire's CONSTRAINTS.md is kept at ${constraintsPath} for recovery.`
128281
128419
  );
128282
128420
  return;
128283
128421
  }
128284
128422
  const fresh = reread.loop.constraintsMd ?? "";
128285
128423
  const merged = mergeConstraints(boot, materializedConstraints, fresh);
128286
128424
  if (merged.refusal) {
128287
- const kept = preserveRefusedConstraints(loop2.slug, sessionId2, materializedConstraints, runRoot.root);
128425
+ const kept = preserveRefusedConstraints(task.slug, sessionId2, materializedConstraints, runRoot.root);
128288
128426
  constraintsReport = { outcome: "refused", keptAt: kept.path ?? constraintsPath };
128289
128427
  console.error(
128290
- `Loop ${loop2.slug}: ${merged.refusal} This fire's constraints were NOT discarded: its folder is KEPT at ${dir}` + (kept.path ? `, and the refused document is copied to ${kept.path}.` : ` \u2014 but the durable copy could NOT be written (${kept.error}), so that folder is the only place it survives.`)
128428
+ `Task ${task.slug}: ${merged.refusal} This fire's constraints were NOT discarded: its folder is KEPT at ${dir}` + (kept.path ? `, and the refused document is copied to ${kept.path}.` : ` \u2014 but the durable copy could NOT be written (${kept.error}), so that folder is the only place it survives.`)
128291
128429
  );
128292
128430
  return;
128293
128431
  }
128294
128432
  if (merged.next === void 0) return;
128295
128433
  if (!merged.cleanAppend && attempt === 1) {
128296
128434
  console.warn(
128297
- `Loop ${loop2.slug}: this fire edited existing constraints text, not only appended to it \u2014 shipping its own document. No section was lost (the shrink guard passed).`
128435
+ `Task ${task.slug}: this fire edited existing constraints text, not only appended to it \u2014 shipping its own document. No section was lost (the shrink guard passed).`
128436
+ );
128437
+ }
128438
+ const put = await sdk.loops.update(apiKey, taskId, { constraintsMd: merged.next });
128439
+ if (put?.status !== "ok") {
128440
+ if (isForbidden(put ?? { status: "nok" })) {
128441
+ const owner = typeof task.createdBy === "string" ? task.createdBy : "someone else";
128442
+ constraintsReport = { outcome: "skipped-not-owner", owner };
128443
+ console.log(
128444
+ `Task ${task.slug}: ship-back skipped: this task is owned by ${owner}; your learnings stay in the run diary`
128445
+ );
128446
+ return;
128447
+ }
128448
+ constraintsReport = { outcome: "failed" };
128449
+ console.error(
128450
+ `Task ${task.slug}: ship-back failed: the record refused the write (${put?.error ?? "unknown error"}). This fire's CONSTRAINTS.md is kept at ${constraintsPath} for recovery.`
128298
128451
  );
128452
+ return;
128299
128453
  }
128300
- await sdk.loops.update(apiKey, loopId, { constraintsMd: merged.next });
128301
- const after = await sdk.loops.get(apiKey, loopId);
128454
+ const after = await sdk.loops.get(apiKey, taskId);
128302
128455
  if (after.status !== "ok") {
128303
- console.warn(`Loop ${loop2.slug}: shipped back, but could not verify it landed.`);
128456
+ console.warn(`Task ${task.slug}: shipped back, but could not verify it landed.`);
128304
128457
  return;
128305
128458
  }
128306
128459
  const afterText = after.loop.constraintsMd ?? "";
@@ -128309,28 +128462,28 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
128309
128462
  constraintsReport = { outcome: "shipped" };
128310
128463
  if (merged.added.length > 0) {
128311
128464
  console.log(
128312
- `Loop ${loop2.slug}: shipped back ${merged.added.length} new constraints section(s).`
128465
+ `Task ${task.slug}: shipped back ${merged.added.length} new constraints section(s).`
128313
128466
  );
128314
128467
  } else {
128315
- console.log(`Loop ${loop2.slug}: shipped back a constraints rewrite (no new sections).`);
128468
+ console.log(`Task ${task.slug}: shipped back a constraints rewrite (no new sections).`);
128316
128469
  }
128317
128470
  return;
128318
128471
  }
128319
128472
  if (attempt === 2) {
128320
128473
  constraintsReport = { outcome: "failed" };
128321
128474
  console.error(
128322
- `Loop ${loop2.slug}: ship-back RACED and could not be repaired \u2014 ${missing.length} section(s) did not survive a concurrent write: ${missing.join(", ")}. They remain at ${constraintsPath}.`
128475
+ `Task ${task.slug}: ship-back RACED and could not be repaired \u2014 ${missing.length} section(s) did not survive a concurrent write: ${missing.join(", ")}. They remain at ${constraintsPath}.`
128323
128476
  );
128324
128477
  return;
128325
128478
  }
128326
128479
  console.warn(
128327
- `Loop ${loop2.slug}: a concurrent write clobbered ${missing.length} of our sections; retrying the merge.`
128480
+ `Task ${task.slug}: a concurrent write clobbered ${missing.length} of our sections; retrying the merge.`
128328
128481
  );
128329
128482
  }
128330
128483
  } catch (err) {
128331
128484
  constraintsReport = { outcome: "failed" };
128332
128485
  console.error(
128333
- `Loop ${loop2.slug}: ship-back failed: ${err instanceof Error ? err.message : String(err)}`
128486
+ `Task ${task.slug}: ship-back failed: ${err instanceof Error ? err.message : String(err)}`
128334
128487
  );
128335
128488
  }
128336
128489
  }
@@ -128344,7 +128497,7 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
128344
128497
  void Promise.race([shipBackConstraints().then(() => "shipped"), deadline]).then((outcome) => {
128345
128498
  if (outcome === "timeout") {
128346
128499
  console.error(
128347
- `Loop ${loop2.slug}: the ship-back did not finish within ${SHIPBACK_SIGNAL_TIMEOUT_MS}ms of the signal \u2014 exiting anyway so the shutdown is not held open. Nothing was discarded: this fire's rules remain at ${join24(dir, "CONSTRAINTS.md")}, and any state doc that did not get shipped remains beside it in ${dir}. The next fire can recover them by hand from there \u2014 nothing in the CLI reads a prior fire's folder automatically. \u26D4 Constraints are shipped FIRST, so the state docs are the likelier victim of this deadline \u2014 though a constraints ship-back that alone exceeds it truncates the rules too.`
128500
+ `Task ${task.slug}: the ship-back did not finish within ${SHIPBACK_SIGNAL_TIMEOUT_MS}ms of the signal \u2014 exiting anyway so the shutdown is not held open. Nothing was discarded: this fire's rules remain at ${join24(dir, "CONSTRAINTS.md")}, and any state doc that did not get shipped remains beside it in ${dir}. The next fire can recover them by hand from there \u2014 nothing in the CLI reads a prior fire's folder automatically. \u26D4 Constraints are shipped FIRST, so the state docs are the likelier victim of this deadline \u2014 though a constraints ship-back that alone exceeds it truncates the rules too.`
128348
128501
  );
128349
128502
  }
128350
128503
  }).finally(() => process.exit(130));
@@ -128363,8 +128516,8 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
128363
128516
  heartbeat.stop();
128364
128517
  dashboard?.close();
128365
128518
  const accounted = recordFireAccounting({
128366
- loopDir: dir,
128367
- loopSlug: loop2.slug,
128519
+ runDir: dir,
128520
+ taskSlug: task.slug,
128368
128521
  sessionId: sessionId2,
128369
128522
  cwd: launch.cwd,
128370
128523
  exitCode,
@@ -128372,8 +128525,8 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
128372
128525
  });
128373
128526
  if (!accounted) {
128374
128527
  recordFailedLaunch({
128375
- loopDir: dir,
128376
- loopSlug: loop2.slug,
128528
+ runDir: dir,
128529
+ taskSlug: task.slug,
128377
128530
  sessionId: sessionId2,
128378
128531
  reason: spawnError ? `spawn failed: ${spawnError}` : `the fire produced no transcript (exit ${exitCode}) \u2014 it never started`,
128379
128532
  exitCode,
@@ -128388,7 +128541,7 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
128388
128541
  if (clean2) {
128389
128542
  if (constraintsReport.outcome === "refused") {
128390
128543
  console.log(
128391
- `Loop ${loop2.slug}: folder KEPT at ${dir} \u2014 the constraints ship-back was refused, so this fire's rules exist nowhere else. Recovery copy: ${constraintsReport.keptAt}.`
128544
+ `Task ${task.slug}: folder KEPT at ${dir} \u2014 the constraints ship-back was refused, so this fire's rules exist nowhere else. Recovery copy: ${constraintsReport.keptAt}.`
128392
128545
  );
128393
128546
  return { status: "clean", dir, shipBack: constraintsReport };
128394
128547
  }
@@ -128398,24 +128551,24 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
128398
128551
  return { status: "dirty", dir, shipBack: constraintsReport };
128399
128552
  }
128400
128553
 
128401
- // src/loops/show.ts
128554
+ // src/tasks/show.ts
128402
128555
  init_esm_shims();
128403
- function formatLoopFiles(loop2) {
128556
+ function formatTaskFiles(task) {
128404
128557
  const section = (title, body) => `
128405
128558
  ===== ${title} =====
128406
128559
  ${(body ?? "").trim() || "(empty)"}
128407
128560
  `;
128408
- return `Loop: ${loop2.slug} (${loop2.id})
128409
- ` + section("SKILL.md", loop2.markdownBody) + section("VISION.md", loop2.visionMd) + section("CONSTRAINTS.md", loop2.constraintsMd) + section("README.md", loop2.readmeMd) + section("stateDocs", formatMemoryManifest(loop2.stateDocs));
128561
+ return `Task: ${task.slug} (${task.id})
128562
+ ` + section("SKILL.md", task.markdownBody) + section("VISION.md", task.visionMd) + section("CONSTRAINTS.md", task.constraintsMd) + section("README.md", task.readmeMd) + section("stateDocs", formatMemoryManifest(task.stateDocs));
128410
128563
  }
128411
- async function showLoop(loopId) {
128564
+ async function showTask(taskId) {
128412
128565
  const { sdk, apiKey } = await getCliSdk();
128413
- const got = await sdk.loops.get(apiKey, loopId);
128414
- if (got.status !== "ok") throw new Error(`loop not found: ${got.error}`);
128415
- console.log(formatLoopFiles(got.loop));
128566
+ const got = await sdk.loops.get(apiKey, taskId);
128567
+ if (got.status !== "ok") throw new Error(`task not found: ${got.error}`);
128568
+ console.log(formatTaskFiles(got.loop));
128416
128569
  }
128417
128570
 
128418
- // src/loops/schedule.ts
128571
+ // src/tasks/schedule.ts
128419
128572
  init_esm_shims();
128420
128573
  init_paths();
128421
128574
  import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync12, rmSync as rmSync3, existsSync as existsSync17, readdirSync as readdirSync6, readFileSync as readFileSync16, accessSync as accessSync4, constants as constants4 } from "fs";
@@ -128431,7 +128584,7 @@ var LOOP_KIND = {
128431
128584
  return args;
128432
128585
  },
128433
128586
  stateDir(slug, cwd) {
128434
- return join25(cwd, `.ametyst${ENV_SUFFIX}`, "loops", safeSlug2(slug, this.noun), ".state");
128587
+ return join25(cwd, `.ametyst${ENV_SUFFIX}`, LEGACY_RUNS_SEGMENT, safeSlug2(slug, this.noun), ".state");
128435
128588
  }
128436
128589
  };
128437
128590
  var TASK_KIND = {
@@ -128440,10 +128593,11 @@ var TASK_KIND = {
128440
128593
  buildRunCmd(slug, opts) {
128441
128594
  const args = [process.execPath, process.argv[1], "task", "run", slug];
128442
128595
  if (opts.maxBudgetUsd != null) args.push("--max-budget-usd", String(opts.maxBudgetUsd));
128596
+ if (typeof opts.input === "string" && opts.input.trim() !== "") args.push("--input", opts.input);
128443
128597
  return args;
128444
128598
  },
128445
128599
  stateDir(slug, cwd) {
128446
- return join25(cwd, `.ametyst${ENV_SUFFIX}`, "loops", safeSlug2(slug, this.noun), ".state");
128600
+ return join25(cwd, `.ametyst${ENV_SUFFIX}`, RUNS_SEGMENT, safeSlug2(slug, this.noun), ".state");
128447
128601
  }
128448
128602
  };
128449
128603
  var COMPOUND_KIND = {
@@ -128564,11 +128718,11 @@ function cadenceLabel(opts) {
128564
128718
  function escapeXml(s) {
128565
128719
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
128566
128720
  }
128567
- function shellQuote2(s) {
128721
+ function shellQuote3(s) {
128568
128722
  return `'${s.replace(/'/g, `'\\''`)}'`;
128569
128723
  }
128570
128724
  function shellArg(s) {
128571
- return /^[A-Za-z0-9_@+=:,./-]+$/.test(s) ? s : shellQuote2(s);
128725
+ return /^[A-Za-z0-9_@+=:,./-]+$/.test(s) ? s : shellQuote3(s);
128572
128726
  }
128573
128727
  function cronEscapePercent(s) {
128574
128728
  return s.replace(/%/g, "\\%");
@@ -128653,6 +128807,10 @@ function schedule(kind, slug, opts = {}) {
128653
128807
  const why = (resolved.rejected ?? []).map((r) => `${r.dir}: ${r.why}`).join("; ");
128654
128808
  console.warn(`\u26A0\uFE0F the job will run in ${cwd} \u2014 the current folder cannot host a run (${why}).`);
128655
128809
  }
128810
+ if (kind === TASK_KIND) {
128811
+ const legacyHint = legacyRunFolderHint(slug, cwd);
128812
+ if (legacyHint) console.warn(legacyHint);
128813
+ }
128656
128814
  const procEnv = opts.env ?? process.env;
128657
128815
  const envPath = procEnv.PATH ?? "";
128658
128816
  const model = resolveScheduledModel(opts, home, procEnv);
@@ -128704,9 +128862,9 @@ launchctl said: ${loaded.output.trim()}` : "")
128704
128862
  if (model) cronEnv.ANTHROPIC_MODEL = model;
128705
128863
  Object.assign(cronEnv, extraEnv);
128706
128864
  const effectiveCronModel = cronEnv.ANTHROPIC_MODEL;
128707
- const envPrefix = Object.entries(cronEnv).map(([k, v]) => `${k}=${shellQuote2(v)}`).join(" ");
128865
+ const envPrefix = Object.entries(cronEnv).map(([k, v]) => `${k}=${shellQuote3(v)}`).join(" ");
128708
128866
  const cmd = launcherArgv(args, stateDir).map(shellArg).join(" ");
128709
- const command = cronEscapePercent(`cd ${shellQuote2(cwd)} && ${envPrefix} ${cmd}`);
128867
+ const command = cronEscapePercent(`cd ${shellQuote3(cwd)} && ${envPrefix} ${cmd}`);
128710
128868
  const line = `${cronField(opts)} ${command} # ${lbl}`;
128711
128869
  const kept = stripLabel(readCrontab(), lbl);
128712
128870
  kept.push(line);
@@ -128845,114 +129003,14 @@ function unscheduleEverywhere(slug, opts = {}) {
128845
129003
  }
128846
129004
  return removed;
128847
129005
  }
128848
- function scheduleLoop(slug, opts = {}) {
128849
- return schedule(LOOP_KIND, slug, opts);
128850
- }
128851
- function unscheduleLoop(slug, opts = {}) {
128852
- unschedule(LOOP_KIND, slug, opts);
128853
- }
128854
- function listScheduleEntries(opts = {}) {
128855
- return listEntries(LOOP_KIND, opts);
128856
- }
128857
- function scheduleCompound(slug, opts = {}) {
128858
- return schedule(COMPOUND_KIND, slug, opts);
128859
- }
128860
- function unscheduleCompound(slug, opts = {}) {
128861
- unschedule(COMPOUND_KIND, slug, opts);
128862
- }
128863
- function listCompoundSchedules(opts = {}) {
128864
- return list(COMPOUND_KIND, opts);
128865
- }
128866
-
128867
- // src/commands/task-verbs.ts
128868
- init_esm_shims();
128869
-
128870
- // src/compounds/index.ts
128871
- init_esm_shims();
128872
-
128873
- // src/compounds/run.ts
128874
- init_esm_shims();
128875
- import { spawn as spawn3 } from "child_process";
128876
-
128877
- // src/compounds/launch.ts
128878
- init_esm_shims();
128879
- function buildCompoundLaunchArgs(body, opts = {}, slug) {
128880
- const maxBudget = resolveCompoundMaxBudgetUsd(opts);
128881
- const prompt = `You are running the Ametyst compound skill${slug ? ` "${slug}"` : ""} headless and unattended.
128882
-
128883
- Follow these instructions exactly, then STOP (a compound is a one-shot skill \u2014 run it once, do not loop):
128884
-
128885
- ${body}
128886
-
128887
- For any step that costs money, use the Ametyst \`spend\` MCP tool (the on-chain policy enforces the budget) \u2014 do NOT invent another payment path. When you have completed the instructions, exit.`;
128888
- const args = [
128889
- "-p",
128890
- prompt,
128891
- "--dangerously-skip-permissions",
128892
- "--add-dir",
128893
- process.cwd()
128894
- ];
128895
- if (maxBudget !== void 0) {
128896
- args.push("--max-budget-usd", String(maxBudget));
128897
- }
128898
- args.push("--settings", '{"env":{"ENABLE_TOOL_SEARCH":"false"}}');
128899
- return {
128900
- cmd: "claude",
128901
- args,
128902
- cwd: process.cwd()
128903
- };
128904
- }
128905
- function resolveCompoundMaxBudgetUsd(opts, env = process.env) {
128906
- if (opts.maxBudgetUsd !== void 0) return opts.maxBudgetUsd;
128907
- const raw = env.AMETYST_COMPOUND_MAX_BUDGET_USD;
128908
- if (raw === void 0 || raw.trim() === "") return void 0;
128909
- const n = Number(raw);
128910
- return Number.isFinite(n) ? n : void 0;
128911
- }
128912
-
128913
- // src/compounds/run.ts
128914
- function killTree2(pid) {
128915
- try {
128916
- process.kill(-pid, "SIGTERM");
128917
- } catch {
128918
- }
128919
- setTimeout(() => {
128920
- try {
128921
- process.kill(-pid, "SIGKILL");
128922
- } catch {
128923
- }
128924
- }, 2e3).unref?.();
128925
- }
128926
- async function runCompound(compoundId, opts = {}) {
128927
- const { sdk, apiKey } = await getCliSdk();
128928
- const got = await sdk.compoundedSkills.get(apiKey, compoundId);
128929
- if (got.status !== "ok") throw new Error(`compound not found: ${got.error}`);
128930
- const compound = got.skill;
128931
- const launch = buildCompoundLaunchArgs(compound.markdownBody ?? "", opts, compound.slug);
128932
- const child = spawn3(launch.cmd, launch.args, { cwd: launch.cwd, stdio: "inherit", detached: true });
128933
- const onSignal = () => {
128934
- if (child.pid) killTree2(child.pid);
128935
- process.exit(130);
128936
- };
128937
- process.on("SIGINT", onSignal);
128938
- process.on("SIGTERM", onSignal);
128939
- const exitCode = await new Promise((resolve3) => {
128940
- child.on("exit", (code) => resolve3(code ?? 1));
128941
- child.on("error", () => resolve3(1));
128942
- });
128943
- process.off("SIGINT", onSignal);
128944
- process.off("SIGTERM", onSignal);
128945
- if (child.pid) killTree2(child.pid);
128946
- return { exitCode };
128947
- }
128948
129006
 
128949
- // src/compounds/push.ts
129007
+ // src/tasks/push.ts
128950
129008
  init_esm_shims();
128951
- async function pushCompoundFromFile(filePath, opts) {
129009
+ async function pushTaskFromFile(filePath, opts) {
128952
129010
  const slug = (opts.slug ?? "").trim();
128953
129011
  const descriptionShort = (opts.descriptionShort ?? "").trim();
128954
- if (!slug) throw new Error("A --slug is required to push a compound.");
128955
- if (!descriptionShort) throw new Error("A --description is required to push a compound.");
129012
+ if (!slug) throw new Error("A --slug is required to push a task.");
129013
+ if (!descriptionShort) throw new Error("A --description is required to push a task.");
128956
129014
  const markdownBody = readMarkdownFile(filePath);
128957
129015
  let graphJson = {};
128958
129016
  if (opts.graphJson && opts.graphJson.trim()) {
@@ -128967,23 +129025,20 @@ async function pushCompoundFromFile(filePath, opts) {
128967
129025
  if (opts.category && opts.category.trim()) body.category = opts.category.trim();
128968
129026
  const { sdk, apiKey } = await getCliSdk();
128969
129027
  const id = opts.id && opts.id.trim() ? opts.id.trim() : void 0;
128970
- const res = id ? await sdk.compoundedSkills.update(apiKey, id, body) : await sdk.compoundedSkills.create(apiKey, body);
129028
+ const res = id ? await sdk.tasks.update(apiKey, id, body) : await sdk.tasks.create(apiKey, body);
128971
129029
  if (res.status !== "ok") {
128972
129030
  throw new Error(`push failed: ${res.error ?? "unknown error"}${res.code ? ` (${res.code})` : ""}`);
128973
129031
  }
128974
- return { mode: id ? "modified" : "created", compound: res.skill };
129032
+ return { mode: id ? "modified" : "created", task: res.task };
128975
129033
  }
128976
129034
 
128977
129035
  // src/commands/task-verbs.ts
128978
- function warnDeprecated(oldInvocation, newInvocation) {
128979
- console.warn(
128980
- `\u26A0\uFE0F \`ametyst ${oldInvocation}\` is DEPRECATED and still works unchanged \u2014 use \`ametyst ${newInvocation}\`.`
128981
- );
128982
- }
129036
+ init_esm_shims();
128983
129037
  async function runTaskVerb(id, opts, flavor) {
128984
- const r = await runLoop(id, {
129038
+ const r = await runTask(id, {
128985
129039
  maxBudgetUsd: opts.maxBudgetUsd,
128986
- maxConcurrentFires: opts.maxConcurrentFires
129040
+ maxConcurrentFires: opts.maxConcurrentFires,
129041
+ input: opts.input
128987
129042
  });
128988
129043
  console.log(
128989
129044
  r.status === "clean" ? cleanFinishLine(flavor.noun, r.shipBack) : r.status === "skipped" ? "\u23ED\uFE0F nothing launched; the concurrency ceiling is already filled by live fires." : `\u23F8\uFE0F ${flavor.noun} paused; this fire's folder kept for resume at ${r.dir}.`
@@ -128999,15 +129054,17 @@ function cleanFinishLine(noun, shipBack) {
128999
129054
  return `\u2705 ${noun} completed; the ship-back did NOT complete (see the error above), and this fire's folder is cleaned up.`;
129000
129055
  case "shipped":
129001
129056
  return `\u2705 ${noun} completed; improvements shipped back and this fire's folder cleaned up.`;
129057
+ case "skipped-not-owner":
129058
+ return `\u2705 ${noun} completed; ship-back skipped \u2014 this task is owned by ${shipBack.owner ?? "someone else"}, so your learnings stay in the run diary; this fire's folder is cleaned up.`;
129002
129059
  }
129003
129060
  }
129004
129061
  async function showTaskVerb(id) {
129005
- await showLoop(id);
129062
+ await showTask(id);
129006
129063
  }
129007
129064
  async function pushTaskVerb(path2, opts, flavor) {
129008
129065
  try {
129009
- const r = await pushCompoundFromFile(path2, opts);
129010
- const c = r.compound;
129066
+ const r = await pushTaskFromFile(path2, opts);
129067
+ const c = r.task;
129011
129068
  console.log(`\u2705 ${flavor.noun} ${r.mode}: ${c?.slug ?? opts.slug}${c?.id ? ` (${c.id})` : ""}`);
129012
129069
  } catch (err) {
129013
129070
  console.error(`\u274C ${err instanceof Error ? err.message : String(err)}`);
@@ -129019,8 +129076,8 @@ function warnDoubleArm(noun, slug, found) {
129019
129076
  const where = found.map((f) => `${f.namespace} (${f.label})`).join(", ");
129020
129077
  console.warn(
129021
129078
  `\u26A0\uFE0F ALREADY SCHEDULED ELSEWHERE \u2014 ${slug} is armed under: ${where}.
129022
- Arming it here ADDS a SECOND job (xyz.ametyst.${noun}.${slug}) alongside it: launchd's singleton is per label, so the old job keeps firing and ${slug} runs TWICE per interval \u2014 both jobs writing into the same loops/${slug}/.state/launchd.out.log, whose ledger line names neither, so the duplicate ticks cannot be attributed.
129023
- Nothing has been removed. To arm it ONCE, run \`ametyst task unschedule ${slug}\` (it removes the slug from EVERY namespace \u2014 task, loop and compound) and schedule it again.`
129079
+ Arming it here ADDS a SECOND job (xyz.ametyst.${noun}.${slug}) alongside it: launchd's singleton is per label, so the old job keeps firing and ${slug} runs TWICE per interval \u2014 each job writing its own launchd.out.log (the legacy one under loops/${slug}/.state/, this one under tasks/${slug}/.state/) with a ledger line that names neither, so the duplicate ticks cannot be attributed.
129080
+ Nothing has been removed. To arm it ONCE, run \`ametyst task unschedule ${slug}\` (it removes the slug from EVERY label namespace, the legacy ones included) and schedule it again.`
129024
129081
  );
129025
129082
  }
129026
129083
  function scheduleTaskVerb(slug, opts, flavor) {
@@ -129052,14 +129109,17 @@ function scheduleTaskVerb(slug, opts, flavor) {
129052
129109
  // src/commands/task.ts
129053
129110
  var TASK = { noun: "task" };
129054
129111
  var taskCommand = new Command("task").description(
129055
- "Manage and run Ametyst tasks \u2014 the unified surface over what used to be compounds and loops"
129112
+ "Manage and run Ametyst tasks \u2014 publish, run, show, schedule and sync them, and read their memory"
129056
129113
  );
129057
129114
  taskCommand.command("run <id>").description(
129058
- "Materialize a task and run it headless, then ship back improvements on clean exit. While it runs, the task's dashboard (if it has one) is served on http://localhost:4477 (base port: AMETYST_LOOP_DASHBOARD_PORT, walking up when taken) and opened in the browser once when stdout is a TTY (set AMETYST_DASHBOARD_NO_OPEN=1 to skip)"
129115
+ "Materialize a task and run it headless, then ship back improvements on clean exit. While it runs, the task's dashboard (if it has one) is served on http://localhost:4477 (base port: AMETYST_TASK_DASHBOARD_PORT, walking up when taken) and opened in the browser once when stdout is a TTY (set AMETYST_DASHBOARD_NO_OPEN=1 to skip)"
129059
129116
  ).option("--max-budget-usd <x>", "hard spend ceiling in USD", (v) => Number(v)).option(
129060
129117
  "--max-concurrent-fires <n>",
129061
- "ceiling on simultaneously-live fires of this task; 0 = unlimited (default: AMETYST_LOOP_MAX_CONCURRENT_FIRES, else 6)",
129118
+ "ceiling on simultaneously-live fires of this task; 0 = unlimited (default: AMETYST_TASK_MAX_CONCURRENT_FIRES, else 6)",
129062
129119
  (v) => Number(v)
129120
+ ).option(
129121
+ "--input <text>",
129122
+ `the user's arguments for this run, exactly as they would type them in chat (e.g. "run it on skyfire.com, cap $5"); reaches the task as AMETYST_TASK_INPUT and as a LAUNCH INPUT block in its instructions`
129063
129123
  ).action((id, opts) => runTaskVerb(id, opts, TASK));
129064
129124
  taskCommand.command("show <id>").description(
129065
129125
  "Print a task's stored files (SKILL/VISION/CONSTRAINTS/README) from Ametyst without running it"
@@ -129089,6 +129149,9 @@ taskCommand.command("schedule <slug>").description("Schedule a headless task run
129089
129149
  ).option(
129090
129150
  "--model <id>",
129091
129151
  "model to pin the scheduled fires to (default: ANTHROPIC_MODEL, else your Claude Code default at schedule time)"
129152
+ ).option(
129153
+ "--input <text>",
129154
+ "the user's arguments for EVERY scheduled fire, persisted on the job's command line (same as `task run --input`)"
129092
129155
  ).option(
129093
129156
  "--env <KEY=VALUE>",
129094
129157
  "extra environment baked into the scheduled job; repeatable, and merged LAST so it wins over PATH/HOME/ANTHROPIC_MODEL",
@@ -129109,17 +129172,17 @@ taskCommand.command("schedule <slug>").description("Schedule a headless task run
129109
129172
  })
129110
129173
  );
129111
129174
  taskCommand.command("unschedule <slug>").description(
129112
- "Remove a task's schedule \u2014 from EVERY label namespace it is armed in (task, loop, compound), so a job scheduled before `ametyst task` existed is still removable"
129175
+ "Remove a task's schedule \u2014 from EVERY label namespace it is armed in, the legacy ones an older cli used included, so a job scheduled before `ametyst task` existed is still removable"
129113
129176
  ).action((slug) => {
129114
129177
  const removed = unscheduleEverywhere(slug);
129115
129178
  if (removed.length === 0) {
129116
- console.log(`\u2139\uFE0F nothing scheduled for ${slug} \u2014 no task, loop or compound job was armed.`);
129179
+ console.log(`\u2139\uFE0F nothing scheduled for ${slug} \u2014 no job was armed under any label namespace.`);
129117
129180
  return;
129118
129181
  }
129119
129182
  console.log(`\u{1F5D1}\uFE0F unscheduled ${slug} (${removed.join(", ")})`);
129120
129183
  });
129121
129184
  taskCommand.command("schedules").description(
129122
- "List every scheduled job and the environment each armed job carries \u2014 across the task, loop and compound label namespaces"
129185
+ "List every scheduled job and the environment each armed job carries \u2014 across every label namespace, the legacy ones an older cli used included"
129123
129186
  ).action(() => {
129124
129187
  const entries = listAllScheduleEntries();
129125
129188
  console.log(
@@ -129128,6 +129191,25 @@ taskCommand.command("schedules").description(
129128
129191
  ).join("\n") : "no scheduled tasks"
129129
129192
  );
129130
129193
  });
129194
+ taskCommand.command("sync-skills").description(
129195
+ "Write a local pointer skill per published task into the host agent's skills directory \u2014 .claude/skills/<slug>/SKILL.md for Claude, .codex/skills/<slug>/SKILL.md for Codex (restores /-slash-command invocation in file-based hosts)"
129196
+ ).option("--global", "write to the home-dir root (~/.claude/skills | ~/.codex/skills) instead of the project-local one").option(
129197
+ "--target <target>",
129198
+ "host agent skills dir to sync: claude | codex | both (default: auto \u2014 every host detected as present; none detected falls back to claude)"
129199
+ ).action(async (opts) => {
129200
+ try {
129201
+ const targets = resolveSyncSkillsCliTargets(opts.target);
129202
+ for (const target of targets) {
129203
+ const r = await syncSkills({ global: opts.global, target });
129204
+ console.log(
129205
+ `\u2705 sync-skills [${target}]: ${r.written} written, ${r.pruned} pruned${r.skipped.length ? `, ${r.skipped.length} skipped (unmanaged): ${r.skipped.join(", ")}` : ""} \u2192 ${r.root}${r.fallback ? ` (fallback to the home root: ${r.fallback.why}; project-local ${r.fallback.from} was not used)` : ""}`
129206
+ );
129207
+ }
129208
+ } catch (err) {
129209
+ console.error(`\u274C ${err instanceof Error ? err.message : String(err)}`);
129210
+ process.exitCode = 1;
129211
+ }
129212
+ });
129131
129213
  function runVerb(fn) {
129132
129214
  fn().catch((err) => {
129133
129215
  console.error(`\u274C ${err instanceof Error ? err.message : String(err)}`);
@@ -129159,130 +129241,6 @@ memoryCommand.command("attach <slug>", { hidden: true }).allowUnknownOption(true
129159
129241
  );
129160
129242
  taskCommand.addCommand(memoryCommand);
129161
129243
 
129162
- // src/commands/loop.ts
129163
- init_esm_shims();
129164
- var LOOP = { noun: "loop" };
129165
- var loopCommand = new Command("loop").description(
129166
- "DEPRECATED alias of `ametyst task` \u2014 manage and run Ametyst loops (still works unchanged)"
129167
- );
129168
- loopCommand.command("run <id>").description(
129169
- "DEPRECATED \u2014 use `ametyst task run`. Materialize a loop and run it headless, then ship back improvements on clean exit"
129170
- ).option("--max-budget-usd <x>", "hard spend ceiling in USD", (v) => Number(v)).option(
129171
- "--max-concurrent-fires <n>",
129172
- "ceiling on simultaneously-live fires of this loop; 0 = unlimited (default: AMETYST_LOOP_MAX_CONCURRENT_FIRES, else 6)",
129173
- (v) => Number(v)
129174
- ).action((id, opts) => {
129175
- warnDeprecated("loop run", "task run");
129176
- return runTaskVerb(id, opts, LOOP);
129177
- });
129178
- loopCommand.command("show <id>").description(
129179
- "DEPRECATED \u2014 use `ametyst task show`. Print a loop's stored files (SKILL/VISION/CONSTRAINTS/README) from Ametyst without running it"
129180
- ).action((id) => {
129181
- warnDeprecated("loop show", "task show");
129182
- return showTaskVerb(id);
129183
- });
129184
- loopCommand.command("schedule <slug>").description(
129185
- "DEPRECATED \u2014 use `ametyst task schedule`. Schedule a headless loop run (launchd on macOS, crontab on Linux)"
129186
- ).option("--at <hhmm>", "daily run time, 24h HH:MM (e.g. 02:30)").option("--every <dur>", "recurring interval, e.g. 30m, 1h, 1d").option("--max-budget-usd <x>", "optional hard spend ceiling per run in USD (opt-in; omitted by default)", (v) => Number(v)).option("--model <id>", "model to pin the scheduled fires to (default: ANTHROPIC_MODEL, else your Claude Code default at schedule time)").option(
129187
- "--env <KEY=VALUE>",
129188
- "extra environment baked into the scheduled job; repeatable, and merged LAST so it wins over PATH/HOME/ANTHROPIC_MODEL",
129189
- (v, prev = []) => [...prev, v],
129190
- []
129191
- ).action((slug, opts) => {
129192
- warnDeprecated("loop schedule", "task schedule");
129193
- scheduleTaskVerb(slug, opts, { ...LOOP, arm: scheduleLoop });
129194
- });
129195
- loopCommand.command("unschedule <slug>").description(
129196
- "DEPRECATED \u2014 use `ametyst task unschedule` (which also removes jobs armed under the loop/compound labels). Remove a loop's schedule"
129197
- ).action((slug) => {
129198
- warnDeprecated("loop unschedule", "task unschedule");
129199
- unscheduleLoop(slug);
129200
- console.log(`\u{1F5D1}\uFE0F unscheduled loop ${slug}`);
129201
- });
129202
- loopCommand.command("schedules").description(
129203
- "DEPRECATED \u2014 use `ametyst task schedules` (which also lists jobs armed under the loop/compound labels). List scheduled loops and the environment each armed job carries"
129204
- ).action(() => {
129205
- warnDeprecated("loop schedules", "task schedules");
129206
- const entries = listScheduleEntries();
129207
- console.log(
129208
- entries.length ? entries.map((e) => `${e.slug} env: ${e.envKeys.length ? e.envKeys.join(", ") : "\u2014"}`).join("\n") : "no scheduled loops"
129209
- );
129210
- });
129211
-
129212
- // src/commands/compound.ts
129213
- init_esm_shims();
129214
- var COMPOUND = { noun: "compound" };
129215
- var compoundCommand = new Command("compound").description(
129216
- "DEPRECATED alias-era group \u2014 prefer `ametyst task`. Run Ametyst compound skills (still works unchanged)"
129217
- );
129218
- compoundCommand.command("push <path>").description("DEPRECATED \u2014 use `ametyst task push`. Publish a compound whose body is read verbatim (byte-for-byte) from a local markdown file").requiredOption("--slug <slug>", "URL-safe unique slug for the compound").requiredOption("--description <text>", "one-line description of what the compound does").option("--category <category>", "free-text category for browsing/filtering").option("--graph-json <json>", "JSON string of the canvas node graph (defaults to {})").option("--id <id>", "MODIFY the existing compound with this id instead of creating a new one").option(
129219
- "--draft <bool>",
129220
- "draft state: 'true' to keep as draft (default on create), 'false' to publish",
129221
- (v) => v === "true" || v === "1" || v === "yes"
129222
- ).action(
129223
- (path2, opts) => {
129224
- warnDeprecated("compound push", "task push");
129225
- return pushTaskVerb(
129226
- path2,
129227
- {
129228
- slug: opts.slug,
129229
- descriptionShort: opts.description,
129230
- category: opts.category,
129231
- graphJson: opts.graphJson,
129232
- id: opts.id,
129233
- draft: opts.draft
129234
- },
129235
- COMPOUND
129236
- );
129237
- }
129238
- );
129239
- compoundCommand.command("sync-skills").description(
129240
- "Write a local pointer skill per published compound/loop into the host agent's skills directory \u2014 .claude/skills/<slug>/SKILL.md for Claude, .codex/skills/<slug>/SKILL.md for Codex (restores /-slash-command invocation in file-based hosts)"
129241
- ).option("--global", "write to the home-dir root (~/.claude/skills | ~/.codex/skills) instead of the project-local one").option(
129242
- "--target <target>",
129243
- "host agent skills dir to sync: claude | codex | both (default: auto \u2014 every host detected as present; none detected falls back to claude)"
129244
- ).action(async (opts) => {
129245
- try {
129246
- const targets = resolveSyncSkillsCliTargets(opts.target);
129247
- for (const target of targets) {
129248
- const r = await syncSkills({ global: opts.global, target });
129249
- console.log(
129250
- `\u2705 sync-skills [${target}]: ${r.written} written, ${r.pruned} pruned${r.skipped.length ? `, ${r.skipped.length} skipped (unmanaged): ${r.skipped.join(", ")}` : ""} \u2192 ${r.root}${r.fallback ? ` (fallback to the home root: ${r.fallback.why}; project-local ${r.fallback.from} was not used)` : ""}`
129251
- );
129252
- }
129253
- } catch (err) {
129254
- console.error(`\u274C ${err instanceof Error ? err.message : String(err)}`);
129255
- process.exitCode = 1;
129256
- }
129257
- });
129258
- compoundCommand.command("run <id>").description(
129259
- "DEPRECATED \u2014 prefer `ametyst task run`, which materializes the task's definition files and ships constraints back. This one-shot runner is kept UNCHANGED: it runs the compound body with no folder, no STATUS and no ship-back"
129260
- ).option("--max-budget-usd <x>", "optional hard spend ceiling in USD (opt-in; omitted by default)", (v) => Number(v)).option("--dangerously-skip-permissions", "run fully unattended (already implied for compounds)").action(async (id, opts) => {
129261
- warnDeprecated("compound run", "task run");
129262
- const r = await runCompound(id, { maxBudgetUsd: opts.maxBudgetUsd });
129263
- console.log(
129264
- r.exitCode === 0 ? "\u2705 compound completed." : `\u26A0\uFE0F compound exited with code ${r.exitCode}.`
129265
- );
129266
- });
129267
- compoundCommand.command("schedule <slug>").description("DEPRECATED \u2014 use `ametyst task schedule`. Schedule a headless compound run (launchd on macOS, crontab on Linux)").option("--at <hhmm>", "daily run time, 24h HH:MM (e.g. 02:30)").option("--every <dur>", "recurring interval, e.g. 30m, 1h, 1d").option("--max-budget-usd <x>", "optional hard spend ceiling per run in USD (opt-in; omitted by default)", (v) => Number(v)).option("--model <id>", "model to pin the scheduled fires to (default: ANTHROPIC_MODEL, else your Claude Code default at schedule time)").action((slug, opts) => {
129268
- warnDeprecated("compound schedule", "task schedule");
129269
- scheduleTaskVerb(slug, opts, { ...COMPOUND, arm: scheduleCompound });
129270
- });
129271
- compoundCommand.command("unschedule <slug>").description(
129272
- "DEPRECATED \u2014 use `ametyst task unschedule` (which also removes jobs armed under the task/loop labels). Remove a compound's schedule"
129273
- ).action((slug) => {
129274
- warnDeprecated("compound unschedule", "task unschedule");
129275
- unscheduleCompound(slug);
129276
- console.log(`\u{1F5D1}\uFE0F unscheduled compound ${slug}`);
129277
- });
129278
- compoundCommand.command("schedules").description(
129279
- "DEPRECATED \u2014 use `ametyst task schedules` (which also lists jobs armed under the task/loop labels). List scheduled compounds"
129280
- ).action(() => {
129281
- warnDeprecated("compound schedules", "task schedules");
129282
- const s = listCompoundSchedules();
129283
- console.log(s.length ? s.join("\n") : "no scheduled compounds");
129284
- });
129285
-
129286
129244
  // src/commands/delegate.ts
129287
129245
  init_esm_shims();
129288
129246
  function toDelegateOptions(task, opts) {
@@ -129373,8 +129331,6 @@ program2.command("delegate [task]").description(
129373
129331
  ).action(delegateCommand);
129374
129332
  program2.addCommand(connectionsCommand);
129375
129333
  program2.addCommand(taskCommand);
129376
- program2.addCommand(loopCommand);
129377
- program2.addCommand(compoundCommand);
129378
129334
  program2.addCommand(walletCommand);
129379
129335
  program2.parseAsync().catch((e) => {
129380
129336
  console.error(e instanceof Error ? e.message : String(e));