@ametyst/cli 0.3.8 → 0.3.12
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.
- package/dist/index.js +1426 -1279
- 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
|
|
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 =
|
|
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
|
|
106411
|
-
return join(runRoot ?? resolveRunRoot().root, `.ametyst${ENV_SUFFIX}`,
|
|
106410
|
+
function tasksRoot(runRoot) {
|
|
106411
|
+
return join(runRoot ?? resolveRunRoot().root, `.ametyst${ENV_SUFFIX}`, RUNS_SEGMENT);
|
|
106412
106412
|
}
|
|
106413
|
-
function
|
|
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
|
|
106416
|
-
return join(
|
|
106415
|
+
if (!safe) throw new Error(`invalid task slug: ${JSON.stringify(slug)}`);
|
|
106416
|
+
return join(tasksRoot(runRoot), safe);
|
|
106417
106417
|
}
|
|
106418
|
-
function
|
|
106419
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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, `.${
|
|
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.
|
|
112490
|
+
CLI_VERSION = true ? "0.3.12" : "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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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/
|
|
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
|
|
116877
|
-
|
|
116878
|
-
|
|
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
|
|
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.
|
|
117129
|
-
if (counts.
|
|
117130
|
-
if (counts.
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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 =
|
|
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/
|
|
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 =
|
|
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
|
|
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
|
|
118007
|
+
const spawn3 = deps.spawn ?? realSpawn;
|
|
117962
118008
|
try {
|
|
117963
|
-
const child =
|
|
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.
|
|
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.
|
|
117980
|
-
const
|
|
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.
|
|
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.
|
|
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.
|
|
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,557 +118136,264 @@ 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(`
|
|
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
|
-
` (
|
|
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(` (
|
|
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/
|
|
118154
|
+
// src/tasks/dashboard-docs.ts
|
|
118073
118155
|
init_esm_shims();
|
|
118074
|
-
|
|
118075
|
-
|
|
118076
|
-
|
|
118077
|
-
|
|
118078
|
-
|
|
118079
|
-
|
|
118080
|
-
|
|
118081
|
-
|
|
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
|
|
118084
|
-
if (typeof
|
|
118085
|
-
|
|
118086
|
-
|
|
118087
|
-
|
|
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
|
-
|
|
118092
|
-
|
|
118093
|
-
|
|
118094
|
-
|
|
118095
|
-
|
|
118096
|
-
|
|
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
|
-
|
|
118099
|
-
|
|
118100
|
-
|
|
118101
|
-
if (
|
|
118102
|
-
|
|
118103
|
-
|
|
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
|
-
|
|
118225
|
+
console.log(human());
|
|
118106
118226
|
}
|
|
118107
|
-
function
|
|
118108
|
-
|
|
118109
|
-
|
|
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
|
|
118121
|
-
|
|
118122
|
-
|
|
118123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118146
|
-
|
|
118147
|
-
|
|
118148
|
-
"
|
|
118149
|
-
|
|
118150
|
-
|
|
118151
|
-
|
|
118152
|
-
|
|
118153
|
-
|
|
118154
|
-
|
|
118155
|
-
|
|
118156
|
-
|
|
118157
|
-
|
|
118158
|
-
|
|
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
|
-
|
|
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
|
|
118190
|
-
|
|
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
|
|
118193
|
-
|
|
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
|
|
118196
|
-
|
|
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
|
|
118199
|
-
|
|
118200
|
-
|
|
118201
|
-
|
|
118202
|
-
|
|
118203
|
-
|
|
118204
|
-
|
|
118205
|
-
|
|
118206
|
-
|
|
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 & 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
|
-
|
|
118274
|
-
|
|
118275
|
-
|
|
118276
|
-
if (
|
|
118277
|
-
|
|
118278
|
-
|
|
118279
|
-
|
|
118280
|
-
|
|
118281
|
-
|
|
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
|
-
|
|
118288
|
-
|
|
118289
|
-
|
|
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
|
-
|
|
118294
|
-
|
|
118295
|
-
|
|
118296
|
-
|
|
118297
|
-
|
|
118298
|
-
|
|
118299
|
-
|
|
118300
|
-
|
|
118301
|
-
|
|
118302
|
-
|
|
118303
|
-
|
|
118304
|
-
|
|
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
|
-
|
|
118308
|
-
|
|
118309
|
-
|
|
118310
|
-
|
|
118311
|
-
|
|
118312
|
-
|
|
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
|
-
|
|
118316
|
-
|
|
118317
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118360
|
-
|
|
118361
|
-
|
|
118362
|
-
|
|
118363
|
-
|
|
118364
|
-
|
|
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
|
-
|
|
118371
|
-
|
|
118372
|
-
|
|
118373
|
-
|
|
118374
|
-
|
|
118375
|
-
|
|
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
|
}
|
|
118386
|
-
|
|
118387
|
-
|
|
118388
|
-
|
|
118389
|
-
|
|
118390
|
-
|
|
118391
|
-
|
|
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}.`
|
|
118418
|
-
);
|
|
118419
|
-
}
|
|
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
|
|
118426
|
-
});
|
|
118427
|
-
return { sdk, apiKey };
|
|
118428
|
-
}
|
|
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;
|
|
118435
|
-
}
|
|
118436
|
-
console.log(human());
|
|
118437
|
-
}
|
|
118438
|
-
function fail(res, what) {
|
|
118439
|
-
const code = res.code ? ` (${res.code})` : "";
|
|
118440
|
-
throw new Error(`could not ${what}${code}: ${res.error ?? "unknown error"}`);
|
|
118441
|
-
}
|
|
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
|
-
}
|
|
118454
|
-
}
|
|
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
|
-
}
|
|
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
|
-
);
|
|
118475
|
-
}
|
|
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)}`);
|
|
118482
|
-
}
|
|
118483
|
-
return key;
|
|
118484
|
-
}
|
|
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 };
|
|
118490
|
-
}
|
|
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 };
|
|
118495
|
-
}
|
|
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;
|
|
118513
|
-
}
|
|
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)}`);
|
|
118519
|
-
}
|
|
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)}`);
|
|
118530
|
-
}
|
|
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)}`);
|
|
118534
|
-
}
|
|
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 } : {}
|
|
118564
|
-
};
|
|
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;
|
|
118570
|
-
}
|
|
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`);
|
|
118585
|
-
}
|
|
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;
|
|
118593
|
-
}
|
|
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));
|
|
118599
|
-
return;
|
|
118600
|
-
}
|
|
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;
|
|
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;
|
|
118608
118397
|
}
|
|
118609
118398
|
const manifest = scope === void 0 || scope === "all" ? await readTaskManifest(sdk, apiKey, slug) : void 0;
|
|
118610
118399
|
const manifestUnreadable = manifest === void 0 && (scope === void 0 || scope === "all");
|
|
@@ -118708,11 +118497,481 @@ async function taskMemoryArchiveVerb(rawSlug, opts) {
|
|
|
118708
118497
|
if (res.code === 409) throw new Error(`"${key}" is already archived \u2014 its latest version is closed (409): ${res.error ?? ""}`.trim());
|
|
118709
118498
|
fail(res, `archive "${key}"`);
|
|
118710
118499
|
}
|
|
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
|
-
);
|
|
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
|
+
);
|
|
118505
|
+
}
|
|
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
|
+
`;
|
|
118516
|
+
}
|
|
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;
|
|
118524
|
+
}
|
|
118525
|
+
}
|
|
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;
|
|
118533
|
+
}
|
|
118534
|
+
if (email) {
|
|
118535
|
+
out.GIT_AUTHOR_EMAIL = email;
|
|
118536
|
+
out.GIT_COMMITTER_EMAIL = email;
|
|
118537
|
+
}
|
|
118538
|
+
return out;
|
|
118539
|
+
}
|
|
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));
|
|
118594
|
+
}
|
|
118595
|
+
if (opts.sessionId) {
|
|
118596
|
+
args.push("--session-id", opts.sessionId);
|
|
118597
|
+
}
|
|
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
|
+
};
|
|
118629
|
+
}
|
|
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);
|
|
118655
|
+
}
|
|
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();
|
|
118670
|
+
}
|
|
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 };
|
|
118690
|
+
}
|
|
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/ownership.ts
|
|
118712
|
+
init_esm_shims();
|
|
118713
|
+
async function resolveTaskOwnership(sdk, apiKey, entity) {
|
|
118714
|
+
try {
|
|
118715
|
+
const createdBy = typeof entity?.createdBy === "string" ? entity.createdBy.trim() : "";
|
|
118716
|
+
if (!createdBy) return { resolved: false };
|
|
118717
|
+
const res = await sdk.compoundedSkills.getSyncSelection(apiKey);
|
|
118718
|
+
const name = res?.status === "ok" && typeof res.self?.name === "string" ? res.self.name.trim() : "";
|
|
118719
|
+
if (!name) return { resolved: false };
|
|
118720
|
+
return { resolved: true, isOwner: name === createdBy, createdBy };
|
|
118721
|
+
} catch {
|
|
118722
|
+
return { resolved: false };
|
|
118723
|
+
}
|
|
118724
|
+
}
|
|
118725
|
+
|
|
118726
|
+
// src/tasks/dashboard-template.ts
|
|
118727
|
+
init_esm_shims();
|
|
118728
|
+
var DEFAULT_DASHBOARD_FILES = [
|
|
118729
|
+
"VISION.md",
|
|
118730
|
+
"CONSTRAINTS.md",
|
|
118731
|
+
"QUEUE.md",
|
|
118732
|
+
"STATUS.md",
|
|
118733
|
+
"README.md",
|
|
118734
|
+
"rounds.jsonl"
|
|
118735
|
+
];
|
|
118736
|
+
function parseProcessSpec(manifest) {
|
|
118737
|
+
if (typeof manifest !== "string" || !manifest.trim()) return null;
|
|
118738
|
+
let parsed;
|
|
118739
|
+
try {
|
|
118740
|
+
parsed = JSON.parse(manifest);
|
|
118741
|
+
} catch {
|
|
118742
|
+
return null;
|
|
118743
|
+
}
|
|
118744
|
+
const raw = parsed?.processSpec;
|
|
118745
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
118746
|
+
const spec = raw;
|
|
118747
|
+
const stages = (Array.isArray(spec.stages) ? spec.stages : []).map((s) => {
|
|
118748
|
+
if (typeof s === "string" && s.trim()) return { label: s.trim() };
|
|
118749
|
+
if (s && typeof s === "object" && !Array.isArray(s)) {
|
|
118750
|
+
const o = s;
|
|
118751
|
+
if (typeof o.label === "string" && o.label.trim()) {
|
|
118752
|
+
return {
|
|
118753
|
+
label: o.label.trim(),
|
|
118754
|
+
...typeof o.id === "string" && o.id.trim() ? { id: o.id.trim() } : {},
|
|
118755
|
+
...typeof o.detail === "string" && o.detail.trim() ? { detail: o.detail.trim() } : {}
|
|
118756
|
+
};
|
|
118757
|
+
}
|
|
118758
|
+
}
|
|
118759
|
+
return null;
|
|
118760
|
+
}).filter((s) => s !== null);
|
|
118761
|
+
const strings = (v) => (Array.isArray(v) ? v : []).filter((x) => typeof x === "string" && x.trim() !== "");
|
|
118762
|
+
const out = {
|
|
118763
|
+
stages,
|
|
118764
|
+
inputs: strings(spec.inputs),
|
|
118765
|
+
outputs: strings(spec.outputs),
|
|
118766
|
+
...typeof spec.title === "string" && spec.title.trim() ? { title: spec.title.trim() } : {}
|
|
118767
|
+
};
|
|
118768
|
+
return stages.length || out.inputs.length || out.outputs.length ? out : null;
|
|
118769
|
+
}
|
|
118770
|
+
function defaultDashboardManifest() {
|
|
118771
|
+
return JSON.stringify({ files: [...DEFAULT_DASHBOARD_FILES] });
|
|
118772
|
+
}
|
|
118773
|
+
function embedJson(value2) {
|
|
118774
|
+
return JSON.stringify(value2).replace(/</g, "\\u003c");
|
|
118775
|
+
}
|
|
118776
|
+
function escapeHtml(s) {
|
|
118777
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
118778
|
+
}
|
|
118779
|
+
function renderTaskDashboardTemplate(args) {
|
|
118780
|
+
const title = escapeHtml(args.processSpec?.title ?? args.slug);
|
|
118781
|
+
const seed = embedJson({
|
|
118782
|
+
slug: args.slug,
|
|
118783
|
+
descriptionShort: args.descriptionShort ?? "",
|
|
118784
|
+
processSpec: args.processSpec ?? null
|
|
118785
|
+
});
|
|
118786
|
+
return `<!doctype html>
|
|
118787
|
+
<html lang="en">
|
|
118788
|
+
<head>
|
|
118789
|
+
<meta charset="utf-8">
|
|
118790
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
118791
|
+
<title>${title} \u2014 task dashboard</title>
|
|
118792
|
+
<style>
|
|
118793
|
+
:root { --bg:#f7f7fb; --panel:#fff; --border:#e3e3ee; --muted:#6b6b80; --accent:#5b5bd6; --ok:#1a7f37; --bad:#b42318; }
|
|
118794
|
+
* { box-sizing: border-box; }
|
|
118795
|
+
body { margin:0; font:14px/1.5 -apple-system, "Segoe UI", Roboto, sans-serif; background:var(--bg); color:#1d1d2b; padding:24px; }
|
|
118796
|
+
h1 { font-size:20px; margin:0 0 4px; }
|
|
118797
|
+
h2 { font-size:14px; margin:24px 0 8px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
|
|
118798
|
+
.sub { color:var(--muted); margin:0 0 16px; }
|
|
118799
|
+
.panel { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px; }
|
|
118800
|
+
.map { display:flex; flex-wrap:wrap; align-items:stretch; gap:8px; }
|
|
118801
|
+
.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; }
|
|
118802
|
+
.stage .label { font-weight:600; }
|
|
118803
|
+
.stage .detail { color:var(--muted); font-size:12px; margin-top:2px; }
|
|
118804
|
+
.arrow { align-self:center; color:var(--muted); }
|
|
118805
|
+
.io { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
|
118806
|
+
ul { margin:6px 0 0; padding-left:18px; }
|
|
118807
|
+
table { width:100%; border-collapse:collapse; background:var(--panel); border:1px solid var(--border); border-radius:12px; overflow:hidden; }
|
|
118808
|
+
th, td { text-align:left; padding:8px 12px; border-top:1px solid var(--border); font-size:13px; vertical-align:top; }
|
|
118809
|
+
thead th { border-top:none; background:var(--bg); color:var(--muted); font-weight:600; }
|
|
118810
|
+
.ok { color:var(--ok); } .bad { color:var(--bad); }
|
|
118811
|
+
details { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:10px 14px; margin-bottom:8px; }
|
|
118812
|
+
summary { cursor:pointer; font-weight:600; }
|
|
118813
|
+
pre { overflow-x:auto; font-size:12px; background:var(--bg); border-radius:8px; padding:10px; }
|
|
118814
|
+
.empty { color:var(--muted); font-style:italic; }
|
|
118815
|
+
#live { font-size:12px; color:var(--muted); float:right; }
|
|
118816
|
+
</style>
|
|
118817
|
+
</head>
|
|
118818
|
+
<body>
|
|
118819
|
+
<span id="live">loading\u2026</span>
|
|
118820
|
+
<h1 id="title"></h1>
|
|
118821
|
+
<p class="sub" id="desc"></p>
|
|
118822
|
+
|
|
118823
|
+
<h2>Process</h2>
|
|
118824
|
+
<div id="map" class="map panel"></div>
|
|
118825
|
+
|
|
118826
|
+
<h2>Inputs & outputs</h2>
|
|
118827
|
+
<div class="io">
|
|
118828
|
+
<div class="panel"><strong>Inputs</strong><ul id="inputs"></ul></div>
|
|
118829
|
+
<div class="panel"><strong>Outputs</strong><ul id="outputs"></ul></div>
|
|
118830
|
+
</div>
|
|
118831
|
+
|
|
118832
|
+
<h2>Rounds</h2>
|
|
118833
|
+
<div id="rounds"></div>
|
|
118834
|
+
|
|
118835
|
+
<h2>Files</h2>
|
|
118836
|
+
<div id="files"></div>
|
|
118837
|
+
|
|
118838
|
+
<script type="application/json" id="loop-seed">${seed}</script>
|
|
118839
|
+
<script>
|
|
118840
|
+
(function () {
|
|
118841
|
+
"use strict";
|
|
118842
|
+
var seed = JSON.parse(document.getElementById("loop-seed").textContent);
|
|
118843
|
+
document.getElementById("title").textContent = (seed.processSpec && seed.processSpec.title) || seed.slug;
|
|
118844
|
+
document.getElementById("desc").textContent = seed.descriptionShort || "";
|
|
118845
|
+
document.title = seed.slug + " \u2014 task dashboard";
|
|
118846
|
+
|
|
118847
|
+
function el(tag, cls, text) {
|
|
118848
|
+
var e = document.createElement(tag);
|
|
118849
|
+
if (cls) e.className = cls;
|
|
118850
|
+
if (text !== undefined) e.textContent = text;
|
|
118851
|
+
return e;
|
|
118852
|
+
}
|
|
118853
|
+
|
|
118854
|
+
// \u2500\u2500 Process map + IO from the embedded spec (static \u2014 rendered once). \u2500\u2500
|
|
118855
|
+
var map = document.getElementById("map");
|
|
118856
|
+
var spec = seed.processSpec;
|
|
118857
|
+
if (spec && spec.stages && spec.stages.length) {
|
|
118858
|
+
spec.stages.forEach(function (s, i) {
|
|
118859
|
+
if (i > 0) map.appendChild(el("div", "arrow", "\\u2192"));
|
|
118860
|
+
var box = el("div", "stage");
|
|
118861
|
+
box.appendChild(el("div", "label", s.label));
|
|
118862
|
+
if (s.detail) box.appendChild(el("div", "detail", s.detail));
|
|
118863
|
+
map.appendChild(box);
|
|
118864
|
+
});
|
|
118865
|
+
} else {
|
|
118866
|
+
map.appendChild(el("span", "empty", "No process spec on this task \\u2014 live files and rounds below."));
|
|
118867
|
+
}
|
|
118868
|
+
function fillList(id, items) {
|
|
118869
|
+
var ul = document.getElementById(id);
|
|
118870
|
+
ul.textContent = "";
|
|
118871
|
+
if (!items || !items.length) { ul.appendChild(el("li", "empty", "\\u2014")); return; }
|
|
118872
|
+
items.forEach(function (x) { ul.appendChild(el("li", null, x)); });
|
|
118873
|
+
}
|
|
118874
|
+
fillList("inputs", spec && spec.inputs);
|
|
118875
|
+
fillList("outputs", spec && spec.outputs);
|
|
118876
|
+
|
|
118877
|
+
// \u2500\u2500 Live data: rounds + files, refreshed from GET /data. \u2500\u2500
|
|
118878
|
+
function parseJsonl(text) {
|
|
118879
|
+
var rows = [];
|
|
118880
|
+
(text || "").split("\\n").forEach(function (line) {
|
|
118881
|
+
line = line.trim();
|
|
118882
|
+
if (!line) return;
|
|
118883
|
+
try { rows.push(JSON.parse(line)); } catch (e) { /* skip malformed line */ }
|
|
118884
|
+
});
|
|
118885
|
+
return rows;
|
|
118886
|
+
}
|
|
118887
|
+
|
|
118888
|
+
function renderRounds(fires, rounds) {
|
|
118889
|
+
var host = document.getElementById("rounds");
|
|
118890
|
+
host.textContent = "";
|
|
118891
|
+
if (!fires.length && !rounds.length) {
|
|
118892
|
+
var p = el("div", "panel"); p.appendChild(el("span", "empty", "No rounds yet \\u2014 this fills in as the task runs."));
|
|
118893
|
+
host.appendChild(p);
|
|
118894
|
+
return;
|
|
118895
|
+
}
|
|
118896
|
+
var table = document.createElement("table");
|
|
118897
|
+
var thead = document.createElement("thead");
|
|
118898
|
+
var hr = document.createElement("tr");
|
|
118899
|
+
["when", "duration", "turns", "tokens", "exit", "round detail (rounds.jsonl)"].forEach(function (h) {
|
|
118900
|
+
hr.appendChild(el("th", null, h));
|
|
118901
|
+
});
|
|
118902
|
+
thead.appendChild(hr); table.appendChild(thead);
|
|
118903
|
+
var tbody = document.createElement("tbody");
|
|
118904
|
+
var n = Math.max(fires.length, rounds.length);
|
|
118905
|
+
for (var i = n - 1; i >= 0; i--) { // newest first
|
|
118906
|
+
var f = fires[i] || {};
|
|
118907
|
+
var r = rounds[i];
|
|
118908
|
+
var tr = document.createElement("tr");
|
|
118909
|
+
tr.appendChild(el("td", null, f.ts || (r && r.ts) || "\\u2014"));
|
|
118910
|
+
tr.appendChild(el("td", null, f.duration_s != null ? f.duration_s + "s" : "\\u2014"));
|
|
118911
|
+
tr.appendChild(el("td", null, f.turns != null ? String(f.turns) : "\\u2014"));
|
|
118912
|
+
tr.appendChild(el("td", null, f.tokens_total != null ? Number(f.tokens_total).toLocaleString() : "\\u2014"));
|
|
118913
|
+
tr.appendChild(el("td", f.exit === 0 ? "ok" : f.exit != null ? "bad" : null, f.exit != null ? String(f.exit) : "\\u2014"));
|
|
118914
|
+
var detail = el("td");
|
|
118915
|
+
if (f.launch_failed) { detail.textContent = "launch failed \\u2014 " + (f.reason || "unknown reason"); }
|
|
118916
|
+
else if (r) { var pre = document.createElement("pre"); pre.textContent = JSON.stringify(r, null, 1); detail.appendChild(pre); }
|
|
118917
|
+
else detail.textContent = "\\u2014";
|
|
118918
|
+
tr.appendChild(detail);
|
|
118919
|
+
tbody.appendChild(tr);
|
|
118920
|
+
}
|
|
118921
|
+
table.appendChild(tbody);
|
|
118922
|
+
host.appendChild(table);
|
|
118923
|
+
}
|
|
118924
|
+
|
|
118925
|
+
function renderFiles(files) {
|
|
118926
|
+
var host = document.getElementById("files");
|
|
118927
|
+
host.textContent = "";
|
|
118928
|
+
var names = Object.keys(files || {}).filter(function (n) { return n !== "rounds.jsonl"; });
|
|
118929
|
+
if (!names.length) { var p = el("div", "panel"); p.appendChild(el("span", "empty", "No files surfaced by the manifest.")); host.appendChild(p); return; }
|
|
118930
|
+
names.forEach(function (name) {
|
|
118931
|
+
var d = document.createElement("details");
|
|
118932
|
+
d.appendChild(el("summary", null, name));
|
|
118933
|
+
var pre = document.createElement("pre");
|
|
118934
|
+
pre.textContent = files[name];
|
|
118935
|
+
d.appendChild(pre);
|
|
118936
|
+
host.appendChild(d);
|
|
118937
|
+
});
|
|
118938
|
+
}
|
|
118939
|
+
|
|
118940
|
+
function refresh() {
|
|
118941
|
+
fetch("/data").then(function (res) { return res.json(); }).then(function (data) {
|
|
118942
|
+
var fires = parseJsonl(data.state && data.state["fires.jsonl"]);
|
|
118943
|
+
var rounds = parseJsonl(data.files && data.files["rounds.jsonl"]);
|
|
118944
|
+
renderRounds(fires, rounds);
|
|
118945
|
+
renderFiles(data.files);
|
|
118946
|
+
document.getElementById("live").textContent = "updated " + new Date().toLocaleTimeString();
|
|
118947
|
+
}).catch(function () {
|
|
118948
|
+
document.getElementById("live").textContent = "server stopped";
|
|
118949
|
+
});
|
|
118950
|
+
}
|
|
118951
|
+
refresh();
|
|
118952
|
+
setInterval(refresh, 5000);
|
|
118953
|
+
})();
|
|
118954
|
+
</script>
|
|
118955
|
+
</body>
|
|
118956
|
+
</html>
|
|
118957
|
+
`;
|
|
118958
|
+
}
|
|
118959
|
+
function injectDefaultDashboard(task) {
|
|
118960
|
+
const hasHtml = typeof task.dashboardHtml === "string" && task.dashboardHtml.trim() !== "";
|
|
118961
|
+
if (hasHtml) return;
|
|
118962
|
+
if (task.dashboardManifest && typeof task.dashboardManifest === "object") {
|
|
118963
|
+
try {
|
|
118964
|
+
task.dashboardManifest = JSON.stringify(task.dashboardManifest);
|
|
118965
|
+
} catch {
|
|
118966
|
+
}
|
|
118967
|
+
}
|
|
118968
|
+
const manifest = typeof task.dashboardManifest === "string" ? task.dashboardManifest : void 0;
|
|
118969
|
+
task.dashboardHtml = renderTaskDashboardTemplate({
|
|
118970
|
+
slug: typeof task.slug === "string" ? task.slug : "task",
|
|
118971
|
+
descriptionShort: typeof task.descriptionShort === "string" ? task.descriptionShort : void 0,
|
|
118972
|
+
processSpec: parseProcessSpec(manifest)
|
|
118973
|
+
});
|
|
118974
|
+
if (!manifest || !manifest.trim()) task.dashboardManifest = defaultDashboardManifest();
|
|
118716
118975
|
}
|
|
118717
118976
|
|
|
118718
118977
|
// src/mcp-server/memory-scope.ts
|
|
@@ -118741,51 +119000,41 @@ init_esm_shims();
|
|
|
118741
119000
|
|
|
118742
119001
|
// src/tasks/task-run-section.ts
|
|
118743
119002
|
init_esm_shims();
|
|
118744
|
-
var
|
|
118745
|
-
function
|
|
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) {
|
|
119003
|
+
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.";
|
|
119004
|
+
function defaultModeHighlight(mode2) {
|
|
118749
119005
|
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
119006
|
return `
|
|
118756
119007
|
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
119008
|
`;
|
|
118758
119009
|
}
|
|
118759
|
-
function taskRunSection(slug,
|
|
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).`;
|
|
119010
|
+
function taskRunSection(slug, opts = {}) {
|
|
118761
119011
|
return `Before running it, ASK the user HOW to run it, and wait for an answer \u2014 always ask, never pick one silently:
|
|
118762
119012
|
|
|
118763
|
-
1. **one-time, in this chat, followed here** \u2014 ${
|
|
119013
|
+
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
119014
|
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. ${
|
|
118766
|
-
${defaultModeHighlight(
|
|
119015
|
+
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}
|
|
119016
|
+
${defaultModeHighlight(opts.defaultRunMode)}
|
|
118767
119017
|
Stop or inspect a schedule: \`ametyst task unschedule ${slug}\` \xB7 \`ametyst task schedules\`.
|
|
118768
119018
|
To READ it without executing: \`ametyst task show ${slug}\`.
|
|
118769
119019
|
`;
|
|
118770
119020
|
}
|
|
118771
119021
|
|
|
118772
119022
|
// src/mcp-server/prompt-descriptors.ts
|
|
118773
|
-
function promptName(
|
|
118774
|
-
return
|
|
119023
|
+
function promptName(slug) {
|
|
119024
|
+
return `task__${slug}`;
|
|
118775
119025
|
}
|
|
118776
|
-
function buildPromptEntry(
|
|
119026
|
+
function buildPromptEntry(summary) {
|
|
118777
119027
|
const cat = summary.category ? ` [${summary.category}]` : "";
|
|
118778
|
-
const label2 = kind === "compound" ? "Compound skill" : "Loop";
|
|
118779
119028
|
return {
|
|
118780
|
-
name: promptName(
|
|
118781
|
-
description:
|
|
119029
|
+
name: promptName(summary.slug),
|
|
119030
|
+
description: `Task: ${summary.descriptionShort ?? summary.slug}${cat}`
|
|
118782
119031
|
};
|
|
118783
119032
|
}
|
|
118784
|
-
function buildPromptContent(
|
|
118785
|
-
const text = `This is the
|
|
119033
|
+
function buildPromptContent(full) {
|
|
119034
|
+
const text = `This is the task "${full.slug}".
|
|
118786
119035
|
|
|
118787
|
-
${taskRunSection(full.slug,
|
|
118788
|
-
---
|
|
119036
|
+
${taskRunSection(full.slug, { defaultRunMode: parseDefaultRunMode(full.markdownBody) })}
|
|
119037
|
+
--- task body ---
|
|
118789
119038
|
${full.markdownBody ?? ""}`;
|
|
118790
119039
|
return { messages: [{ role: "user", content: { type: "text", text } }] };
|
|
118791
119040
|
}
|
|
@@ -118813,9 +119062,9 @@ function resolveBodyFromInlineOrFile(inline, filePath) {
|
|
|
118813
119062
|
return void 0;
|
|
118814
119063
|
}
|
|
118815
119064
|
|
|
118816
|
-
// src/mcp-server/
|
|
119065
|
+
// src/mcp-server/task-upsert-summary.ts
|
|
118817
119066
|
init_esm_shims();
|
|
118818
|
-
var
|
|
119067
|
+
var TASK_CONTENT_FIELDS = [
|
|
118819
119068
|
"markdownBody",
|
|
118820
119069
|
"visionMd",
|
|
118821
119070
|
"constraintsMd",
|
|
@@ -118918,7 +119167,7 @@ var BRANCH_MATCHERS = [
|
|
|
118918
119167
|
];
|
|
118919
119168
|
function allLinesOf(content) {
|
|
118920
119169
|
const out = [];
|
|
118921
|
-
for (const f of
|
|
119170
|
+
for (const f of TASK_CONTENT_FIELDS) {
|
|
118922
119171
|
const v = content[f];
|
|
118923
119172
|
if (typeof v === "string" && v) out.push(...v.split(/\r?\n/));
|
|
118924
119173
|
}
|
|
@@ -118927,7 +119176,7 @@ function allLinesOf(content) {
|
|
|
118927
119176
|
function mergeEffective(sent, previous) {
|
|
118928
119177
|
const base2 = previous && previous.available ? { ...previous.content } : {};
|
|
118929
119178
|
const provenance = [];
|
|
118930
|
-
const keys = [...
|
|
119179
|
+
const keys = [...TASK_CONTENT_FIELDS, "descriptionShort"];
|
|
118931
119180
|
const content = { ...base2 };
|
|
118932
119181
|
for (const k of keys) {
|
|
118933
119182
|
const sentVal = sent[k];
|
|
@@ -118990,7 +119239,7 @@ function buildReceipt(input) {
|
|
|
118990
119239
|
if (prevVal === void 0) return "changed";
|
|
118991
119240
|
return String(sentVal) === String(prevVal) ? "unchanged" : "changed";
|
|
118992
119241
|
};
|
|
118993
|
-
for (const f of
|
|
119242
|
+
for (const f of TASK_CONTENT_FIELDS) {
|
|
118994
119243
|
const sentVal = sent[f];
|
|
118995
119244
|
const wasSent = typeof sentVal === "string" && sentVal.length > 0;
|
|
118996
119245
|
if (!wasSent && mode2 === "created") continue;
|
|
@@ -119030,7 +119279,7 @@ function deriveChanges(input, receipt) {
|
|
|
119030
119279
|
if (mode2 === "created") {
|
|
119031
119280
|
return {
|
|
119032
119281
|
title: "WHAT CHANGED",
|
|
119033
|
-
lines: ["new
|
|
119282
|
+
lines: ["new task \u2014 there is no previous version to compare against"]
|
|
119034
119283
|
};
|
|
119035
119284
|
}
|
|
119036
119285
|
if (!previous || !previous.available) {
|
|
@@ -119063,7 +119312,7 @@ function deriveChanges(input, receipt) {
|
|
|
119063
119312
|
function renderSections(sections2) {
|
|
119064
119313
|
return sections2.map((s) => [s.title, ...s.lines.map((l) => ` - ${l}`)].join("\n")).join("\n\n");
|
|
119065
119314
|
}
|
|
119066
|
-
function
|
|
119315
|
+
function buildTaskUpsertSummary(input) {
|
|
119067
119316
|
const { content, provenance } = mergeEffective(input.sent, input.previous);
|
|
119068
119317
|
const sections2 = deriveNarrative(content);
|
|
119069
119318
|
const receipt = buildReceipt(input);
|
|
@@ -119086,9 +119335,9 @@ var IDENTITY_KEYS = [
|
|
|
119086
119335
|
"createdAt",
|
|
119087
119336
|
"updatedAt"
|
|
119088
119337
|
];
|
|
119089
|
-
function
|
|
119090
|
-
if (!
|
|
119091
|
-
const src =
|
|
119338
|
+
function projectTaskIdentity(task) {
|
|
119339
|
+
if (!task || typeof task !== "object") return {};
|
|
119340
|
+
const src = task;
|
|
119092
119341
|
const out = {};
|
|
119093
119342
|
for (const k of IDENTITY_KEYS) {
|
|
119094
119343
|
const v = src[k];
|
|
@@ -119871,7 +120120,7 @@ init_paths();
|
|
|
119871
120120
|
import * as nodeFs4 from "fs";
|
|
119872
120121
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
119873
120122
|
import { homedir as homedir8 } from "os";
|
|
119874
|
-
import { basename as
|
|
120123
|
+
import { basename as basename5, join as join14 } from "path";
|
|
119875
120124
|
var DELEGATED_CHILD_ENV_VAR = "AMETYST_DELEGATED";
|
|
119876
120125
|
var SPEND_GRANT_TOKEN_ENV_VAR = "AMETYST_DELEGATE_SPEND_TOKEN";
|
|
119877
120126
|
var SPEND_KILL_SWITCH_ENV_VAR = "AMETYST_DELEGATE_NO_SPEND";
|
|
@@ -119913,7 +120162,7 @@ function defaultProcessTable() {
|
|
|
119913
120162
|
function isOurOpencodeBinary(argv0, home = homedir8()) {
|
|
119914
120163
|
if (!argv0) return false;
|
|
119915
120164
|
if (argv0 === opencodeBinaryPath(home)) return true;
|
|
119916
|
-
return /^opencode-\d/.test(
|
|
120165
|
+
return /^opencode-\d/.test(basename5(argv0));
|
|
119917
120166
|
}
|
|
119918
120167
|
var ancestryMemo;
|
|
119919
120168
|
function classifyAncestry(deps = {}) {
|
|
@@ -121710,7 +121959,7 @@ function registerDelegateTools(deps) {
|
|
|
121710
121959
|
return { registered: [...defs.keys()], parentDeathHook: isProductionStore, mcpServers };
|
|
121711
121960
|
}
|
|
121712
121961
|
|
|
121713
|
-
// src/mcp-server/write-
|
|
121962
|
+
// src/mcp-server/write-task-body.ts
|
|
121714
121963
|
init_esm_shims();
|
|
121715
121964
|
import { tmpdir as tmpdir2 } from "os";
|
|
121716
121965
|
import { join as joinPath } from "path";
|
|
@@ -122195,8 +122444,6 @@ var allowlistCache = null;
|
|
|
122195
122444
|
var capabilityIndexCache = null;
|
|
122196
122445
|
var firstMcpSessionSeen = false;
|
|
122197
122446
|
var firstSessionGuideEmitted = false;
|
|
122198
|
-
var compoundIndexCache = null;
|
|
122199
|
-
var loopIndexCache = null;
|
|
122200
122447
|
var taskIndexCache = null;
|
|
122201
122448
|
var cardCounts = null;
|
|
122202
122449
|
var lastNudgeAtCall = null;
|
|
@@ -122328,8 +122575,6 @@ function dropIdentityScopedCaches() {
|
|
|
122328
122575
|
transactionsCache = null;
|
|
122329
122576
|
allowlistCache = null;
|
|
122330
122577
|
capabilityIndexCache = null;
|
|
122331
|
-
compoundIndexCache = null;
|
|
122332
|
-
loopIndexCache = null;
|
|
122333
122578
|
taskIndexCache = null;
|
|
122334
122579
|
cardCounts = null;
|
|
122335
122580
|
servicesDiscovered = null;
|
|
@@ -122877,7 +123122,7 @@ ${lines}${footer}${buildPersonaProposalSection(index2.persona)}`;
|
|
|
122877
123122
|
}
|
|
122878
123123
|
var INDEX_MAX_CATEGORIES = 12;
|
|
122879
123124
|
var INDEX_MAX_RECENT = 5;
|
|
122880
|
-
function
|
|
123125
|
+
function buildTaskIndexSection(items, noun) {
|
|
122881
123126
|
if (!items || items.length === 0) return "";
|
|
122882
123127
|
const counts = /* @__PURE__ */ new Map();
|
|
122883
123128
|
for (const it of items) {
|
|
@@ -122901,9 +123146,21 @@ Your workspace has ${total} ${noun}${total === 1 ? "" : "s"} across ${catCount}
|
|
|
122901
123146
|
${catLines}${moreCats}${recentLine}
|
|
122902
123147
|
Call with an intent or category to pull the full body of the one you want.`;
|
|
122903
123148
|
}
|
|
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
|
|
123149
|
+
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
123150
|
function buildGetTaskDescription(items) {
|
|
122906
|
-
return `${GET_TASK_BASE_DESCRIPTION}${
|
|
123151
|
+
return `${GET_TASK_BASE_DESCRIPTION}${buildTaskIndexSection(items, "task")}`;
|
|
123152
|
+
}
|
|
123153
|
+
function pickExactSlugFromIntent(intent, index2, category) {
|
|
123154
|
+
if (!index2 || index2.length === 0) return null;
|
|
123155
|
+
const tokens = new Set(intent.toLowerCase().match(/[a-z0-9-]+/g) ?? []);
|
|
123156
|
+
if (tokens.size === 0) return null;
|
|
123157
|
+
const hits = /* @__PURE__ */ new Set();
|
|
123158
|
+
for (const item of index2) {
|
|
123159
|
+
if (typeof item.slug !== "string" || item.slug === "") continue;
|
|
123160
|
+
if (category !== void 0 && (item.category ?? void 0) !== category) continue;
|
|
123161
|
+
if (tokens.has(item.slug.toLowerCase())) hits.add(item.slug);
|
|
123162
|
+
}
|
|
123163
|
+
return hits.size === 1 ? [...hits][0] : null;
|
|
122907
123164
|
}
|
|
122908
123165
|
var refreshGetAllowlistDescriptionInFlight = false;
|
|
122909
123166
|
var pendingRefreshTimer = null;
|
|
@@ -122982,19 +123239,14 @@ async function refreshDynamicPrompts() {
|
|
|
122982
123239
|
const sdk = await getSDK();
|
|
122983
123240
|
const nativeServer = server.nativeServer;
|
|
122984
123241
|
if (!nativeServer?.registerPrompt) return;
|
|
122985
|
-
const
|
|
122986
|
-
|
|
122987
|
-
|
|
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 };
|
|
123242
|
+
const listRes = await sdk.loops.list(apiKey).catch(() => ({ status: "nok" }));
|
|
123243
|
+
const tasks2 = listRes?.status === "ok" ? listRes.items.filter((l) => l.draft === false) : [];
|
|
123244
|
+
if (listRes?.status === "ok" && Array.isArray(listRes.items)) {
|
|
123245
|
+
cardCounts = { tasks: listRes.items.length };
|
|
122993
123246
|
}
|
|
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
123247
|
const taskBySlug = /* @__PURE__ */ new Map();
|
|
122997
|
-
for (const
|
|
123248
|
+
for (const l of tasks2) {
|
|
123249
|
+
const it = { slug: l.slug, category: l.category, updatedAt: l.updatedAt };
|
|
122998
123250
|
const existing = taskBySlug.get(it.slug);
|
|
122999
123251
|
if (!existing || String(it.updatedAt ?? "") > String(existing.updatedAt ?? "")) taskBySlug.set(it.slug, it);
|
|
123000
123252
|
}
|
|
@@ -123009,8 +123261,7 @@ async function refreshDynamicPrompts() {
|
|
|
123009
123261
|
} catch {
|
|
123010
123262
|
}
|
|
123011
123263
|
const desired = /* @__PURE__ */ new Map();
|
|
123012
|
-
for (const
|
|
123013
|
-
for (const l of loops2) desired.set(promptName("loop", l.slug), { kind: "loop", id: l.id, summary: l });
|
|
123264
|
+
for (const l of tasks2) if (!desired.has(promptName(l.slug))) desired.set(promptName(l.slug), { id: l.id, summary: l });
|
|
123014
123265
|
for (const [name, handle] of dynamicPromptHandles) {
|
|
123015
123266
|
if (!desired.has(name)) {
|
|
123016
123267
|
try {
|
|
@@ -123020,19 +123271,13 @@ async function refreshDynamicPrompts() {
|
|
|
123020
123271
|
dynamicPromptHandles.delete(name);
|
|
123021
123272
|
}
|
|
123022
123273
|
}
|
|
123023
|
-
for (const [name, {
|
|
123274
|
+
for (const [name, { id, summary }] of desired) {
|
|
123024
123275
|
if (dynamicPromptHandles.has(name)) continue;
|
|
123025
|
-
const entry = buildPromptEntry(
|
|
123276
|
+
const entry = buildPromptEntry(summary);
|
|
123026
123277
|
const cb = async () => {
|
|
123027
|
-
|
|
123028
|
-
|
|
123029
|
-
|
|
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: "" });
|
|
123278
|
+
const got = await sdk.loops.get(apiKey, id);
|
|
123279
|
+
const full = got?.status === "ok" ? got.loop : void 0;
|
|
123280
|
+
return buildPromptContent(full ?? { slug: summary.slug, markdownBody: "" });
|
|
123036
123281
|
};
|
|
123037
123282
|
try {
|
|
123038
123283
|
const handle = nativeServer.registerPrompt(name, { description: entry.description, argsSchema: void 0 }, cb);
|
|
@@ -123305,8 +123550,8 @@ function buildPolicyActiveOnchainReader(sdk) {
|
|
|
123305
123550
|
}
|
|
123306
123551
|
};
|
|
123307
123552
|
}
|
|
123308
|
-
var DEFAULT_POLICY_POLL_INTERVAL_MS =
|
|
123309
|
-
var DEFAULT_POLICY_POLL_WINDOW_MS =
|
|
123553
|
+
var DEFAULT_POLICY_POLL_INTERVAL_MS = 5e3;
|
|
123554
|
+
var DEFAULT_POLICY_POLL_WINDOW_MS = 25e3;
|
|
123310
123555
|
function parsePollEnvMs(raw, fallback2) {
|
|
123311
123556
|
const n = Number(raw?.trim());
|
|
123312
123557
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback2;
|
|
@@ -123329,6 +123574,7 @@ var approvalWaitConfig = (() => {
|
|
|
123329
123574
|
delay: (ms) => new Promise((resolve3) => setTimeout(resolve3, ms))
|
|
123330
123575
|
};
|
|
123331
123576
|
})();
|
|
123577
|
+
var pendingAccessRequest = null;
|
|
123332
123578
|
async function tryResolvePendingApproval(probe) {
|
|
123333
123579
|
if (currentCredentials.authorizationStatus === "approved") return true;
|
|
123334
123580
|
if (currentCredentials.authorizationStatus !== "pending" || !currentCredentials.apiKey || !currentCredentials.eoaAddress) {
|
|
@@ -123860,9 +124106,10 @@ server.tool(
|
|
|
123860
124106
|
server.tool(
|
|
123861
124107
|
{
|
|
123862
124108
|
name: "requestAccess",
|
|
123863
|
-
description: "Request wallet authorization with a chosen policy. Sends request to admin for approval.",
|
|
124109
|
+
description: "Request wallet authorization with a chosen policy. Sends the request to the admin for approval and waits BRIEFLY for it (~25 s \u2014 short by design, so this call returns before your MCP host times out). If it comes back `pending`, the request stays valid on the admin's side: call getWalletStatus once the user says it was approved, and do NOT call requestAccess again in a loop. While a request from this session is pending, calling requestAccess again RE-CHECKS that request and never submits a new one; `resubmit: true` is the only way to force a fresh request.",
|
|
123864
124110
|
inputs: [
|
|
123865
|
-
{ name: "policy_id", type: "number", required: true, description: "The policy ID to request access with" }
|
|
124111
|
+
{ name: "policy_id", type: "number", required: true, description: "The policy ID to request access with" },
|
|
124112
|
+
{ name: "resubmit", type: "boolean", required: false, description: "Force a FRESH request even though this session already has one pending (mints a new session key and queues a second approval for the admin). Default false: while a request from this session is pending, requestAccess only re-checks it." }
|
|
123866
124113
|
]
|
|
123867
124114
|
},
|
|
123868
124115
|
async (params) => {
|
|
@@ -123894,6 +124141,56 @@ server.tool(
|
|
|
123894
124141
|
content: [{ type: "text", text: JSON.stringify({ success: false, error: "No API key or wallet address. Run ametyst login, then start_session." }) }]
|
|
123895
124142
|
};
|
|
123896
124143
|
}
|
|
124144
|
+
const windowSeconds = Math.round(resolveApprovalWaitConfigFromEnv().windowMs / 1e3);
|
|
124145
|
+
const approvedResponse = (signerAddress, forPolicyId) => ({
|
|
124146
|
+
content: [{
|
|
124147
|
+
type: "text",
|
|
124148
|
+
text: JSON.stringify({
|
|
124149
|
+
success: true,
|
|
124150
|
+
data: {
|
|
124151
|
+
eoaAddress: signerAddress,
|
|
124152
|
+
policyId: forPolicyId,
|
|
124153
|
+
status: "approved",
|
|
124154
|
+
walletAddress: currentCredentials.walletAddress,
|
|
124155
|
+
message: "Authorization approved. Wallet is ready to spend."
|
|
124156
|
+
},
|
|
124157
|
+
guidance: {
|
|
124158
|
+
say_to_user: "Your admin approved the policy \u2014 the wallet is ready. Go ahead and tell me what you'd like to do.",
|
|
124159
|
+
next_action: "Proceed with the user's original request (e.g. getAllowlist or spend)."
|
|
124160
|
+
}
|
|
124161
|
+
})
|
|
124162
|
+
}]
|
|
124163
|
+
});
|
|
124164
|
+
const pendingResponse = (signerAddress, forPolicyId, requestId, rechecked) => ({
|
|
124165
|
+
content: [{
|
|
124166
|
+
type: "text",
|
|
124167
|
+
text: JSON.stringify({
|
|
124168
|
+
success: true,
|
|
124169
|
+
data: {
|
|
124170
|
+
eoaAddress: signerAddress,
|
|
124171
|
+
policyId: forPolicyId,
|
|
124172
|
+
status: "pending",
|
|
124173
|
+
...requestId !== void 0 ? { requestId } : {},
|
|
124174
|
+
alreadyPending: rechecked,
|
|
124175
|
+
message: rechecked ? `This session already has an authorization request pending${requestId ? ` (id ${requestId})` : ""} \u2014 re-checked just now, still awaiting the admin; nothing new was submitted. It stays valid: call getWalletStatus once your admin approves. Pass resubmit: true only to force a fresh request (it mints a new session key and queues a second approval).` : `Authorization request sent and still pending after the ${windowSeconds}s auto-wait window \u2014 short by design, so this call returns before your MCP host times out. The request stays valid: call getWalletStatus once your admin approves. Do NOT call requestAccess again in a loop \u2014 a repeat call only re-checks this same request without submitting a new one; resubmit: true is the only way to force a fresh request.`
|
|
124176
|
+
},
|
|
124177
|
+
guidance: {
|
|
124178
|
+
say_to_user: rechecked ? "Your authorization request is still pending your admin's approval (I re-checked it just now, no new request was sent). Once they approve it, just ask me again or say 'check wallet status' and I'll pick it up." : "I've requested authorization and it's pending your admin's approval. I waited briefly \u2014 the wait is short by design \u2014 and it hasn't been approved yet, but the request stays valid on their side. Once they approve it, just ask me again or say 'check wallet status' and I'll pick it up. No need to do anything else right now.",
|
|
124179
|
+
next_action: "When the user says the admin approved, call getWalletStatus to sync. Do NOT call requestAccess again in a loop: a repeat call re-checks this pending request and never submits a new one (resubmit: true forces a fresh request \u2014 only when the user asks for one).",
|
|
124180
|
+
stop: true
|
|
124181
|
+
}
|
|
124182
|
+
})
|
|
124183
|
+
}]
|
|
124184
|
+
});
|
|
124185
|
+
if (currentCredentials.authorizationStatus === "pending" && pendingAccessRequest !== null && pendingAccessRequest.walletId === currentCredentials.pendingWalletId && params.resubmit !== true) {
|
|
124186
|
+
const pending = pendingAccessRequest;
|
|
124187
|
+
console.error(`\u{1F501} [requestAccess] A request from this session is already pending${pending.walletId ? ` (id ${pending.walletId})` : ""} \u2014 re-checking, not submitting`);
|
|
124188
|
+
if (await tryResolvePendingApproval()) {
|
|
124189
|
+
pendingAccessRequest = null;
|
|
124190
|
+
return approvedResponse(pending.signerAddress, pending.policyId);
|
|
124191
|
+
}
|
|
124192
|
+
return pendingResponse(pending.signerAddress, pending.policyId, pending.walletId, true);
|
|
124193
|
+
}
|
|
123897
124194
|
const { virtualWalletsManagers: virtualWalletsManagers2 } = await getSDK();
|
|
123898
124195
|
const result = await submitRotatedAccessRequest(
|
|
123899
124196
|
virtualWalletsManagers2,
|
|
@@ -123907,49 +124204,15 @@ server.tool(
|
|
|
123907
124204
|
);
|
|
123908
124205
|
currentCredentials.authorizationStatus = "pending";
|
|
123909
124206
|
currentCredentials.pendingWalletId = result.id ? String(result.id) : void 0;
|
|
124207
|
+
pendingAccessRequest = { walletId: currentCredentials.pendingWalletId, policyId, signerAddress: result.signerAddress };
|
|
123910
124208
|
console.error(`\u2705 [requestAccess] Authorization request sent for ${result.signerAddress}`);
|
|
123911
124209
|
await emitPendingApprovalNotice();
|
|
123912
124210
|
const approved = await autoWaitForApproval();
|
|
123913
124211
|
if (approved) {
|
|
123914
|
-
|
|
123915
|
-
|
|
123916
|
-
type: "text",
|
|
123917
|
-
text: JSON.stringify({
|
|
123918
|
-
success: true,
|
|
123919
|
-
data: {
|
|
123920
|
-
eoaAddress: result.signerAddress,
|
|
123921
|
-
policyId,
|
|
123922
|
-
status: "approved",
|
|
123923
|
-
walletAddress: currentCredentials.walletAddress,
|
|
123924
|
-
message: "Authorization approved. Wallet is ready to spend."
|
|
123925
|
-
},
|
|
123926
|
-
guidance: {
|
|
123927
|
-
say_to_user: "Your admin approved the policy \u2014 the wallet is ready. Go ahead and tell me what you'd like to do.",
|
|
123928
|
-
next_action: "Proceed with the user's original request (e.g. getAllowlist or spend)."
|
|
123929
|
-
}
|
|
123930
|
-
})
|
|
123931
|
-
}]
|
|
123932
|
-
};
|
|
124212
|
+
pendingAccessRequest = null;
|
|
124213
|
+
return approvedResponse(result.signerAddress, policyId);
|
|
123933
124214
|
}
|
|
123934
|
-
return
|
|
123935
|
-
content: [{
|
|
123936
|
-
type: "text",
|
|
123937
|
-
text: JSON.stringify({
|
|
123938
|
-
success: true,
|
|
123939
|
-
data: {
|
|
123940
|
-
eoaAddress: result.signerAddress,
|
|
123941
|
-
policyId,
|
|
123942
|
-
status: "pending",
|
|
123943
|
-
message: "Authorization request sent and still pending after the auto-wait window elapsed. The request is still valid \u2014 call getWalletStatus to re-check once your admin approves, or re-run requestAccess to wait again."
|
|
123944
|
-
},
|
|
123945
|
-
guidance: {
|
|
123946
|
-
say_to_user: "I've requested authorization and waited a while, but your admin hasn't approved it yet. The request is still pending on their side \u2014 once they approve it, just ask me again (or say 'check wallet status') and I'll pick it up. No need to do anything else right now.",
|
|
123947
|
-
next_action: "When the user is ready, call getWalletStatus to re-check approval, or requestAccess to wait again.",
|
|
123948
|
-
stop: true
|
|
123949
|
-
}
|
|
123950
|
-
})
|
|
123951
|
-
}]
|
|
123952
|
-
};
|
|
124215
|
+
return pendingResponse(result.signerAddress, policyId, currentCredentials.pendingWalletId, false);
|
|
123953
124216
|
} catch (error) {
|
|
123954
124217
|
console.error("\u274C Error in requestAccess:", error);
|
|
123955
124218
|
return {
|
|
@@ -124460,25 +124723,34 @@ function suggestedCategoryFromSlug(slug) {
|
|
|
124460
124723
|
const prefix = String(slug ?? "").trim().split(/[-_]/)[0]?.trim();
|
|
124461
124724
|
return prefix ? prefix.toLowerCase() : void 0;
|
|
124462
124725
|
}
|
|
124463
|
-
function categoryGateResponse(params, slug
|
|
124726
|
+
function categoryGateResponse(params, slug) {
|
|
124464
124727
|
if (typeof params.category === "string" && params.category.trim()) return void 0;
|
|
124465
124728
|
const suggestion = suggestedCategoryFromSlug(slug);
|
|
124466
124729
|
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
124730
|
return {
|
|
124470
124731
|
content: [{ type: "text", text: JSON.stringify({
|
|
124471
124732
|
success: false,
|
|
124472
124733
|
error: "category_required",
|
|
124473
124734
|
guidance: {
|
|
124474
|
-
say_to_user: `Before I publish this
|
|
124475
|
-
next_action: `Ask the user for the category, then call
|
|
124735
|
+
say_to_user: `Before I publish this task, which category should it go under?${suggestionText}`,
|
|
124736
|
+
next_action: `Ask the user for the category, then call createTask again with an explicit \`category\`.`,
|
|
124476
124737
|
stop: true
|
|
124477
124738
|
},
|
|
124478
124739
|
...suggestion ? { suggestedCategory: suggestion } : {}
|
|
124479
124740
|
}) }]
|
|
124480
124741
|
};
|
|
124481
124742
|
}
|
|
124743
|
+
function renderSingleTaskMarkdown(task) {
|
|
124744
|
+
const updated = typeof task.updatedAt === "string" && task.updatedAt ? task.updatedAt.slice(0, 10) : "unknown";
|
|
124745
|
+
const catSeg = task.category ? `category: ${task.category} \xB7 ` : "";
|
|
124746
|
+
return `## ${task.slug}
|
|
124747
|
+
|
|
124748
|
+
> ${catSeg}updated ${updated}
|
|
124749
|
+
|
|
124750
|
+
${task.descriptionShort ?? ""}
|
|
124751
|
+
|
|
124752
|
+
${task.markdownBody ?? ""}`;
|
|
124753
|
+
}
|
|
124482
124754
|
async function resolveTasksCore(params, flavor) {
|
|
124483
124755
|
try {
|
|
124484
124756
|
const intent = typeof params.intent === "string" ? params.intent.trim() : "";
|
|
@@ -124487,7 +124759,16 @@ async function resolveTasksCore(params, flavor) {
|
|
|
124487
124759
|
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: "No API key configured." }) }] };
|
|
124488
124760
|
}
|
|
124489
124761
|
const sdk = await getSDK();
|
|
124490
|
-
|
|
124762
|
+
let exactTask = null;
|
|
124763
|
+
if (flavor.fetchExact && intent !== "") {
|
|
124764
|
+
if (!taskIndexCache || taskIndexCache.length === 0) await refreshDynamicPrompts();
|
|
124765
|
+
const slug = pickExactSlugFromIntent(intent, taskIndexCache, category);
|
|
124766
|
+
if (slug) {
|
|
124767
|
+
const got = await flavor.fetchExact(sdk, currentCredentials.apiKey, slug).catch(() => null);
|
|
124768
|
+
if (got?.status === "ok" && got.task && typeof got.task.slug === "string") exactTask = got.task;
|
|
124769
|
+
}
|
|
124770
|
+
}
|
|
124771
|
+
const res = exactTask ? { status: "ok", markdown: renderSingleTaskMarkdown(exactTask), markdownBody: String(exactTask.markdownBody ?? "") } : await flavor.resolve(sdk, currentCredentials.apiKey, intent, category);
|
|
124491
124772
|
if (res.status === "ok") {
|
|
124492
124773
|
const payload = {
|
|
124493
124774
|
success: true,
|
|
@@ -124501,7 +124782,7 @@ async function resolveTasksCore(params, flavor) {
|
|
|
124501
124782
|
if (spilled.filePath) payload.markdownBodyFilePath = spilled.filePath;
|
|
124502
124783
|
else if (typeof spilled.inline === "string") payload.markdownBody = spilled.inline;
|
|
124503
124784
|
if (slug && flavor.fetchManifest) {
|
|
124504
|
-
const manifest = await flavor.fetchManifest(sdk, currentCredentials.apiKey, slug);
|
|
124785
|
+
const manifest = exactTask ? normalizeMemoryManifest(exactTask.stateDocs ?? null) : await flavor.fetchManifest(sdk, currentCredentials.apiKey, slug);
|
|
124505
124786
|
if (manifest) payload.stateDocs = manifest;
|
|
124506
124787
|
if (manifest !== void 0) payload.memory = memorySummaryLine(manifest);
|
|
124507
124788
|
}
|
|
@@ -124534,12 +124815,16 @@ server.tool(
|
|
|
124534
124815
|
resolve: (sdk, apiKey, intent, category) => sdk.tasks.resolve(apiKey, intent, category),
|
|
124535
124816
|
// Same wording as the slash-prompt directive (src/tasks/task-run-section.ts): after the pick,
|
|
124536
124817
|
// 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.",
|
|
124818
|
+
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
124819
|
resolverFailedSayToUser: "Couldn't search tasks right now.",
|
|
124539
124820
|
surfaceRawBodies: true,
|
|
124540
|
-
fetchManifest: (sdk, apiKey, slug) => readTaskManifest(sdk, apiKey, slug)
|
|
124821
|
+
fetchManifest: (sdk, apiKey, slug) => readTaskManifest(sdk, apiKey, slug),
|
|
124822
|
+
fetchExact: (sdk, apiKey, slug) => sdk.tasks.get(apiKey, slug)
|
|
124541
124823
|
})
|
|
124542
124824
|
);
|
|
124825
|
+
function shellQuote2(s) {
|
|
124826
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
124827
|
+
}
|
|
124543
124828
|
function taskMemoryBlock(slug, docKey) {
|
|
124544
124829
|
const listRecords = `taskMemoryGet({ taskSlug: "${slug}", records: true, kind: "<kind>" })`;
|
|
124545
124830
|
return {
|
|
@@ -124562,7 +124847,19 @@ function closeAllLiveDashboards() {
|
|
|
124562
124847
|
liveDashboards.clear();
|
|
124563
124848
|
}
|
|
124564
124849
|
process.once("exit", closeAllLiveDashboards);
|
|
124565
|
-
|
|
124850
|
+
function shutdownForHostClose() {
|
|
124851
|
+
try {
|
|
124852
|
+
fireAutoShareOnShutdown();
|
|
124853
|
+
} catch {
|
|
124854
|
+
}
|
|
124855
|
+
if (liveUnlockListener) {
|
|
124856
|
+
void liveUnlockListener.close("closed").catch(() => {
|
|
124857
|
+
});
|
|
124858
|
+
liveUnlockListener = null;
|
|
124859
|
+
}
|
|
124860
|
+
closeAllLiveDashboards();
|
|
124861
|
+
}
|
|
124862
|
+
async function startLiveDashboard(entity, dir, sdk, apiKey) {
|
|
124566
124863
|
const slug = typeof entity?.slug === "string" ? entity.slug : "";
|
|
124567
124864
|
const html = entity?.dashboardHtml;
|
|
124568
124865
|
if (typeof html !== "string" || !html) return null;
|
|
@@ -124573,8 +124870,11 @@ async function startLiveDashboard(entity, dir) {
|
|
|
124573
124870
|
previous.close();
|
|
124574
124871
|
}
|
|
124575
124872
|
const handle = await startDashboardServer({
|
|
124576
|
-
|
|
124577
|
-
|
|
124873
|
+
task: { slug, dashboardHtml: html, dashboardManifest: entity?.dashboardManifest },
|
|
124874
|
+
runDir: dir,
|
|
124875
|
+
// `/data` reads the manifest-named docs from Ametyst MEMORY first — the run writes there
|
|
124876
|
+
// through taskMemoryAppend and touches the local file only at exit — then the file.
|
|
124877
|
+
readDoc: memoryDocResolver(sdk, apiKey, slug, normalizeMemoryManifest(entity?.stateDocs ?? null)),
|
|
124578
124878
|
deps: { log: (line) => console.error(line) }
|
|
124579
124879
|
});
|
|
124580
124880
|
if (!handle) return null;
|
|
@@ -124617,16 +124917,20 @@ async function runTaskCore(params, flavor) {
|
|
|
124617
124917
|
guidance: { say_to_user: flavor.missingRefSayToUser, next_action: flavor.missingRefNextAction }
|
|
124618
124918
|
}) }] };
|
|
124619
124919
|
}
|
|
124620
|
-
const
|
|
124621
|
-
|
|
124622
|
-
|
|
124623
|
-
|
|
124624
|
-
|
|
124625
|
-
|
|
124626
|
-
|
|
124627
|
-
|
|
124628
|
-
|
|
124629
|
-
|
|
124920
|
+
const input = typeof params.input === "string" && params.input.trim() !== "" ? params.input : void 0;
|
|
124921
|
+
const headless = (modeSource2) => {
|
|
124922
|
+
const command = flavor.headlessCommand(ref, input);
|
|
124923
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
124924
|
+
success: true,
|
|
124925
|
+
mode: "headless",
|
|
124926
|
+
...modeSource2 ? { modeSource: modeSource2 } : {},
|
|
124927
|
+
command,
|
|
124928
|
+
guidance: {
|
|
124929
|
+
say_to_user: flavor.headlessSayToUser(ref, command),
|
|
124930
|
+
next_action: "Tell the user to run the command in a shell."
|
|
124931
|
+
}
|
|
124932
|
+
}) }] };
|
|
124933
|
+
};
|
|
124630
124934
|
const invalidMode = (provided) => ({ content: [{ type: "text", text: JSON.stringify({
|
|
124631
124935
|
success: false,
|
|
124632
124936
|
error: "invalid_mode",
|
|
@@ -124664,13 +124968,15 @@ async function runTaskCore(params, flavor) {
|
|
|
124664
124968
|
`(${entity.slug}: run folder anchored on the fallback ${runRoot.root} \u2014 ${(runRoot.rejected ?? []).map((r) => `${r.dir}: ${r.why}`).join("; ")})`
|
|
124665
124969
|
);
|
|
124666
124970
|
}
|
|
124971
|
+
const legacyHint = legacyRunFolderHint(entity.slug, runRoot.root);
|
|
124972
|
+
if (legacyHint) console.error(legacyHint);
|
|
124667
124973
|
const materialized = flavor.materialize(entity, randomUUID4(), runRoot.root);
|
|
124668
124974
|
const docBoot = await materializeMemoryDocs(sdk, apiKey, entity, materialized.dir);
|
|
124669
124975
|
for (const note of docBoot.notes) console.error(`(${entity.slug} memory docs: ${note})`);
|
|
124670
124976
|
const runFiles = { ...materialized.files, ...docBoot.files };
|
|
124671
124977
|
const est = estimateBlastRadius(entity);
|
|
124672
|
-
const shipBack = flavor.buildShipBack({ dir: materialized.dir, entity });
|
|
124673
|
-
const dashboardUrl = await startLiveDashboard(entity, materialized.dir);
|
|
124978
|
+
const shipBack = await flavor.buildShipBack({ dir: materialized.dir, entity, sdk, apiKey });
|
|
124979
|
+
const dashboardUrl = await startLiveDashboard(entity, materialized.dir, sdk, apiKey);
|
|
124674
124980
|
const dashboardLine = dashboardUrl ? `Live dashboard: ${dashboardUrl}` : NO_DASHBOARD_MESSAGE;
|
|
124675
124981
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
124676
124982
|
success: true,
|
|
@@ -124690,7 +124996,9 @@ async function runTaskCore(params, flavor) {
|
|
|
124690
124996
|
// the slug because the run context carries no task identity — which is exactly why
|
|
124691
124997
|
// the taskMemory* tools take an explicit `taskSlug`.
|
|
124692
124998
|
memory: taskMemoryBlock(entity.slug, memoryDocKey),
|
|
124693
|
-
|
|
124999
|
+
// The user's arguments lead the directive, in the same LAUNCH INPUT block the headless
|
|
125000
|
+
// launcher prepends to its prompt — so both surfaces hand them over in the same words.
|
|
125001
|
+
directive: launchInputBlock(input) + flavor.buildDirective({ dir: materialized.dir, slug: entity.slug, files: runFiles, docKey: memoryDocKey, runRoot }),
|
|
124694
125002
|
...shipBack ? { shipBack } : {}
|
|
124695
125003
|
}) }, { type: "text", text: dashboardLine }] };
|
|
124696
125004
|
} catch (error) {
|
|
@@ -124701,11 +125009,12 @@ async function runTaskCore(params, flavor) {
|
|
|
124701
125009
|
server.tool(
|
|
124702
125010
|
{
|
|
124703
125011
|
name: "runTask",
|
|
124704
|
-
description: "Run a workspace TASK after the user has confirmed it (getTask already presented + stopped). A task is the
|
|
125012
|
+
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
125013
|
inputs: [
|
|
124706
125014
|
{ name: "task", type: "string", required: true, description: "Slug or id of the task to run (as returned by getTask)." },
|
|
124707
125015
|
{ 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)." }
|
|
125016
|
+
{ 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)." },
|
|
125017
|
+
{ 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
125018
|
]
|
|
124710
125019
|
},
|
|
124711
125020
|
async (params) => runTaskCore(params, {
|
|
@@ -124717,8 +125026,8 @@ server.tool(
|
|
|
124717
125026
|
notFoundError: "task_not_found",
|
|
124718
125027
|
fetch: (sdk, apiKey, ref) => sdk.tasks.get(apiKey, ref),
|
|
124719
125028
|
pick: (res) => res.task,
|
|
124720
|
-
headlessCommand: (ref) => `ametyst task run ${ref}`,
|
|
124721
|
-
headlessSayToUser: (
|
|
125029
|
+
headlessCommand: (ref, input) => `ametyst task run ${ref}${input === void 0 ? "" : ` --input ${shellQuote2(input)}`}`,
|
|
125030
|
+
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. If you add \`--max-budget-usd <x>\`, that caps the MODEL's token spend for the headless run (it is forwarded to \`claude -p\`); merchant payments are bounded by your on-chain policy, not by that flag.`,
|
|
124722
125031
|
honorFrontmatterDefault: true,
|
|
124723
125032
|
materialize: (task, runId, runRoot) => materializeTask(task, runId, runRoot),
|
|
124724
125033
|
buildDirective: ({ dir, slug, files, docKey, runRoot }) => {
|
|
@@ -124726,7 +125035,7 @@ server.tool(
|
|
|
124726
125035
|
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
125036
|
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
125037
|
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
|
|
125038
|
+
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
125039
|
|
|
124731
125040
|
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
125041
|
|
|
@@ -124737,7 +125046,22 @@ ${TASK_MEMORY_MODEL_SECTION}`;
|
|
|
124737
125046
|
// The `dir` cleanup does NOT live here: it is owed by EVERY in-chat run, and a task
|
|
124738
125047
|
// with no constraints gets no ship-back at all — so it is stated once, in the
|
|
124739
125048
|
// directive. This sentence only orders the two (ship back, then clean up).
|
|
124740
|
-
|
|
125049
|
+
//
|
|
125050
|
+
// ⛔ ONLY THE OWNER CAN SHIP BACK. buyer-api lets the row's creator or the workspace admin
|
|
125051
|
+
// modify a task; a member running an admin-owned task was told to `createTask` anyway,
|
|
125052
|
+
// tripped the category gate first and then got a 403. When ownership resolves and the
|
|
125053
|
+
// caller is not the owner, the directive says where the learnings go instead. When the
|
|
125054
|
+
// caller IS the owner, the call names the task's existing `category` so a MODIFY does not
|
|
125055
|
+
// trip `category_required`. Unresolvable → today's directive.
|
|
125056
|
+
buildShipBack: async ({ entity, sdk, apiKey }) => {
|
|
125057
|
+
if (!(typeof entity?.constraintsMd === "string" && entity.constraintsMd.length > 0)) return void 0;
|
|
125058
|
+
const owner = await resolveTaskOwnership(sdk, apiKey, entity);
|
|
125059
|
+
if (owner.resolved && !owner.isOwner) {
|
|
125060
|
+
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.`;
|
|
125061
|
+
}
|
|
125062
|
+
const category = typeof entity?.category === "string" && entity.category.trim() ? `, category="${entity.category}"` : "";
|
|
125063
|
+
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.`;
|
|
125064
|
+
}
|
|
124741
125065
|
})
|
|
124742
125066
|
);
|
|
124743
125067
|
function taskMemoryContext(params) {
|
|
@@ -124757,7 +125081,7 @@ function taskMemoryContext(params) {
|
|
|
124757
125081
|
error: "taskSlug_required",
|
|
124758
125082
|
guidance: {
|
|
124759
125083
|
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
|
|
125084
|
+
next_action: "Pass taskSlug explicitly \u2014 it is the slug of the task you are running (also in AMETYST_TASK_SLUG)."
|
|
124761
125085
|
}
|
|
124762
125086
|
}) }] }
|
|
124763
125087
|
};
|
|
@@ -124854,7 +125178,7 @@ server.tool(
|
|
|
124854
125178
|
name: "taskMemoryAppend",
|
|
124855
125179
|
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
125180
|
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
|
|
125181
|
+
{ 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
125182
|
{ 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
125183
|
{ 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
125184
|
{ 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 +125287,7 @@ server.tool(
|
|
|
124963
125287
|
name: "taskMemoryGet",
|
|
124964
125288
|
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
125289
|
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
|
|
125290
|
+
{ 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
125291
|
{ 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
125292
|
{ 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
125293
|
{ 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 +125481,7 @@ server.tool(
|
|
|
125157
125481
|
name: "taskMemoryArchive",
|
|
125158
125482
|
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
125483
|
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
|
|
125484
|
+
{ 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
125485
|
{ name: "key", type: "string", required: true, description: "The item's key, exactly as written (case-sensitive)." },
|
|
125162
125486
|
{ name: "note", type: "string", required: false, description: 'Optional archive note \u2014 why it is closed ("shipped in #212", "superseded by pbi-9").' }
|
|
125163
125487
|
]
|
|
@@ -125235,7 +125559,7 @@ async function upsertTaskCore(params, flavor) {
|
|
|
125235
125559
|
void refreshDynamicPrompts();
|
|
125236
125560
|
const mode2 = id ? "modified" : "created";
|
|
125237
125561
|
const effectiveManifest = "stateDocs" in entity ? entity.stateDocs : !id ? null : previous && previous.available ? previous.content.stateDocs ?? null : void 0;
|
|
125238
|
-
const summary =
|
|
125562
|
+
const summary = buildTaskUpsertSummary({
|
|
125239
125563
|
mode: mode2,
|
|
125240
125564
|
sent: entity,
|
|
125241
125565
|
previous,
|
|
@@ -125244,7 +125568,7 @@ async function upsertTaskCore(params, flavor) {
|
|
|
125244
125568
|
const payload = enforceResponseCap({
|
|
125245
125569
|
success: true,
|
|
125246
125570
|
mode: mode2,
|
|
125247
|
-
[flavor.responseKey]:
|
|
125571
|
+
[flavor.responseKey]: projectTaskIdentity(flavor.pick(res)),
|
|
125248
125572
|
// WHERE IT LANDED, on the receipt itself. The 2026-08-31 incident put a
|
|
125249
125573
|
// task into the wrong (admin) workspace and the success payload named
|
|
125250
125574
|
// no workspace at all, so neither the agent nor the human reading the
|
|
@@ -125298,7 +125622,7 @@ async function upsertTaskCore(params, flavor) {
|
|
|
125298
125622
|
["visionMd", "visionFilePath"],
|
|
125299
125623
|
["constraintsMd", "constraintsFilePath"],
|
|
125300
125624
|
["readmeMd", "readmeFilePath"],
|
|
125301
|
-
// Per-
|
|
125625
|
+
// Per-task dashboard (loop-run D12): same inline-or-file verbatim plumbing as the
|
|
125302
125626
|
// *Md files — the bytes reach the SDK CreateLoopInput unchanged.
|
|
125303
125627
|
["dashboardHtml", "dashboardHtmlFilePath"],
|
|
125304
125628
|
["dashboardManifest", "dashboardManifestFilePath"]
|
|
@@ -125331,7 +125655,7 @@ async function upsertTaskCore(params, flavor) {
|
|
|
125331
125655
|
}) }] };
|
|
125332
125656
|
}
|
|
125333
125657
|
}
|
|
125334
|
-
const gate = categoryGateResponse(params, slug
|
|
125658
|
+
const gate = categoryGateResponse(params, slug);
|
|
125335
125659
|
if (gate) return gate;
|
|
125336
125660
|
let graphJson;
|
|
125337
125661
|
if (graphJsonProvided) {
|
|
@@ -125384,10 +125708,10 @@ async function upsertTaskCore(params, flavor) {
|
|
|
125384
125708
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
125385
125709
|
success: false,
|
|
125386
125710
|
redirect: "task-architect",
|
|
125387
|
-
reason: "a new
|
|
125711
|
+
reason: "a new task needs the qualification gate, brakes, status vocabulary and dashboard that the architect interview produces",
|
|
125388
125712
|
guidance: {
|
|
125389
|
-
say_to_user: "I can't spin up a
|
|
125390
|
-
next_action: 'Call getTask({ intent: "create a new
|
|
125713
|
+
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.",
|
|
125714
|
+
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
125715
|
stop: true
|
|
125392
125716
|
}
|
|
125393
125717
|
}) }] };
|
|
@@ -125401,7 +125725,7 @@ async function upsertTaskCore(params, flavor) {
|
|
|
125401
125725
|
server.tool(
|
|
125402
125726
|
{
|
|
125403
125727
|
name: "createTask",
|
|
125404
|
-
description: "Create OR modify a workspace TASK (upsert). A task is the
|
|
125728
|
+
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
125729
|
inputs: [
|
|
125406
125730
|
{ name: "slug", type: "string", required: false, description: "URL-safe unique slug for the task within the workspace. Required on CREATE." },
|
|
125407
125731
|
{ name: "descriptionShort", type: "string", required: false, description: "One-line description of what the task does. Required on CREATE." },
|
|
@@ -125427,7 +125751,6 @@ server.tool(
|
|
|
125427
125751
|
async (params) => upsertTaskCore(params, {
|
|
125428
125752
|
toolName: "createTask",
|
|
125429
125753
|
responseKey: "task",
|
|
125430
|
-
gateKind: "task",
|
|
125431
125754
|
get: (sdk, apiKey, id) => sdk.tasks.get(apiKey, id),
|
|
125432
125755
|
// `body` is assembled field-by-field in `upsertTaskCore` precisely so MODIFY stays a
|
|
125433
125756
|
// no-clobber PATCH, so it cannot statically satisfy `CreateTaskInput`'s required
|
|
@@ -126616,6 +126939,18 @@ async function startMCPServer(walletKeystoreJson, eoaAddress, config, versionNot
|
|
|
126616
126939
|
}
|
|
126617
126940
|
};
|
|
126618
126941
|
}
|
|
126942
|
+
if (hooks?.onTransportClosed) {
|
|
126943
|
+
const inner = server.nativeServer.server;
|
|
126944
|
+
const previousOnClose = inner.onclose;
|
|
126945
|
+
inner.onclose = () => {
|
|
126946
|
+
previousOnClose?.();
|
|
126947
|
+
try {
|
|
126948
|
+
hooks.onTransportClosed?.();
|
|
126949
|
+
} catch (err) {
|
|
126950
|
+
console.error(`\u26A0\uFE0F onTransportClosed hook failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
126951
|
+
}
|
|
126952
|
+
};
|
|
126953
|
+
}
|
|
126619
126954
|
const transport = new StdioServerTransport();
|
|
126620
126955
|
await server.nativeServer.connect(transport);
|
|
126621
126956
|
console.error("\u2705 MCP Server started on stdio");
|
|
@@ -126667,6 +127002,39 @@ function delegateAllowanceRef(merchantSlug, capability) {
|
|
|
126667
127002
|
return { permissionId, commerceInfoId };
|
|
126668
127003
|
}
|
|
126669
127004
|
|
|
127005
|
+
// src/commands/serve-lifecycle.ts
|
|
127006
|
+
init_esm_shims();
|
|
127007
|
+
var HOST_CLOSE_GRACE_MS = 1e3;
|
|
127008
|
+
var PARENT_WATCHDOG_INTERVAL_MS = 3e4;
|
|
127009
|
+
var HOST_CLOSED_MESSAGE = "ametyst serve: host closed the session \u2014 exiting";
|
|
127010
|
+
function armHostLifecycle(deps) {
|
|
127011
|
+
const graceMs = deps.graceMs ?? HOST_CLOSE_GRACE_MS;
|
|
127012
|
+
const watchdogMs = deps.watchdogIntervalMs ?? PARENT_WATCHDOG_INTERVAL_MS;
|
|
127013
|
+
let fired = false;
|
|
127014
|
+
const hostClosed = (reason) => {
|
|
127015
|
+
if (fired) return;
|
|
127016
|
+
fired = true;
|
|
127017
|
+
deps.log(`${HOST_CLOSED_MESSAGE} (${reason})`);
|
|
127018
|
+
try {
|
|
127019
|
+
void Promise.resolve(deps.shutdown()).catch(() => {
|
|
127020
|
+
});
|
|
127021
|
+
} catch {
|
|
127022
|
+
}
|
|
127023
|
+
setTimeout(() => deps.exit(0), graceMs);
|
|
127024
|
+
};
|
|
127025
|
+
deps.stdin.once("end", () => hostClosed("stdin ended"));
|
|
127026
|
+
deps.stdin.once("close", () => hostClosed("stdin closed"));
|
|
127027
|
+
const watchdog = setInterval(() => {
|
|
127028
|
+
if (deps.getPpid() === 1) hostClosed("parent process gone");
|
|
127029
|
+
}, watchdogMs);
|
|
127030
|
+
watchdog.unref?.();
|
|
127031
|
+
return {
|
|
127032
|
+
hostClosed,
|
|
127033
|
+
triggered: () => fired,
|
|
127034
|
+
dispose: () => clearInterval(watchdog)
|
|
127035
|
+
};
|
|
127036
|
+
}
|
|
127037
|
+
|
|
126670
127038
|
// src/commands/serve.ts
|
|
126671
127039
|
init_version5();
|
|
126672
127040
|
|
|
@@ -126800,7 +127168,7 @@ import { existsSync as existsSync15 } from "fs";
|
|
|
126800
127168
|
import { homedir as homedir12 } from "os";
|
|
126801
127169
|
import { join as join19 } from "path";
|
|
126802
127170
|
|
|
126803
|
-
// src/
|
|
127171
|
+
// src/tasks/sync-skills.ts
|
|
126804
127172
|
init_esm_shims();
|
|
126805
127173
|
init_paths();
|
|
126806
127174
|
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 +127202,7 @@ async function fetchSelectionContext(sdk, apiKey) {
|
|
|
126834
127202
|
}
|
|
126835
127203
|
}
|
|
126836
127204
|
function buildStubContent(item) {
|
|
126837
|
-
const {
|
|
127205
|
+
const { slug, descriptionShort } = item;
|
|
126838
127206
|
return `---
|
|
126839
127207
|
name: ${slug}
|
|
126840
127208
|
description: ${JSON.stringify(descriptionShort)}
|
|
@@ -126842,9 +127210,9 @@ description: ${JSON.stringify(descriptionShort)}
|
|
|
126842
127210
|
|
|
126843
127211
|
${MANAGED_MARKER}
|
|
126844
127212
|
|
|
126845
|
-
This is a pointer to the Ametyst
|
|
127213
|
+
This is a pointer to the Ametyst task \`${slug}\` \u2014 the real task lives in the Ametyst workspace, not in this file.
|
|
126846
127214
|
|
|
126847
|
-
${taskRunSection(slug
|
|
127215
|
+
${taskRunSection(slug)}`;
|
|
126848
127216
|
}
|
|
126849
127217
|
function isManaged(file) {
|
|
126850
127218
|
try {
|
|
@@ -126925,27 +127293,15 @@ async function syncSkills(opts = {}) {
|
|
|
126925
127293
|
const global2 = scope.global;
|
|
126926
127294
|
const fallback2 = "fallback" in scope ? scope.fallback : void 0;
|
|
126927
127295
|
const { sdk, apiKey } = await getCliSdk();
|
|
126928
|
-
const
|
|
126929
|
-
if (
|
|
126930
|
-
|
|
126931
|
-
|
|
126932
|
-
|
|
126933
|
-
|
|
126934
|
-
|
|
126935
|
-
|
|
126936
|
-
|
|
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
|
-
];
|
|
127296
|
+
const listRes = await sdk.loops.list(apiKey);
|
|
127297
|
+
if (listRes?.status !== "ok") throw new Error(`failed to list tasks: ${listRes?.error ?? "unknown error"}`);
|
|
127298
|
+
const allItems = listRes.items.filter((l) => l.draft === false).map((l) => ({
|
|
127299
|
+
slug: l.slug,
|
|
127300
|
+
descriptionShort: l.descriptionShort,
|
|
127301
|
+
id: l.id ?? "",
|
|
127302
|
+
category: l.category ?? null,
|
|
127303
|
+
createdBy: l.createdBy ?? ""
|
|
127304
|
+
}));
|
|
126949
127305
|
const items = applySyncSelection(allItems, await fetchSelectionContext(sdk, apiKey));
|
|
126950
127306
|
const desired = /* @__PURE__ */ new Map();
|
|
126951
127307
|
for (const item of items) {
|
|
@@ -127071,9 +127427,21 @@ async function serveCommand() {
|
|
|
127071
127427
|
console.error(
|
|
127072
127428
|
persisted ? isPlaintextVault(persisted) ? "Starting MCP server... (persisted UNENCRYPTED wallet found \u2014 always unlocked, no start_session needed)" : "Starting MCP server... (persisted wallet found \u2014 unlock with start_session)" : "Starting MCP server... (no saved wallet \u2014 one will be created on start_session)"
|
|
127073
127429
|
);
|
|
127430
|
+
let bridge = null;
|
|
127431
|
+
const lifecycle = armHostLifecycle({
|
|
127432
|
+
stdin: process.stdin,
|
|
127433
|
+
getPpid: () => process.ppid,
|
|
127434
|
+
shutdown: async () => {
|
|
127435
|
+
shutdownForHostClose();
|
|
127436
|
+
if (bridge) await bridge.close();
|
|
127437
|
+
},
|
|
127438
|
+
exit: (code) => process.exit(code),
|
|
127439
|
+
log: (message) => console.error(message)
|
|
127440
|
+
});
|
|
127074
127441
|
await startMCPServer(keystore, eoa, { ...config, apiKey, apiKeySource: resolved.source ?? void 0 }, versionNotice, {
|
|
127442
|
+
onTransportClosed: () => lifecycle.hostClosed("transport closed"),
|
|
127075
127443
|
// Best-effort: materialize local `/`-command pointer skills for every published
|
|
127076
|
-
//
|
|
127444
|
+
// task so they're available without a manual `ametyst task sync-skills`.
|
|
127077
127445
|
// Deferred from boot to the MCP initialize handshake so the sync is CLIENT-AWARE:
|
|
127078
127446
|
// clientInfo.name tells us whether the host reads `.claude/skills` (Claude) or
|
|
127079
127447
|
// `.codex/skills` (Codex); when it doesn't, presence detection picks the roots.
|
|
@@ -127085,14 +127453,15 @@ async function serveCommand() {
|
|
|
127085
127453
|
console.error("\u{1F512} delegate bridge not started \u2014 this MCP server is running inside a delegated run");
|
|
127086
127454
|
return;
|
|
127087
127455
|
}
|
|
127088
|
-
|
|
127456
|
+
bridge = await startDelegateBridge({
|
|
127089
127457
|
spend: invokeSpendTool,
|
|
127090
127458
|
isUnlocked: isWalletUnlockedForDelegate,
|
|
127091
127459
|
resolveAllowanceRef: () => delegateAllowanceRef(DELEGATE_MERCHANT_SLUG, DELEGATE_CAPABILITY),
|
|
127092
127460
|
version: CLI_VERSION
|
|
127093
127461
|
});
|
|
127094
127462
|
if (bridge) {
|
|
127095
|
-
const
|
|
127463
|
+
const bound = bridge;
|
|
127464
|
+
const closeBridge = () => void bound.close().catch(() => {
|
|
127096
127465
|
});
|
|
127097
127466
|
process.on("exit", closeBridge);
|
|
127098
127467
|
process.on("SIGINT", closeBridge);
|
|
@@ -127667,204 +128036,87 @@ connectionsCommand.command("add <provider>").description(
|
|
|
127667
128036
|
"OAuth providers only: register a new client with the provider instead of reusing the one this machine already registered"
|
|
127668
128037
|
).action(
|
|
127669
128038
|
(provider, opts) => connectionsAddCommand(provider, opts)
|
|
127670
|
-
);
|
|
127671
|
-
connectionsCommand.command("list").description("List stored connections. Shows identity only \u2014 never a credential.").action(connectionsListCommand);
|
|
127672
|
-
connectionsCommand.command("refresh").description(
|
|
127673
|
-
"Re-download the connector catalog from Ametyst. A provider Ametyst stopped offering disappears from this machine here"
|
|
127674
|
-
).action(connectionsRefreshCommand);
|
|
127675
|
-
connectionsCommand.command("providers").description(
|
|
127676
|
-
"List the providers this cli read from the last catalog download \u2014 not necessarily everything Ametyst offers; 'connections refresh' downloads live"
|
|
127677
|
-
).action(connectionsProvidersCommand);
|
|
127678
|
-
connectionsCommand.command("remove <provider>").description("Delete a stored connection's credential from the OS keychain").option("--name <name>", "Which connection to remove when the provider has more than one").action((provider, opts) => connectionsRemoveCommand(provider, opts));
|
|
127679
|
-
|
|
127680
|
-
// src/commands/task.ts
|
|
127681
|
-
init_esm_shims();
|
|
127682
|
-
|
|
127683
|
-
// src/
|
|
127684
|
-
init_esm_shims();
|
|
127685
|
-
|
|
127686
|
-
// src/
|
|
127687
|
-
init_esm_shims();
|
|
127688
|
-
init_paths();
|
|
127689
|
-
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync11 } from "fs";
|
|
127690
|
-
import { join as join20 } from "path";
|
|
127691
|
-
var MIRRORED_DEFINITION_FILES = ["SKILL.md", "VISION.md", "README.md"];
|
|
127692
|
-
function materialize(
|
|
127693
|
-
const dir =
|
|
127694
|
-
mkdirSync10(dir, { recursive: true, mode: 448 });
|
|
127695
|
-
for (const doc of stateDocs) {
|
|
127696
|
-
writeFileSync11(join20(dir, doc.filename), doc.body, { mode: 384 });
|
|
127697
|
-
}
|
|
127698
|
-
const files = {
|
|
127699
|
-
"SKILL.md": loop2.markdownBody ?? "",
|
|
127700
|
-
"VISION.md": loop2.visionMd ?? "",
|
|
127701
|
-
"CONSTRAINTS.md": loop2.constraintsMd ?? "",
|
|
127702
|
-
"README.md": loop2.readmeMd ?? ""
|
|
127703
|
-
};
|
|
127704
|
-
if (typeof loop2.dashboardHtml === "string" && loop2.dashboardHtml) {
|
|
127705
|
-
files["dashboard.html"] = loop2.dashboardHtml;
|
|
127706
|
-
}
|
|
127707
|
-
if (typeof loop2.dashboardManifest === "string" && loop2.dashboardManifest) {
|
|
127708
|
-
files["dashboard.manifest.json"] = loop2.dashboardManifest;
|
|
127709
|
-
}
|
|
127710
|
-
for (const [name, body] of Object.entries(files)) {
|
|
127711
|
-
writeFileSync11(join20(dir, name), body, { mode: 384 });
|
|
127712
|
-
}
|
|
127713
|
-
writeFileSync11(
|
|
127714
|
-
join20(dir, "STATUS.md"),
|
|
127715
|
-
`# STATUS \u2014 ${loop2.slug}
|
|
127716
|
-
|
|
127717
|
-
loop_id: ${loop2.id}
|
|
127718
|
-
fire_id: ${fireId}
|
|
127719
|
-
started: pending
|
|
127720
|
-
queue: not started
|
|
127721
|
-
`,
|
|
127722
|
-
{ mode: 384 }
|
|
127723
|
-
);
|
|
127724
|
-
mirrorDefinitionFiles(loop2.slug, files, runRoot);
|
|
127725
|
-
return dir;
|
|
127726
|
-
}
|
|
127727
|
-
function mirrorDefinitionFiles(slug, files, runRoot) {
|
|
127728
|
-
try {
|
|
127729
|
-
const root2 = loopDir(slug, runRoot);
|
|
127730
|
-
mkdirSync10(root2, { recursive: true, mode: 448 });
|
|
127731
|
-
for (const name of [...MIRRORED_DEFINITION_FILES, "dashboard.html", "dashboard.manifest.json"]) {
|
|
127732
|
-
const body = files[name];
|
|
127733
|
-
if (body === void 0) continue;
|
|
127734
|
-
writeFileSync11(join20(root2, name), body, { mode: 384 });
|
|
127735
|
-
}
|
|
127736
|
-
} catch {
|
|
127737
|
-
}
|
|
127738
|
-
}
|
|
127739
|
-
|
|
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));
|
|
128039
|
+
);
|
|
128040
|
+
connectionsCommand.command("list").description("List stored connections. Shows identity only \u2014 never a credential.").action(connectionsListCommand);
|
|
128041
|
+
connectionsCommand.command("refresh").description(
|
|
128042
|
+
"Re-download the connector catalog from Ametyst. A provider Ametyst stopped offering disappears from this machine here"
|
|
128043
|
+
).action(connectionsRefreshCommand);
|
|
128044
|
+
connectionsCommand.command("providers").description(
|
|
128045
|
+
"List the providers this cli read from the last catalog download \u2014 not necessarily everything Ametyst offers; 'connections refresh' downloads live"
|
|
128046
|
+
).action(connectionsProvidersCommand);
|
|
128047
|
+
connectionsCommand.command("remove <provider>").description("Delete a stored connection's credential from the OS keychain").option("--name <name>", "Which connection to remove when the provider has more than one").action((provider, opts) => connectionsRemoveCommand(provider, opts));
|
|
128048
|
+
|
|
128049
|
+
// src/commands/task.ts
|
|
128050
|
+
init_esm_shims();
|
|
128051
|
+
|
|
128052
|
+
// src/tasks/index.ts
|
|
128053
|
+
init_esm_shims();
|
|
128054
|
+
|
|
128055
|
+
// src/tasks/materialize.ts
|
|
128056
|
+
init_esm_shims();
|
|
128057
|
+
init_paths();
|
|
128058
|
+
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync11 } from "fs";
|
|
128059
|
+
import { join as join20 } from "path";
|
|
128060
|
+
var MIRRORED_DEFINITION_FILES = ["SKILL.md", "VISION.md", "README.md"];
|
|
128061
|
+
function materialize(task, fireId, stateDocs = [], runRoot) {
|
|
128062
|
+
const dir = runFireDir(task.slug, fireId, runRoot);
|
|
128063
|
+
mkdirSync10(dir, { recursive: true, mode: 448 });
|
|
128064
|
+
for (const doc of stateDocs) {
|
|
128065
|
+
writeFileSync11(join20(dir, doc.filename), doc.body, { mode: 384 });
|
|
127820
128066
|
}
|
|
127821
|
-
|
|
127822
|
-
|
|
128067
|
+
const files = {
|
|
128068
|
+
"SKILL.md": task.markdownBody ?? "",
|
|
128069
|
+
"VISION.md": task.visionMd ?? "",
|
|
128070
|
+
"CONSTRAINTS.md": task.constraintsMd ?? "",
|
|
128071
|
+
"README.md": task.readmeMd ?? ""
|
|
128072
|
+
};
|
|
128073
|
+
if (typeof task.dashboardHtml === "string" && task.dashboardHtml) {
|
|
128074
|
+
files["dashboard.html"] = task.dashboardHtml;
|
|
127823
128075
|
}
|
|
127824
|
-
|
|
127825
|
-
|
|
127826
|
-
|
|
127827
|
-
|
|
127828
|
-
|
|
127829
|
-
|
|
127830
|
-
|
|
128076
|
+
if (typeof task.dashboardManifest === "string" && task.dashboardManifest) {
|
|
128077
|
+
files["dashboard.manifest.json"] = task.dashboardManifest;
|
|
128078
|
+
}
|
|
128079
|
+
for (const [name, body] of Object.entries(files)) {
|
|
128080
|
+
writeFileSync11(join20(dir, name), body, { mode: 384 });
|
|
128081
|
+
}
|
|
128082
|
+
writeFileSync11(
|
|
128083
|
+
join20(dir, "STATUS.md"),
|
|
128084
|
+
`# STATUS \u2014 ${task.slug}
|
|
128085
|
+
|
|
128086
|
+
loop_id: ${task.id}
|
|
128087
|
+
fire_id: ${fireId}
|
|
128088
|
+
started: pending
|
|
128089
|
+
queue: not started
|
|
128090
|
+
`,
|
|
128091
|
+
{ mode: 384 }
|
|
127831
128092
|
);
|
|
127832
|
-
|
|
127833
|
-
|
|
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
|
-
};
|
|
128093
|
+
mirrorDefinitionFiles(task.slug, files, runRoot);
|
|
128094
|
+
return dir;
|
|
127848
128095
|
}
|
|
127849
|
-
function
|
|
127850
|
-
|
|
127851
|
-
|
|
127852
|
-
|
|
127853
|
-
|
|
127854
|
-
|
|
128096
|
+
function mirrorDefinitionFiles(slug, files, runRoot) {
|
|
128097
|
+
try {
|
|
128098
|
+
const root2 = runDir(slug, runRoot);
|
|
128099
|
+
mkdirSync10(root2, { recursive: true, mode: 448 });
|
|
128100
|
+
for (const name of [...MIRRORED_DEFINITION_FILES, "dashboard.html", "dashboard.manifest.json"]) {
|
|
128101
|
+
const body = files[name];
|
|
128102
|
+
if (body === void 0) continue;
|
|
128103
|
+
writeFileSync11(join20(root2, name), body, { mode: 384 });
|
|
128104
|
+
}
|
|
128105
|
+
} catch {
|
|
128106
|
+
}
|
|
127855
128107
|
}
|
|
127856
128108
|
|
|
127857
|
-
// src/
|
|
128109
|
+
// src/tasks/heartbeat.ts
|
|
127858
128110
|
init_esm_shims();
|
|
127859
128111
|
import * as realFs2 from "fs";
|
|
127860
128112
|
import { join as join21 } from "path";
|
|
127861
|
-
function startHeartbeat(
|
|
128113
|
+
function startHeartbeat(runDir2, info, deps = {}) {
|
|
127862
128114
|
const fs = deps.fs ?? realFs2;
|
|
127863
128115
|
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
127864
128116
|
const setI = deps.setInterval ?? globalThis.setInterval;
|
|
127865
128117
|
const clearI = deps.clearInterval ?? globalThis.clearInterval;
|
|
127866
128118
|
const intervalMs = deps.intervalMs ?? 6e4;
|
|
127867
|
-
const stateDir = join21(
|
|
128119
|
+
const stateDir = join21(runDir2, ".state");
|
|
127868
128120
|
const path2 = join21(stateDir, "fire.running");
|
|
127869
128121
|
try {
|
|
127870
128122
|
fs.mkdirSync(stateDir, { recursive: true, mode: 448 });
|
|
@@ -127901,7 +128153,7 @@ ${info.sessionId ? `session=${info.sessionId}
|
|
|
127901
128153
|
};
|
|
127902
128154
|
}
|
|
127903
128155
|
|
|
127904
|
-
// src/
|
|
128156
|
+
// src/tasks/accounting.ts
|
|
127905
128157
|
init_esm_shims();
|
|
127906
128158
|
import * as realFs3 from "fs";
|
|
127907
128159
|
import { homedir as homedir13 } from "os";
|
|
@@ -127965,21 +128217,21 @@ function recordFireAccounting(args) {
|
|
|
127965
128217
|
const stats = parseTranscriptStats(fs.readFileSync(transcript, "utf-8"));
|
|
127966
128218
|
const rec = {
|
|
127967
128219
|
ts: new Date(args.startedAtEpochMs).toISOString(),
|
|
127968
|
-
loop: args.
|
|
128220
|
+
loop: args.taskSlug,
|
|
127969
128221
|
session: args.sessionId,
|
|
127970
128222
|
exit: args.exitCode,
|
|
127971
128223
|
duration_s: Math.max(0, Math.round((now().getTime() - args.startedAtEpochMs) / 1e3)),
|
|
127972
128224
|
...stats
|
|
127973
128225
|
};
|
|
127974
|
-
appendFireLine(fs, args.
|
|
128226
|
+
appendFireLine(fs, args.runDir, rec);
|
|
127975
128227
|
return rec;
|
|
127976
128228
|
} catch (err) {
|
|
127977
128229
|
log(` (accounting failed \u2014 fire itself unaffected: ${err instanceof Error ? err.message : String(err)})`);
|
|
127978
128230
|
return null;
|
|
127979
128231
|
}
|
|
127980
128232
|
}
|
|
127981
|
-
function appendFireLine(fs,
|
|
127982
|
-
const stateDir = join22(
|
|
128233
|
+
function appendFireLine(fs, runDir2, rec) {
|
|
128234
|
+
const stateDir = join22(runDir2, ".state");
|
|
127983
128235
|
fs.mkdirSync(stateDir, { recursive: true, mode: 448 });
|
|
127984
128236
|
fs.appendFileSync(join22(stateDir, "fires.jsonl"), JSON.stringify(rec) + "\n", { mode: 384 });
|
|
127985
128237
|
}
|
|
@@ -127990,7 +128242,7 @@ function recordFailedLaunch(args) {
|
|
|
127990
128242
|
try {
|
|
127991
128243
|
const rec = {
|
|
127992
128244
|
ts: new Date(args.startedAtEpochMs).toISOString(),
|
|
127993
|
-
loop: args.
|
|
128245
|
+
loop: args.taskSlug,
|
|
127994
128246
|
session: args.sessionId,
|
|
127995
128247
|
exit: args.exitCode ?? 1,
|
|
127996
128248
|
duration_s: Math.max(0, Math.round((now().getTime() - args.startedAtEpochMs) / 1e3)),
|
|
@@ -127998,7 +128250,7 @@ function recordFailedLaunch(args) {
|
|
|
127998
128250
|
launch_failed: true,
|
|
127999
128251
|
reason: args.reason
|
|
128000
128252
|
};
|
|
128001
|
-
appendFireLine(fs, args.
|
|
128253
|
+
appendFireLine(fs, args.runDir, rec);
|
|
128002
128254
|
return rec;
|
|
128003
128255
|
} catch (err) {
|
|
128004
128256
|
log(` (failed-launch record could not be written: ${err instanceof Error ? err.message : String(err)})`);
|
|
@@ -128006,14 +128258,14 @@ function recordFailedLaunch(args) {
|
|
|
128006
128258
|
}
|
|
128007
128259
|
}
|
|
128008
128260
|
|
|
128009
|
-
// src/
|
|
128261
|
+
// src/tasks/run.ts
|
|
128010
128262
|
init_esm_shims();
|
|
128011
128263
|
import { spawn as spawn2 } from "child_process";
|
|
128012
128264
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
128013
128265
|
import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync15, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "fs";
|
|
128014
128266
|
import { dirname as dirname9, join as join24 } from "path";
|
|
128015
128267
|
|
|
128016
|
-
// src/
|
|
128268
|
+
// src/tasks/claude-binary.ts
|
|
128017
128269
|
init_esm_shims();
|
|
128018
128270
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
128019
128271
|
var defaultRun = (cmd, args) => {
|
|
@@ -128060,7 +128312,7 @@ function ensureClaudeBinary(deps = {}) {
|
|
|
128060
128312
|
return { status: "healed" };
|
|
128061
128313
|
}
|
|
128062
128314
|
|
|
128063
|
-
// src/
|
|
128315
|
+
// src/tasks/concurrency.ts
|
|
128064
128316
|
init_esm_shims();
|
|
128065
128317
|
import * as realFs4 from "fs";
|
|
128066
128318
|
import { join as join23 } from "path";
|
|
@@ -128080,21 +128332,21 @@ function parseHeartbeatPid(body) {
|
|
|
128080
128332
|
const pid = Number(m[1]);
|
|
128081
128333
|
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
128082
128334
|
}
|
|
128083
|
-
function liveFires(
|
|
128335
|
+
function liveFires(taskRoot, deps = {}) {
|
|
128084
128336
|
const fs = deps.fs ?? realFs4;
|
|
128085
128337
|
const isAlive = deps.isAlive ?? pidIsAlive;
|
|
128086
128338
|
const now = deps.now ?? Date.now;
|
|
128087
128339
|
const staleMs = deps.staleMs ?? DEFAULT_HEARTBEAT_STALE_MS;
|
|
128088
128340
|
let entries;
|
|
128089
128341
|
try {
|
|
128090
|
-
entries = fs.readdirSync(join23(
|
|
128342
|
+
entries = fs.readdirSync(join23(taskRoot, "fires"));
|
|
128091
128343
|
} catch {
|
|
128092
128344
|
return [];
|
|
128093
128345
|
}
|
|
128094
128346
|
const out = [];
|
|
128095
128347
|
for (const entry of entries) {
|
|
128096
128348
|
try {
|
|
128097
|
-
const beat = join23(
|
|
128349
|
+
const beat = join23(taskRoot, "fires", String(entry), ".state", "fire.running");
|
|
128098
128350
|
const st = fs.statSync(beat);
|
|
128099
128351
|
const heartbeatAgeMs = now() - Number(st.mtimeMs);
|
|
128100
128352
|
if (!(heartbeatAgeMs <= staleMs)) continue;
|
|
@@ -128109,16 +128361,20 @@ function liveFires(loopRoot, deps = {}) {
|
|
|
128109
128361
|
}
|
|
128110
128362
|
function resolveMaxConcurrentFires(opts = {}, env = process.env) {
|
|
128111
128363
|
const explicit = opts.maxConcurrentFires;
|
|
128112
|
-
const raw = env
|
|
128364
|
+
const raw = readTaskEnv("MAX_CONCURRENT_FIRES", env);
|
|
128113
128365
|
const fromEnv = raw !== void 0 && raw.trim() !== "" ? Number(raw) : void 0;
|
|
128114
128366
|
const picked = explicit !== void 0 && Number.isFinite(explicit) ? explicit : fromEnv !== void 0 && Number.isFinite(fromEnv) ? fromEnv : DEFAULT_MAX_CONCURRENT_FIRES;
|
|
128115
128367
|
if (picked <= 0) return Infinity;
|
|
128116
128368
|
return Math.floor(picked);
|
|
128117
128369
|
}
|
|
128118
128370
|
|
|
128119
|
-
// src/
|
|
128371
|
+
// src/tasks/run.ts
|
|
128120
128372
|
init_paths();
|
|
128121
128373
|
var SHIPBACK_SIGNAL_TIMEOUT_MS = 8e3;
|
|
128374
|
+
function isForbidden(res) {
|
|
128375
|
+
if (res.code === 403) return true;
|
|
128376
|
+
return /\bHTTP 403\b|forbidden|only the creator/i.test(res.error ?? "");
|
|
128377
|
+
}
|
|
128122
128378
|
function preserveRefusedConstraints(slug, fireId, body, runRoot) {
|
|
128123
128379
|
try {
|
|
128124
128380
|
const path2 = refusedConstraintsPath(slug, fireId, runRoot);
|
|
@@ -128141,72 +128397,74 @@ function killTree(pid) {
|
|
|
128141
128397
|
}
|
|
128142
128398
|
}, 2e3).unref?.();
|
|
128143
128399
|
}
|
|
128144
|
-
async function
|
|
128400
|
+
async function runTask(taskId, opts = {}) {
|
|
128145
128401
|
const { sdk, apiKey } = await getCliSdk();
|
|
128146
|
-
const got = await sdk.loops.get(apiKey,
|
|
128147
|
-
if (got.status !== "ok") throw new Error(`
|
|
128148
|
-
const
|
|
128402
|
+
const got = await sdk.loops.get(apiKey, taskId);
|
|
128403
|
+
if (got.status !== "ok") throw new Error(`task not found: ${got.error}`);
|
|
128404
|
+
const task = got.loop;
|
|
128149
128405
|
const runRoot = resolveRunRoot();
|
|
128150
128406
|
if (runRoot.reason === "fallback") {
|
|
128151
128407
|
const why = runRoot.rejected?.map((r) => `${r.dir}: ${r.why}`).join("; ") ?? "no usable cwd";
|
|
128152
|
-
console.log(`
|
|
128408
|
+
console.log(`Task ${task.slug}: running under ${runRoot.root} \u2014 the current folder cannot host a run (${why}).`);
|
|
128153
128409
|
}
|
|
128154
|
-
const
|
|
128410
|
+
const taskRoot = runDir(task.slug, runRoot.root);
|
|
128411
|
+
const legacyHint = legacyRunFolderHint(task.slug, runRoot.root);
|
|
128412
|
+
if (legacyHint) console.log(legacyHint);
|
|
128155
128413
|
const maxConcurrent = resolveMaxConcurrentFires(opts, process.env);
|
|
128156
|
-
const inFlight = liveFires(
|
|
128414
|
+
const inFlight = liveFires(taskRoot);
|
|
128157
128415
|
if (inFlight.length >= maxConcurrent) {
|
|
128158
128416
|
console.log(
|
|
128159
|
-
`
|
|
128417
|
+
`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
128418
|
);
|
|
128161
|
-
return { status: "skipped", dir:
|
|
128419
|
+
return { status: "skipped", dir: taskRoot, shipBack: { outcome: "nothing" } };
|
|
128162
128420
|
}
|
|
128163
|
-
const est = estimateBlastRadius(
|
|
128421
|
+
const est = estimateBlastRadius(task);
|
|
128164
128422
|
const sessionId2 = opts.sessionId ?? randomUUID5();
|
|
128165
|
-
const discovered = await discoverMemoryDocs(sdk, apiKey,
|
|
128423
|
+
const discovered = await discoverMemoryDocs(sdk, apiKey, task.slug);
|
|
128166
128424
|
for (const scope of discovered.failed) {
|
|
128167
128425
|
console.error(
|
|
128168
|
-
`
|
|
128426
|
+
`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
128427
|
);
|
|
128170
128428
|
}
|
|
128171
128429
|
for (const scope of discovered.truncated) {
|
|
128172
128430
|
console.error(
|
|
128173
|
-
`
|
|
128431
|
+
`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
128432
|
);
|
|
128175
128433
|
}
|
|
128176
|
-
const plan = planStateDocs(
|
|
128434
|
+
const plan = planStateDocs(task.stateDocs, discovered);
|
|
128177
128435
|
if (plan.skipped.length > 0) {
|
|
128178
128436
|
console.error(
|
|
128179
|
-
`
|
|
128437
|
+
`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
128438
|
);
|
|
128181
128439
|
}
|
|
128182
|
-
const { fetched: stateDocBodies, failed: stateDocFailures } = plan.docs.length > 0 ? await fetchStateDocs(sdk, apiKey,
|
|
128440
|
+
const { fetched: stateDocBodies, failed: stateDocFailures } = plan.docs.length > 0 ? await fetchStateDocs(sdk, apiKey, task.slug, plan.docs) : { fetched: [], failed: [] };
|
|
128183
128441
|
if (stateDocFailures.length > 0) {
|
|
128184
128442
|
console.error(
|
|
128185
|
-
`
|
|
128443
|
+
`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
128444
|
);
|
|
128187
128445
|
}
|
|
128188
128446
|
if (stateDocBodies.length > 0) {
|
|
128189
|
-
console.log(`
|
|
128447
|
+
console.log(`Task ${task.slug}: state docs \u2192 ${stateDocBodies.map(describeSource).join(", ")}`);
|
|
128190
128448
|
}
|
|
128191
128449
|
const undeclared = stateDocBodies.filter((d) => !d.declared);
|
|
128192
128450
|
if (undeclared.length > 0) {
|
|
128193
128451
|
console.log(
|
|
128194
|
-
`
|
|
128452
|
+
`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
128453
|
);
|
|
128196
128454
|
}
|
|
128197
|
-
const dir = materialize(
|
|
128198
|
-
console.log(`
|
|
128455
|
+
const dir = materialize(task, sessionId2, stateDocBodies, runRoot.root);
|
|
128456
|
+
console.log(`Task ${task.slug}: fire ${sessionId2} \u2192 ${dir}`);
|
|
128199
128457
|
const launch = buildLaunchArgs(
|
|
128200
128458
|
dir,
|
|
128201
128459
|
// The child runs IN the resolved root: for a project cwd that is the cwd it always was; under
|
|
128202
128460
|
// the fallback it is the writable folder rather than the `/` launchd handed us.
|
|
128203
|
-
{ ...opts, sessionId: sessionId2, cwd: opts.cwd ?? runRoot.root, memoryManifest: normalizeMemoryManifest(
|
|
128204
|
-
|
|
128461
|
+
{ ...opts, sessionId: sessionId2, cwd: opts.cwd ?? runRoot.root, memoryManifest: normalizeMemoryManifest(task.stateDocs) },
|
|
128462
|
+
task.slug
|
|
128205
128463
|
);
|
|
128206
128464
|
const budget = resolveMaxBudgetUsd(opts);
|
|
128207
128465
|
const capLabel = budget !== void 0 ? `capped at \u20AC${budget}` : "no external budget cap";
|
|
128208
128466
|
console.log(
|
|
128209
|
-
`
|
|
128467
|
+
`Task ${task.slug}: ${est.steps} steps (${est.paidSteps} paid), est \u20AC${est.estCostEur ?? "?"} \u2014 ${capLabel}`
|
|
128210
128468
|
);
|
|
128211
128469
|
const statusPath = join24(dir, "STATUS.md");
|
|
128212
128470
|
const statusBefore = existsSync16(statusPath) ? readFileSync15(statusPath, "utf-8") : "";
|
|
@@ -128220,10 +128478,14 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
|
|
|
128220
128478
|
{ mode: 384 }
|
|
128221
128479
|
);
|
|
128222
128480
|
const heartbeat = startHeartbeat(dir, { pid: process.pid, sessionId: sessionId2 });
|
|
128223
|
-
const dashboard = await startDashboardServer({
|
|
128481
|
+
const dashboard = await startDashboardServer({
|
|
128482
|
+
task,
|
|
128483
|
+
runDir: dir,
|
|
128484
|
+
readDoc: memoryDocResolver(sdk, apiKey, task.slug, normalizeMemoryManifest(task.stateDocs))
|
|
128485
|
+
});
|
|
128224
128486
|
if (dashboard) {
|
|
128225
128487
|
openDashboardInBrowser(`http://localhost:${dashboard.port}`, { isTTY: Boolean(process.stdout.isTTY) });
|
|
128226
|
-
} else if (typeof
|
|
128488
|
+
} else if (typeof task.dashboardHtml !== "string" || !task.dashboardHtml) {
|
|
128227
128489
|
console.log(NO_DASHBOARD_MESSAGE);
|
|
128228
128490
|
}
|
|
128229
128491
|
const startedAtEpochMs = Date.now();
|
|
@@ -128231,7 +128493,7 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
|
|
|
128231
128493
|
ensureClaudeBinary();
|
|
128232
128494
|
} catch (err) {
|
|
128233
128495
|
const reason = err instanceof Error ? err.message : String(err);
|
|
128234
|
-
recordFailedLaunch({
|
|
128496
|
+
recordFailedLaunch({ runDir: dir, taskSlug: task.slug, sessionId: sessionId2, reason, startedAtEpochMs });
|
|
128235
128497
|
heartbeat.stop();
|
|
128236
128498
|
dashboard?.close();
|
|
128237
128499
|
throw err;
|
|
@@ -128252,55 +128514,78 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
|
|
|
128252
128514
|
async function shipDocs() {
|
|
128253
128515
|
if (stateDocBodies.length === 0) return;
|
|
128254
128516
|
try {
|
|
128255
|
-
const outcomes = await shipBackStateDocs(sdk, apiKey,
|
|
128517
|
+
const outcomes = await shipBackStateDocs(sdk, apiKey, task.slug, dir, stateDocBodies);
|
|
128256
128518
|
for (const o of outcomes) {
|
|
128257
128519
|
if (o.outcome === "shipped") {
|
|
128258
|
-
console.log(`
|
|
128520
|
+
console.log(`Task ${task.slug}: shipped back state doc '${o.key}'.`);
|
|
128259
128521
|
} else if (o.outcome === "refused" || o.outcome === "failed") {
|
|
128260
|
-
console.error(`
|
|
128522
|
+
console.error(`Task ${task.slug}: state doc '${o.key}' ${o.outcome} \u2014 ${o.detail}`);
|
|
128261
128523
|
}
|
|
128262
128524
|
}
|
|
128263
128525
|
} catch (err) {
|
|
128264
128526
|
console.error(
|
|
128265
|
-
`
|
|
128527
|
+
`Task ${task.slug}: state-doc ship-back failed: ${err instanceof Error ? err.message : String(err)}`
|
|
128266
128528
|
);
|
|
128267
128529
|
}
|
|
128268
128530
|
}
|
|
128269
128531
|
async function shipConstraints() {
|
|
128270
128532
|
try {
|
|
128533
|
+
const ownership = await resolveTaskOwnership(sdk, apiKey, task);
|
|
128534
|
+
if (ownership.resolved && !ownership.isOwner) {
|
|
128535
|
+
constraintsReport = { outcome: "skipped-not-owner", owner: ownership.createdBy };
|
|
128536
|
+
console.log(
|
|
128537
|
+
`Task ${task.slug}: ship-back skipped: this task is owned by ${ownership.createdBy}; your learnings stay in the run diary (and the member-scoped "learnings" document when declared)`
|
|
128538
|
+
);
|
|
128539
|
+
return;
|
|
128540
|
+
}
|
|
128271
128541
|
const constraintsPath = join24(dir, "CONSTRAINTS.md");
|
|
128272
128542
|
const materializedConstraints = existsSync16(constraintsPath) ? readFileSync15(constraintsPath, "utf-8") : void 0;
|
|
128273
128543
|
if (materializedConstraints === void 0) return;
|
|
128274
|
-
const boot =
|
|
128544
|
+
const boot = task.constraintsMd ?? "";
|
|
128275
128545
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
128276
|
-
const reread = await sdk.loops.get(apiKey,
|
|
128546
|
+
const reread = await sdk.loops.get(apiKey, taskId);
|
|
128277
128547
|
if (reread.status !== "ok") {
|
|
128278
128548
|
constraintsReport = { outcome: "failed" };
|
|
128279
128549
|
console.error(
|
|
128280
|
-
`
|
|
128550
|
+
`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
128551
|
);
|
|
128282
128552
|
return;
|
|
128283
128553
|
}
|
|
128284
128554
|
const fresh = reread.loop.constraintsMd ?? "";
|
|
128285
128555
|
const merged = mergeConstraints(boot, materializedConstraints, fresh);
|
|
128286
128556
|
if (merged.refusal) {
|
|
128287
|
-
const kept = preserveRefusedConstraints(
|
|
128557
|
+
const kept = preserveRefusedConstraints(task.slug, sessionId2, materializedConstraints, runRoot.root);
|
|
128288
128558
|
constraintsReport = { outcome: "refused", keptAt: kept.path ?? constraintsPath };
|
|
128289
128559
|
console.error(
|
|
128290
|
-
`
|
|
128560
|
+
`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
128561
|
);
|
|
128292
128562
|
return;
|
|
128293
128563
|
}
|
|
128294
128564
|
if (merged.next === void 0) return;
|
|
128295
128565
|
if (!merged.cleanAppend && attempt === 1) {
|
|
128296
128566
|
console.warn(
|
|
128297
|
-
`
|
|
128567
|
+
`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).`
|
|
128568
|
+
);
|
|
128569
|
+
}
|
|
128570
|
+
const put = await sdk.loops.update(apiKey, taskId, { constraintsMd: merged.next });
|
|
128571
|
+
if (put?.status !== "ok") {
|
|
128572
|
+
if (isForbidden(put ?? { status: "nok" })) {
|
|
128573
|
+
const owner = typeof task.createdBy === "string" ? task.createdBy : "someone else";
|
|
128574
|
+
constraintsReport = { outcome: "skipped-not-owner", owner };
|
|
128575
|
+
console.log(
|
|
128576
|
+
`Task ${task.slug}: ship-back skipped: this task is owned by ${owner}; your learnings stay in the run diary`
|
|
128577
|
+
);
|
|
128578
|
+
return;
|
|
128579
|
+
}
|
|
128580
|
+
constraintsReport = { outcome: "failed" };
|
|
128581
|
+
console.error(
|
|
128582
|
+
`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
128583
|
);
|
|
128584
|
+
return;
|
|
128299
128585
|
}
|
|
128300
|
-
await sdk.loops.
|
|
128301
|
-
const after = await sdk.loops.get(apiKey, loopId);
|
|
128586
|
+
const after = await sdk.loops.get(apiKey, taskId);
|
|
128302
128587
|
if (after.status !== "ok") {
|
|
128303
|
-
console.warn(`
|
|
128588
|
+
console.warn(`Task ${task.slug}: shipped back, but could not verify it landed.`);
|
|
128304
128589
|
return;
|
|
128305
128590
|
}
|
|
128306
128591
|
const afterText = after.loop.constraintsMd ?? "";
|
|
@@ -128309,28 +128594,28 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
|
|
|
128309
128594
|
constraintsReport = { outcome: "shipped" };
|
|
128310
128595
|
if (merged.added.length > 0) {
|
|
128311
128596
|
console.log(
|
|
128312
|
-
`
|
|
128597
|
+
`Task ${task.slug}: shipped back ${merged.added.length} new constraints section(s).`
|
|
128313
128598
|
);
|
|
128314
128599
|
} else {
|
|
128315
|
-
console.log(`
|
|
128600
|
+
console.log(`Task ${task.slug}: shipped back a constraints rewrite (no new sections).`);
|
|
128316
128601
|
}
|
|
128317
128602
|
return;
|
|
128318
128603
|
}
|
|
128319
128604
|
if (attempt === 2) {
|
|
128320
128605
|
constraintsReport = { outcome: "failed" };
|
|
128321
128606
|
console.error(
|
|
128322
|
-
`
|
|
128607
|
+
`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
128608
|
);
|
|
128324
128609
|
return;
|
|
128325
128610
|
}
|
|
128326
128611
|
console.warn(
|
|
128327
|
-
`
|
|
128612
|
+
`Task ${task.slug}: a concurrent write clobbered ${missing.length} of our sections; retrying the merge.`
|
|
128328
128613
|
);
|
|
128329
128614
|
}
|
|
128330
128615
|
} catch (err) {
|
|
128331
128616
|
constraintsReport = { outcome: "failed" };
|
|
128332
128617
|
console.error(
|
|
128333
|
-
`
|
|
128618
|
+
`Task ${task.slug}: ship-back failed: ${err instanceof Error ? err.message : String(err)}`
|
|
128334
128619
|
);
|
|
128335
128620
|
}
|
|
128336
128621
|
}
|
|
@@ -128344,7 +128629,7 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
|
|
|
128344
128629
|
void Promise.race([shipBackConstraints().then(() => "shipped"), deadline]).then((outcome) => {
|
|
128345
128630
|
if (outcome === "timeout") {
|
|
128346
128631
|
console.error(
|
|
128347
|
-
`
|
|
128632
|
+
`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
128633
|
);
|
|
128349
128634
|
}
|
|
128350
128635
|
}).finally(() => process.exit(130));
|
|
@@ -128363,8 +128648,8 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
|
|
|
128363
128648
|
heartbeat.stop();
|
|
128364
128649
|
dashboard?.close();
|
|
128365
128650
|
const accounted = recordFireAccounting({
|
|
128366
|
-
|
|
128367
|
-
|
|
128651
|
+
runDir: dir,
|
|
128652
|
+
taskSlug: task.slug,
|
|
128368
128653
|
sessionId: sessionId2,
|
|
128369
128654
|
cwd: launch.cwd,
|
|
128370
128655
|
exitCode,
|
|
@@ -128372,8 +128657,8 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
|
|
|
128372
128657
|
});
|
|
128373
128658
|
if (!accounted) {
|
|
128374
128659
|
recordFailedLaunch({
|
|
128375
|
-
|
|
128376
|
-
|
|
128660
|
+
runDir: dir,
|
|
128661
|
+
taskSlug: task.slug,
|
|
128377
128662
|
sessionId: sessionId2,
|
|
128378
128663
|
reason: spawnError ? `spawn failed: ${spawnError}` : `the fire produced no transcript (exit ${exitCode}) \u2014 it never started`,
|
|
128379
128664
|
exitCode,
|
|
@@ -128388,7 +128673,7 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
|
|
|
128388
128673
|
if (clean2) {
|
|
128389
128674
|
if (constraintsReport.outcome === "refused") {
|
|
128390
128675
|
console.log(
|
|
128391
|
-
`
|
|
128676
|
+
`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
128677
|
);
|
|
128393
128678
|
return { status: "clean", dir, shipBack: constraintsReport };
|
|
128394
128679
|
}
|
|
@@ -128398,24 +128683,24 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
|
|
|
128398
128683
|
return { status: "dirty", dir, shipBack: constraintsReport };
|
|
128399
128684
|
}
|
|
128400
128685
|
|
|
128401
|
-
// src/
|
|
128686
|
+
// src/tasks/show.ts
|
|
128402
128687
|
init_esm_shims();
|
|
128403
|
-
function
|
|
128688
|
+
function formatTaskFiles(task) {
|
|
128404
128689
|
const section = (title, body) => `
|
|
128405
128690
|
===== ${title} =====
|
|
128406
128691
|
${(body ?? "").trim() || "(empty)"}
|
|
128407
128692
|
`;
|
|
128408
|
-
return `
|
|
128409
|
-
` + section("SKILL.md",
|
|
128693
|
+
return `Task: ${task.slug} (${task.id})
|
|
128694
|
+
` + 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
128695
|
}
|
|
128411
|
-
async function
|
|
128696
|
+
async function showTask(taskId) {
|
|
128412
128697
|
const { sdk, apiKey } = await getCliSdk();
|
|
128413
|
-
const got = await sdk.loops.get(apiKey,
|
|
128414
|
-
if (got.status !== "ok") throw new Error(`
|
|
128415
|
-
console.log(
|
|
128698
|
+
const got = await sdk.loops.get(apiKey, taskId);
|
|
128699
|
+
if (got.status !== "ok") throw new Error(`task not found: ${got.error}`);
|
|
128700
|
+
console.log(formatTaskFiles(got.loop));
|
|
128416
128701
|
}
|
|
128417
128702
|
|
|
128418
|
-
// src/
|
|
128703
|
+
// src/tasks/schedule.ts
|
|
128419
128704
|
init_esm_shims();
|
|
128420
128705
|
init_paths();
|
|
128421
128706
|
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 +128716,7 @@ var LOOP_KIND = {
|
|
|
128431
128716
|
return args;
|
|
128432
128717
|
},
|
|
128433
128718
|
stateDir(slug, cwd) {
|
|
128434
|
-
return join25(cwd, `.ametyst${ENV_SUFFIX}`,
|
|
128719
|
+
return join25(cwd, `.ametyst${ENV_SUFFIX}`, LEGACY_RUNS_SEGMENT, safeSlug2(slug, this.noun), ".state");
|
|
128435
128720
|
}
|
|
128436
128721
|
};
|
|
128437
128722
|
var TASK_KIND = {
|
|
@@ -128440,10 +128725,11 @@ var TASK_KIND = {
|
|
|
128440
128725
|
buildRunCmd(slug, opts) {
|
|
128441
128726
|
const args = [process.execPath, process.argv[1], "task", "run", slug];
|
|
128442
128727
|
if (opts.maxBudgetUsd != null) args.push("--max-budget-usd", String(opts.maxBudgetUsd));
|
|
128728
|
+
if (typeof opts.input === "string" && opts.input.trim() !== "") args.push("--input", opts.input);
|
|
128443
128729
|
return args;
|
|
128444
128730
|
},
|
|
128445
128731
|
stateDir(slug, cwd) {
|
|
128446
|
-
return join25(cwd, `.ametyst${ENV_SUFFIX}`,
|
|
128732
|
+
return join25(cwd, `.ametyst${ENV_SUFFIX}`, RUNS_SEGMENT, safeSlug2(slug, this.noun), ".state");
|
|
128447
128733
|
}
|
|
128448
128734
|
};
|
|
128449
128735
|
var COMPOUND_KIND = {
|
|
@@ -128564,11 +128850,11 @@ function cadenceLabel(opts) {
|
|
|
128564
128850
|
function escapeXml(s) {
|
|
128565
128851
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
128566
128852
|
}
|
|
128567
|
-
function
|
|
128853
|
+
function shellQuote3(s) {
|
|
128568
128854
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
128569
128855
|
}
|
|
128570
128856
|
function shellArg(s) {
|
|
128571
|
-
return /^[A-Za-z0-9_@+=:,./-]+$/.test(s) ? s :
|
|
128857
|
+
return /^[A-Za-z0-9_@+=:,./-]+$/.test(s) ? s : shellQuote3(s);
|
|
128572
128858
|
}
|
|
128573
128859
|
function cronEscapePercent(s) {
|
|
128574
128860
|
return s.replace(/%/g, "\\%");
|
|
@@ -128653,6 +128939,10 @@ function schedule(kind, slug, opts = {}) {
|
|
|
128653
128939
|
const why = (resolved.rejected ?? []).map((r) => `${r.dir}: ${r.why}`).join("; ");
|
|
128654
128940
|
console.warn(`\u26A0\uFE0F the job will run in ${cwd} \u2014 the current folder cannot host a run (${why}).`);
|
|
128655
128941
|
}
|
|
128942
|
+
if (kind === TASK_KIND) {
|
|
128943
|
+
const legacyHint = legacyRunFolderHint(slug, cwd);
|
|
128944
|
+
if (legacyHint) console.warn(legacyHint);
|
|
128945
|
+
}
|
|
128656
128946
|
const procEnv = opts.env ?? process.env;
|
|
128657
128947
|
const envPath = procEnv.PATH ?? "";
|
|
128658
128948
|
const model = resolveScheduledModel(opts, home, procEnv);
|
|
@@ -128704,9 +128994,9 @@ launchctl said: ${loaded.output.trim()}` : "")
|
|
|
128704
128994
|
if (model) cronEnv.ANTHROPIC_MODEL = model;
|
|
128705
128995
|
Object.assign(cronEnv, extraEnv);
|
|
128706
128996
|
const effectiveCronModel = cronEnv.ANTHROPIC_MODEL;
|
|
128707
|
-
const envPrefix = Object.entries(cronEnv).map(([k, v]) => `${k}=${
|
|
128997
|
+
const envPrefix = Object.entries(cronEnv).map(([k, v]) => `${k}=${shellQuote3(v)}`).join(" ");
|
|
128708
128998
|
const cmd = launcherArgv(args, stateDir).map(shellArg).join(" ");
|
|
128709
|
-
const command = cronEscapePercent(`cd ${
|
|
128999
|
+
const command = cronEscapePercent(`cd ${shellQuote3(cwd)} && ${envPrefix} ${cmd}`);
|
|
128710
129000
|
const line = `${cronField(opts)} ${command} # ${lbl}`;
|
|
128711
129001
|
const kept = stripLabel(readCrontab(), lbl);
|
|
128712
129002
|
kept.push(line);
|
|
@@ -128750,6 +129040,60 @@ function plistEnvKeys(xml) {
|
|
|
128750
129040
|
if (close < 0) return [];
|
|
128751
129041
|
return [...xml.slice(open, close).matchAll(/<key>([^<]*)<\/key>/g)].map((m) => unescapeXml(m[1]));
|
|
128752
129042
|
}
|
|
129043
|
+
function plistProgramArguments(xml) {
|
|
129044
|
+
const anchor = xml.indexOf("<key>ProgramArguments</key>");
|
|
129045
|
+
if (anchor < 0) return [];
|
|
129046
|
+
const open = xml.indexOf("<array>", anchor);
|
|
129047
|
+
if (open < 0) return [];
|
|
129048
|
+
const close = xml.indexOf("</array>", open);
|
|
129049
|
+
if (close < 0) return [];
|
|
129050
|
+
return [...xml.slice(open, close).matchAll(/<string>([^<]*)<\/string>/g)].map((m) => unescapeXml(m[1]));
|
|
129051
|
+
}
|
|
129052
|
+
function cronCommandArgv(line) {
|
|
129053
|
+
const words = shellWords(line);
|
|
129054
|
+
const cd = words.indexOf("cd");
|
|
129055
|
+
const amp = words.indexOf("&&", cd >= 0 ? cd + 2 : 0);
|
|
129056
|
+
if (amp < 0) return [];
|
|
129057
|
+
let i = amp + 1;
|
|
129058
|
+
while (i < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[i])) i++;
|
|
129059
|
+
const argv = [];
|
|
129060
|
+
for (; i < words.length; i++) {
|
|
129061
|
+
if (words[i] === "#") break;
|
|
129062
|
+
argv.push(words[i].replace(/\\%/g, "%"));
|
|
129063
|
+
}
|
|
129064
|
+
return argv;
|
|
129065
|
+
}
|
|
129066
|
+
function parseScheduledArgs(argv) {
|
|
129067
|
+
const out = {};
|
|
129068
|
+
const valueOf = (i, flag) => {
|
|
129069
|
+
const w = argv[i];
|
|
129070
|
+
if (w === flag) return argv[i + 1];
|
|
129071
|
+
if (w.startsWith(`${flag}=`)) return w.slice(flag.length + 1);
|
|
129072
|
+
return void 0;
|
|
129073
|
+
};
|
|
129074
|
+
for (let i = 0; i < argv.length; i++) {
|
|
129075
|
+
const input = valueOf(i, "--input");
|
|
129076
|
+
if (input !== void 0) out.input = input;
|
|
129077
|
+
const budget = valueOf(i, "--max-budget-usd");
|
|
129078
|
+
if (budget !== void 0) {
|
|
129079
|
+
const n = Number(budget);
|
|
129080
|
+
if (Number.isFinite(n)) out.maxBudgetUsd = n;
|
|
129081
|
+
}
|
|
129082
|
+
}
|
|
129083
|
+
return out;
|
|
129084
|
+
}
|
|
129085
|
+
var SCHEDULE_INPUT_DISPLAY_MAX = 60;
|
|
129086
|
+
function formatScheduleEntryLine(e) {
|
|
129087
|
+
const parts2 = [e.namespace, e.slug];
|
|
129088
|
+
if (typeof e.args.input === "string") {
|
|
129089
|
+
const flat = e.args.input.replace(/\s+/g, " ").trim();
|
|
129090
|
+
const shown = flat.length > SCHEDULE_INPUT_DISPLAY_MAX ? `${flat.slice(0, SCHEDULE_INPUT_DISPLAY_MAX)}\u2026` : flat;
|
|
129091
|
+
parts2.push(`input: "${shown}"`);
|
|
129092
|
+
}
|
|
129093
|
+
if (typeof e.args.maxBudgetUsd === "number") parts2.push(`budget: $${e.args.maxBudgetUsd}`);
|
|
129094
|
+
parts2.push(`env: ${e.envKeys.length ? e.envKeys.join(", ") : "\u2014"}`);
|
|
129095
|
+
return parts2.join(" ");
|
|
129096
|
+
}
|
|
128753
129097
|
function shellWords(s) {
|
|
128754
129098
|
const out = [];
|
|
128755
129099
|
let i = 0;
|
|
@@ -128798,17 +129142,22 @@ function listEntries(kind, opts = {}) {
|
|
|
128798
129142
|
return readdirSync6(dir).filter((f) => f.startsWith(prefix) && f.endsWith(".plist")).map((f) => {
|
|
128799
129143
|
const slug = f.slice(prefix.length, -".plist".length);
|
|
128800
129144
|
let envKeys = [];
|
|
129145
|
+
let args = {};
|
|
128801
129146
|
try {
|
|
128802
|
-
|
|
129147
|
+
const xml = String(readFileSync16(join25(dir, f), "utf-8"));
|
|
129148
|
+
envKeys = plistEnvKeys(xml);
|
|
129149
|
+
args = parseScheduledArgs(plistProgramArguments(xml));
|
|
128803
129150
|
} catch {
|
|
128804
129151
|
envKeys = [];
|
|
129152
|
+
args = {};
|
|
128805
129153
|
}
|
|
128806
|
-
return { slug, envKeys: [...envKeys].sort() };
|
|
129154
|
+
return { slug, envKeys: [...envKeys].sort(), args };
|
|
128807
129155
|
});
|
|
128808
129156
|
}
|
|
128809
129157
|
return readCrontab().split("\n").filter((l) => l.includes(`# ${prefix}`)).map((l) => ({
|
|
128810
129158
|
slug: l.trimEnd().slice(l.trimEnd().lastIndexOf(prefix) + prefix.length),
|
|
128811
|
-
envKeys: cronEnvKeys(l).sort()
|
|
129159
|
+
envKeys: cronEnvKeys(l).sort(),
|
|
129160
|
+
args: parseScheduledArgs(cronCommandArgv(l))
|
|
128812
129161
|
}));
|
|
128813
129162
|
}
|
|
128814
129163
|
function scheduleTask(slug, opts = {}) {
|
|
@@ -128845,114 +129194,14 @@ function unscheduleEverywhere(slug, opts = {}) {
|
|
|
128845
129194
|
}
|
|
128846
129195
|
return removed;
|
|
128847
129196
|
}
|
|
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
129197
|
|
|
128949
|
-
// src/
|
|
129198
|
+
// src/tasks/push.ts
|
|
128950
129199
|
init_esm_shims();
|
|
128951
|
-
async function
|
|
129200
|
+
async function pushTaskFromFile(filePath, opts) {
|
|
128952
129201
|
const slug = (opts.slug ?? "").trim();
|
|
128953
129202
|
const descriptionShort = (opts.descriptionShort ?? "").trim();
|
|
128954
|
-
if (!slug) throw new Error("A --slug is required to push a
|
|
128955
|
-
if (!descriptionShort) throw new Error("A --description is required to push a
|
|
129203
|
+
if (!slug) throw new Error("A --slug is required to push a task.");
|
|
129204
|
+
if (!descriptionShort) throw new Error("A --description is required to push a task.");
|
|
128956
129205
|
const markdownBody = readMarkdownFile(filePath);
|
|
128957
129206
|
let graphJson = {};
|
|
128958
129207
|
if (opts.graphJson && opts.graphJson.trim()) {
|
|
@@ -128967,23 +129216,20 @@ async function pushCompoundFromFile(filePath, opts) {
|
|
|
128967
129216
|
if (opts.category && opts.category.trim()) body.category = opts.category.trim();
|
|
128968
129217
|
const { sdk, apiKey } = await getCliSdk();
|
|
128969
129218
|
const id = opts.id && opts.id.trim() ? opts.id.trim() : void 0;
|
|
128970
|
-
const res = id ? await sdk.
|
|
129219
|
+
const res = id ? await sdk.tasks.update(apiKey, id, body) : await sdk.tasks.create(apiKey, body);
|
|
128971
129220
|
if (res.status !== "ok") {
|
|
128972
129221
|
throw new Error(`push failed: ${res.error ?? "unknown error"}${res.code ? ` (${res.code})` : ""}`);
|
|
128973
129222
|
}
|
|
128974
|
-
return { mode: id ? "modified" : "created",
|
|
129223
|
+
return { mode: id ? "modified" : "created", task: res.task };
|
|
128975
129224
|
}
|
|
128976
129225
|
|
|
128977
129226
|
// src/commands/task-verbs.ts
|
|
128978
|
-
|
|
128979
|
-
console.warn(
|
|
128980
|
-
`\u26A0\uFE0F \`ametyst ${oldInvocation}\` is DEPRECATED and still works unchanged \u2014 use \`ametyst ${newInvocation}\`.`
|
|
128981
|
-
);
|
|
128982
|
-
}
|
|
129227
|
+
init_esm_shims();
|
|
128983
129228
|
async function runTaskVerb(id, opts, flavor) {
|
|
128984
|
-
const r = await
|
|
129229
|
+
const r = await runTask(id, {
|
|
128985
129230
|
maxBudgetUsd: opts.maxBudgetUsd,
|
|
128986
|
-
maxConcurrentFires: opts.maxConcurrentFires
|
|
129231
|
+
maxConcurrentFires: opts.maxConcurrentFires,
|
|
129232
|
+
input: opts.input
|
|
128987
129233
|
});
|
|
128988
129234
|
console.log(
|
|
128989
129235
|
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 +129245,17 @@ function cleanFinishLine(noun, shipBack) {
|
|
|
128999
129245
|
return `\u2705 ${noun} completed; the ship-back did NOT complete (see the error above), and this fire's folder is cleaned up.`;
|
|
129000
129246
|
case "shipped":
|
|
129001
129247
|
return `\u2705 ${noun} completed; improvements shipped back and this fire's folder cleaned up.`;
|
|
129248
|
+
case "skipped-not-owner":
|
|
129249
|
+
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
129250
|
}
|
|
129003
129251
|
}
|
|
129004
129252
|
async function showTaskVerb(id) {
|
|
129005
|
-
await
|
|
129253
|
+
await showTask(id);
|
|
129006
129254
|
}
|
|
129007
129255
|
async function pushTaskVerb(path2, opts, flavor) {
|
|
129008
129256
|
try {
|
|
129009
|
-
const r = await
|
|
129010
|
-
const c = r.
|
|
129257
|
+
const r = await pushTaskFromFile(path2, opts);
|
|
129258
|
+
const c = r.task;
|
|
129011
129259
|
console.log(`\u2705 ${flavor.noun} ${r.mode}: ${c?.slug ?? opts.slug}${c?.id ? ` (${c.id})` : ""}`);
|
|
129012
129260
|
} catch (err) {
|
|
129013
129261
|
console.error(`\u274C ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -129019,8 +129267,8 @@ function warnDoubleArm(noun, slug, found) {
|
|
|
129019
129267
|
const where = found.map((f) => `${f.namespace} (${f.label})`).join(", ");
|
|
129020
129268
|
console.warn(
|
|
129021
129269
|
`\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
|
|
129023
|
-
Nothing has been removed. To arm it ONCE, run \`ametyst task unschedule ${slug}\` (it removes the slug from EVERY namespace
|
|
129270
|
+
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.
|
|
129271
|
+
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
129272
|
);
|
|
129025
129273
|
}
|
|
129026
129274
|
function scheduleTaskVerb(slug, opts, flavor) {
|
|
@@ -129052,14 +129300,21 @@ function scheduleTaskVerb(slug, opts, flavor) {
|
|
|
129052
129300
|
// src/commands/task.ts
|
|
129053
129301
|
var TASK = { noun: "task" };
|
|
129054
129302
|
var taskCommand = new Command("task").description(
|
|
129055
|
-
"Manage and run Ametyst tasks \u2014
|
|
129303
|
+
"Manage and run Ametyst tasks \u2014 publish, run, show, schedule and sync them, and read their memory"
|
|
129056
129304
|
);
|
|
129057
129305
|
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:
|
|
129059
|
-
).option(
|
|
129306
|
+
"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)"
|
|
129307
|
+
).option(
|
|
129308
|
+
"--max-budget-usd <x>",
|
|
129309
|
+
"cap on the MODEL's token spend for this headless run (forwarded to claude -p); merchant payments are bounded by your on-chain policy, not by this flag",
|
|
129310
|
+
(v) => Number(v)
|
|
129311
|
+
).option(
|
|
129060
129312
|
"--max-concurrent-fires <n>",
|
|
129061
|
-
"ceiling on simultaneously-live fires of this task; 0 = unlimited (default:
|
|
129313
|
+
"ceiling on simultaneously-live fires of this task; 0 = unlimited (default: AMETYST_TASK_MAX_CONCURRENT_FIRES, else 6)",
|
|
129062
129314
|
(v) => Number(v)
|
|
129315
|
+
).option(
|
|
129316
|
+
"--input <text>",
|
|
129317
|
+
`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
129318
|
).action((id, opts) => runTaskVerb(id, opts, TASK));
|
|
129064
129319
|
taskCommand.command("show <id>").description(
|
|
129065
129320
|
"Print a task's stored files (SKILL/VISION/CONSTRAINTS/README) from Ametyst without running it"
|
|
@@ -129084,11 +129339,14 @@ taskCommand.command("push <path>").description("Publish a task whose body is rea
|
|
|
129084
129339
|
);
|
|
129085
129340
|
taskCommand.command("schedule <slug>").description("Schedule a headless task 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(
|
|
129086
129341
|
"--max-budget-usd <x>",
|
|
129087
|
-
"
|
|
129342
|
+
"cap on the MODEL's token spend for this headless run (forwarded to claude -p); merchant payments are bounded by your on-chain policy, not by this flag (opt-in; omitted by default)",
|
|
129088
129343
|
(v) => Number(v)
|
|
129089
129344
|
).option(
|
|
129090
129345
|
"--model <id>",
|
|
129091
129346
|
"model to pin the scheduled fires to (default: ANTHROPIC_MODEL, else your Claude Code default at schedule time)"
|
|
129347
|
+
).option(
|
|
129348
|
+
"--input <text>",
|
|
129349
|
+
"the user's arguments for EVERY scheduled fire, persisted on the job's command line (same as `task run --input`)"
|
|
129092
129350
|
).option(
|
|
129093
129351
|
"--env <KEY=VALUE>",
|
|
129094
129352
|
"extra environment baked into the scheduled job; repeatable, and merged LAST so it wins over PATH/HOME/ANTHROPIC_MODEL",
|
|
@@ -129109,24 +129367,39 @@ taskCommand.command("schedule <slug>").description("Schedule a headless task run
|
|
|
129109
129367
|
})
|
|
129110
129368
|
);
|
|
129111
129369
|
taskCommand.command("unschedule <slug>").description(
|
|
129112
|
-
"Remove a task's schedule \u2014 from EVERY label namespace it is armed in
|
|
129370
|
+
"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
129371
|
).action((slug) => {
|
|
129114
129372
|
const removed = unscheduleEverywhere(slug);
|
|
129115
129373
|
if (removed.length === 0) {
|
|
129116
|
-
console.log(`\u2139\uFE0F nothing scheduled for ${slug} \u2014 no
|
|
129374
|
+
console.log(`\u2139\uFE0F nothing scheduled for ${slug} \u2014 no job was armed under any label namespace.`);
|
|
129117
129375
|
return;
|
|
129118
129376
|
}
|
|
129119
129377
|
console.log(`\u{1F5D1}\uFE0F unscheduled ${slug} (${removed.join(", ")})`);
|
|
129120
129378
|
});
|
|
129121
129379
|
taskCommand.command("schedules").description(
|
|
129122
|
-
"List every scheduled job and the environment each armed job carries \u2014 across
|
|
129380
|
+
"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
129381
|
).action(() => {
|
|
129124
129382
|
const entries = listAllScheduleEntries();
|
|
129125
|
-
console.log(
|
|
129126
|
-
|
|
129127
|
-
|
|
129128
|
-
|
|
129129
|
-
|
|
129383
|
+
console.log(entries.length ? entries.map(formatScheduleEntryLine).join("\n") : "no scheduled tasks");
|
|
129384
|
+
});
|
|
129385
|
+
taskCommand.command("sync-skills").description(
|
|
129386
|
+
"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)"
|
|
129387
|
+
).option("--global", "write to the home-dir root (~/.claude/skills | ~/.codex/skills) instead of the project-local one").option(
|
|
129388
|
+
"--target <target>",
|
|
129389
|
+
"host agent skills dir to sync: claude | codex | both (default: auto \u2014 every host detected as present; none detected falls back to claude)"
|
|
129390
|
+
).action(async (opts) => {
|
|
129391
|
+
try {
|
|
129392
|
+
const targets = resolveSyncSkillsCliTargets(opts.target);
|
|
129393
|
+
for (const target of targets) {
|
|
129394
|
+
const r = await syncSkills({ global: opts.global, target });
|
|
129395
|
+
console.log(
|
|
129396
|
+
`\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)` : ""}`
|
|
129397
|
+
);
|
|
129398
|
+
}
|
|
129399
|
+
} catch (err) {
|
|
129400
|
+
console.error(`\u274C ${err instanceof Error ? err.message : String(err)}`);
|
|
129401
|
+
process.exitCode = 1;
|
|
129402
|
+
}
|
|
129130
129403
|
});
|
|
129131
129404
|
function runVerb(fn) {
|
|
129132
129405
|
fn().catch((err) => {
|
|
@@ -129159,130 +129432,6 @@ memoryCommand.command("attach <slug>", { hidden: true }).allowUnknownOption(true
|
|
|
129159
129432
|
);
|
|
129160
129433
|
taskCommand.addCommand(memoryCommand);
|
|
129161
129434
|
|
|
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
129435
|
// src/commands/delegate.ts
|
|
129287
129436
|
init_esm_shims();
|
|
129288
129437
|
function toDelegateOptions(task, opts) {
|
|
@@ -129373,8 +129522,6 @@ program2.command("delegate [task]").description(
|
|
|
129373
129522
|
).action(delegateCommand);
|
|
129374
129523
|
program2.addCommand(connectionsCommand);
|
|
129375
129524
|
program2.addCommand(taskCommand);
|
|
129376
|
-
program2.addCommand(loopCommand);
|
|
129377
|
-
program2.addCommand(compoundCommand);
|
|
129378
129525
|
program2.addCommand(walletCommand);
|
|
129379
129526
|
program2.parseAsync().catch((e) => {
|
|
129380
129527
|
console.error(e instanceof Error ? e.message : String(e));
|