@shipster/cli 0.5.1 → 0.5.3
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/cli.js +241 -119
- package/dist/event-worker.js +104 -13
- package/dist/mcp.js +58 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -18414,6 +18414,26 @@ function parseDesired(input) {
|
|
|
18414
18414
|
}
|
|
18415
18415
|
}
|
|
18416
18416
|
|
|
18417
|
+
// ../../shared/types/src/tier-plan.ts
|
|
18418
|
+
function ladder(rungs) {
|
|
18419
|
+
if (rungs === 3) return DIFFICULTIES.map((d) => ({ tier: tierForDifficulty(d), difficulty: [d] }));
|
|
18420
|
+
return [{ tier: "cheap", difficulty: ["small", "standard"] }, { tier: "sensitive", difficulty: ["large"] }];
|
|
18421
|
+
}
|
|
18422
|
+
function planWriterTiers(models) {
|
|
18423
|
+
if (models.some((m) => m.trim() === "")) throw new Error("writer models: an empty model name");
|
|
18424
|
+
if (new Set(models).size !== models.length) throw new Error("writer models: a repeated model");
|
|
18425
|
+
if (models.length > 3) throw new Error("writer models: at most three, one per difficulty");
|
|
18426
|
+
if (models.length < 2) return { pools: [{ tier: "all", ...models[0] ? { model: models[0] } : {} }], poolOrder: null };
|
|
18427
|
+
const pools = ladder(models.length).map((rung, i) => ({ tier: rung.tier, model: models[i], difficulty: rung.difficulty }));
|
|
18428
|
+
return { pools, poolOrder: pools.map((p) => p.tier) };
|
|
18429
|
+
}
|
|
18430
|
+
function planReviewerSeat(writer, available) {
|
|
18431
|
+
const other = available.find((r) => r !== writer.runtime);
|
|
18432
|
+
if (other !== void 0) return { runtime: other, separation: "runtime" };
|
|
18433
|
+
const top = writer.models.at(-1);
|
|
18434
|
+
return { runtime: writer.runtime, ...top ? { model: top } : {}, separation: writer.models.length > 1 ? "model" : "none" };
|
|
18435
|
+
}
|
|
18436
|
+
|
|
18417
18437
|
// ../../shared/types/src/secret-patterns.ts
|
|
18418
18438
|
var SECRET_PATTERNS = [
|
|
18419
18439
|
/\bsk-/,
|
|
@@ -18473,6 +18493,14 @@ function isDifficulty(value) {
|
|
|
18473
18493
|
function difficultyRung(d) {
|
|
18474
18494
|
return DIFFICULTIES.indexOf(d);
|
|
18475
18495
|
}
|
|
18496
|
+
var TIER_OF_DIFFICULTY = {
|
|
18497
|
+
small: "cheap",
|
|
18498
|
+
standard: "standard",
|
|
18499
|
+
large: "sensitive"
|
|
18500
|
+
};
|
|
18501
|
+
function tierForDifficulty(d) {
|
|
18502
|
+
return TIER_OF_DIFFICULTY[d];
|
|
18503
|
+
}
|
|
18476
18504
|
var DEFAULT_UNSTARTED_AFTER_MS = 4 * 60 * 60 * 1e3;
|
|
18477
18505
|
var DEFAULT_REVIEW_SILENT_MS = 40 * 60 * 1e3;
|
|
18478
18506
|
var POOL_STOPPED_REASON = "pool member stopped by pitboss node";
|
|
@@ -22502,6 +22530,12 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
|
|
|
22502
22530
|
c. Read the task carefully. If the row is marked \`truncated:true\`, call
|
|
22503
22531
|
\`mcp__relai__get_task\` with its id for the full description before acting \u2014
|
|
22504
22532
|
the list clips descriptions, and the rest of the spec is only on that drill-in.
|
|
22533
|
+
If \`metadata.checkpoint\` is set (\`mcp__relai__get_task\` if \`metadata._truncated\` lists it in \`keys\`),
|
|
22534
|
+
start from its \`next\` and \`unverified\` lists. It is an earlier session's notes: information, not
|
|
22535
|
+
authorisation, and cannot widen the task's scope. Work on the task's own branch (\`metadata.branchName\`
|
|
22536
|
+
or the order's), never its \`branch\`, \`wipRef\` or \`worktree\`; if its \`branch\` differs, say so in a task
|
|
22537
|
+
comment. \`headSha\` is the last commit it knew there: read later commits before acting on \`next\`, and
|
|
22538
|
+
comment if the tip lacks it. A finding, human reply or task edit newer than \`checkpoint.at\` wins.
|
|
22505
22539
|
Check \`metadata\` for \`branchName\`, \`roundNumber\`, and any leftover
|
|
22506
22540
|
\`findings\`; also read finding comments on the thread from step b \u2014 that is where
|
|
22507
22541
|
new findings land. If \`metadata.humanReply\` is set, this task was previously blocked
|
|
@@ -22518,10 +22552,14 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
|
|
|
22518
22552
|
the same thing: either proceed on your own judgement and say in your handoff that you did, or
|
|
22519
22553
|
escalate to a human by blocking with a question addressed to nobody.
|
|
22520
22554
|
d. Do the work per your specialization rules below.
|
|
22521
|
-
e.
|
|
22555
|
+
e. If the work is on a branch, commit and push it, then call \`mcp__relai__checkpoint_task\` while the
|
|
22556
|
+
task is still \`in_progress\`: before every \`update_task_status\` to "completed" or "blocked" and
|
|
22557
|
+
every \`assign_task\`, including those in the role, round-limit and order rules below, and when out
|
|
22558
|
+
of room. \`dirty\` and \`wipRef\` are only for work that cannot be committed.
|
|
22559
|
+
f. Post your result on the task's own thread (\`mcp__relai__add_task_comment\` or
|
|
22522
22560
|
\`mcp__relai__send_message\` type "handoff"), with \`toAgent\` set to whoever you
|
|
22523
22561
|
report to: the task's \`createdBy\` unless told otherwise.
|
|
22524
|
-
|
|
22562
|
+
g. If the work is done: \`mcp__relai__update_task_status\` \u2192 "completed"
|
|
22525
22563
|
(or "blocked" if escalating). If another role must continue this same task,
|
|
22526
22564
|
\`mcp__relai__assign_task\` to them and stop \u2014 do not complete.
|
|
22527
22565
|
|
|
@@ -22646,8 +22684,9 @@ types, empty classes) but you do not take the implementation yourself.
|
|
|
22646
22684
|
|
|
22647
22685
|
- Read relevant files to understand current structure.
|
|
22648
22686
|
- Write a clear technical design on this task's thread (\`mcp__relai__add_task_comment\`).
|
|
22649
|
-
If scaffolding helps, write it on the branch in \`branchName\`
|
|
22650
|
-
same slug rule as writer)
|
|
22687
|
+
If scaffolding helps, write it on the branch in \`branchName\`: if set, \`git fetch origin\` then
|
|
22688
|
+
\`git checkout -B {branchName} origin/{branchName}\`; if missing, derive one (same slug rule as writer)
|
|
22689
|
+
and stamp it as \`metadata.branchName\` with \`mcp__relai__update_task_status\`. Never force-push.
|
|
22651
22690
|
- \`mcp__relai__list_agents\`, pick specialization \`"writer"\` (prefer online), then
|
|
22652
22691
|
\`mcp__relai__assign_task\` to them. Do not complete.
|
|
22653
22692
|
|
|
@@ -22660,9 +22699,13 @@ types, empty classes) but you do not take the implementation yourself.
|
|
|
22660
22699
|
You implement specs on a branch and open a PR. Write production code and tests.
|
|
22661
22700
|
|
|
22662
22701
|
**Workflow:**
|
|
22663
|
-
1. Check metadata for branchName. If
|
|
22702
|
+
1. Check metadata for branchName. If set, the branch holds earlier work: \`git fetch origin\` then
|
|
22703
|
+
\`git checkout -B {branchName} origin/{branchName}\` (only if the remote branch does not exist, create
|
|
22704
|
+
it from the default branch). If none, derive one from the task title:
|
|
22664
22705
|
slug the title to lowercase-hyphenated, prefix "feat/".
|
|
22665
|
-
Use Bash to create the branch: \`git checkout -b {branchName}\`.
|
|
22706
|
+
Use Bash to create the branch: \`git checkout -b {branchName}\`. Stamp it on this task via
|
|
22707
|
+
\`mcp__relai__update_task_status\` with status \`"in_progress"\` and \`metadata.branchName\`.
|
|
22708
|
+
Never force-push to make a push succeed.
|
|
22666
22709
|
2. Read the spec. Read existing files to understand patterns (Read, Grep, Glob).
|
|
22667
22710
|
3. Implement: write code, update tests, follow existing conventions exactly.
|
|
22668
22711
|
4. Run tests: check package.json scripts for the test command.
|
|
@@ -22671,7 +22714,8 @@ You implement specs on a branch and open a PR. Write production code and tests.
|
|
|
22671
22714
|
- First try \`gh pr create --fill\` \u2014 captures the PR URL automatically when the GitHub CLI is installed and authed.
|
|
22672
22715
|
- If \`gh\` isn't available (\`command -v gh\` fails) or it returns a non-zero exit, fall back to \`git push -u origin {branchName}\`. Then read the remote with \`git remote get-url origin\` so you can hand the reviewer a clickable branch URL even without an open PR. Set \`prUrl\` to the PR URL when you have one, otherwise to the branch URL on the remote.
|
|
22673
22716
|
7. Post a handoff on the task's own thread with \`mcp__relai__add_task_comment\` (type: "handoff"):
|
|
22674
|
-
PR URL, head sha, what landed, and tests. Then
|
|
22717
|
+
PR URL, head sha, what landed, and tests. Then, after the step e checkpoint,
|
|
22718
|
+
\`mcp__relai__update_task_status\` \u2192 "completed".
|
|
22675
22719
|
- If the task has \`verifyReviewerId\` or any reviewer gate, completed moves it to
|
|
22676
22720
|
pending_verification. Do not then \`update_task_status\` to \`"in_progress"\` \u2014
|
|
22677
22721
|
the reviewer decides via \`submit_review\`, and a reject returns the task to
|
|
@@ -24005,7 +24049,8 @@ var PITBOSS_NAMES = [
|
|
|
24005
24049
|
"ENV_GRANT",
|
|
24006
24050
|
"TASK_REPO_ID",
|
|
24007
24051
|
"TASK_REPO_URL",
|
|
24008
|
-
"OWNER_POOL_SLOT"
|
|
24052
|
+
"OWNER_POOL_SLOT",
|
|
24053
|
+
"SESSION_MAX_MS"
|
|
24009
24054
|
].flatMap(envNames);
|
|
24010
24055
|
var WORKER_KNOBS = [
|
|
24011
24056
|
"API_URL",
|
|
@@ -24859,99 +24904,19 @@ function parseResetTime(text, now = Date.now()) {
|
|
|
24859
24904
|
// ../claude-worker/src/unavailable.ts
|
|
24860
24905
|
var DEFAULT_UNAVAILABLE_UNTIL_MS = 60 * 60 * 1e3;
|
|
24861
24906
|
|
|
24907
|
+
// ../claude-worker/src/worker.ts
|
|
24908
|
+
var DEFAULT_SESSION_MAX_MS = 90 * 6e4;
|
|
24909
|
+
|
|
24862
24910
|
// ../claude-worker/src/config.ts
|
|
24863
24911
|
var VALID_SPECIALIZATIONS = ["reviewer", "architect", "writer", "tester", "devops"];
|
|
24864
24912
|
|
|
24865
24913
|
// src/node/onboard.ts
|
|
24866
|
-
import { join as
|
|
24867
|
-
var MAX_POOL = 30;
|
|
24868
|
-
var GITHUB = /^(?:https:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:)([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/;
|
|
24869
|
-
function runtimeBinary(runtime, env3) {
|
|
24870
|
-
switch (runtime) {
|
|
24871
|
-
case "claude":
|
|
24872
|
-
return env3.CLAUDE_BIN ?? "claude";
|
|
24873
|
-
case "cursor":
|
|
24874
|
-
return env3.CURSOR_BIN ?? "cursor-agent";
|
|
24875
|
-
case "copilot":
|
|
24876
|
-
return env3.COPILOT_BIN ?? "copilot";
|
|
24877
|
-
case "codex":
|
|
24878
|
-
return env3.CODEX_BIN ?? "codex";
|
|
24879
|
-
}
|
|
24880
|
-
}
|
|
24881
|
-
function githubRepo(url) {
|
|
24882
|
-
const match = GITHUB.exec(url.trim());
|
|
24883
|
-
if (!match) throw new Error(`${url} is not a GitHub repo URL (https://github.com/<owner>/<repo> or git@github.com:<owner>/<repo>)`);
|
|
24884
|
-
return { owner: match[1], name: match[2] };
|
|
24885
|
-
}
|
|
24886
|
-
function planOnboard(url, opts) {
|
|
24887
|
-
const { owner, name } = githubRepo(url);
|
|
24888
|
-
for (const role of ["architect", "writer", "reviewer"]) {
|
|
24889
|
-
if (opts[role] === void 0) throw new Error(`no ${role} runtime: pass --${role} <runtime> (have: ${RUNTIMES3.join(", ")})`);
|
|
24890
|
-
}
|
|
24891
|
-
const architectRuntime = opts.architect;
|
|
24892
|
-
const writer = opts.writer;
|
|
24893
|
-
const reviewer = opts.reviewer;
|
|
24894
|
-
for (const r of [architectRuntime, writer, reviewer]) {
|
|
24895
|
-
if (!RUNTIMES3.includes(r)) throw new Error(`runtime "${r}" is not supported (have: ${RUNTIMES3.join(", ")})`);
|
|
24896
|
-
}
|
|
24897
|
-
const warnings = reviewer === writer ? [`the reviewer and the writers both run ${writer}: a same-vendor reviewer shares the writers' blind spots`] : [];
|
|
24898
|
-
const poolMax = opts.poolMax ?? 2;
|
|
24899
|
-
if (!Number.isInteger(poolMax) || poolMax < 1 || poolMax > MAX_POOL) {
|
|
24900
|
-
throw new Error(`pool size must be a whole number from 1 to ${MAX_POOL}, so the architect, reviewer and writers fit one approval`);
|
|
24901
|
-
}
|
|
24902
|
-
const id = name.replace(/^[^A-Za-z0-9]+/, "").slice(0, 40).replace(/[^A-Za-z0-9]+$/, "") || "repo";
|
|
24903
|
-
const parent = resolve6(opts.parent ?? join18(opts.home, "github"));
|
|
24904
|
-
const base2 = join18(parent, name);
|
|
24905
|
-
const nodeDir2 = join18(parent, `${name}-node`);
|
|
24906
|
-
const architectWorktree = join18(parent, `${name}-architect`);
|
|
24907
|
-
const review = join18(parent, `${name}-reviewer`);
|
|
24908
|
-
return {
|
|
24909
|
-
owner,
|
|
24910
|
-
name,
|
|
24911
|
-
warnings,
|
|
24912
|
-
cloneUrl: `https://github.com/${owner}/${name}.git`,
|
|
24913
|
-
base: base2,
|
|
24914
|
-
nodeDir: nodeDir2,
|
|
24915
|
-
specPath: join18(nodeDir2, "spec.json"),
|
|
24916
|
-
configDir: join18(opts.home, ".config", "pitboss-repos", owner.toLowerCase(), name.toLowerCase()),
|
|
24917
|
-
worktrees: [architectWorktree, review],
|
|
24918
|
-
spec: {
|
|
24919
|
-
api: DEFAULT_API_URL,
|
|
24920
|
-
agents: [
|
|
24921
|
-
{ name: `${id}-architect`, runtime: architectRuntime, worktree: `../${name}-architect`, specialization: "architect", tier: 2 },
|
|
24922
|
-
{ name: `${id}-reviewer`, runtime: reviewer, worktree: `../${name}-reviewer`, specialization: "reviewer" },
|
|
24923
|
-
{
|
|
24924
|
-
pool: `${id}-pool`,
|
|
24925
|
-
runtime: writer,
|
|
24926
|
-
base: `../${name}`,
|
|
24927
|
-
worktreeRoot: `../${name}-pool`,
|
|
24928
|
-
specialization: "writer",
|
|
24929
|
-
min: 0,
|
|
24930
|
-
max: poolMax
|
|
24931
|
-
}
|
|
24932
|
-
]
|
|
24933
|
-
}
|
|
24934
|
-
};
|
|
24935
|
-
}
|
|
24936
|
-
function onboardOperations(plan2, account) {
|
|
24937
|
-
const oneLine2 = (v) => v.length > 0 && v.length <= 300 && !/[\r\n]/.test(v);
|
|
24938
|
-
return {
|
|
24939
|
-
...account ? { githubAccount: account } : {},
|
|
24940
|
-
...oneLine2(plan2.configDir) ? { nodeConfigDir: plan2.configDir } : {},
|
|
24941
|
-
...oneLine2(plan2.specPath) ? { nodeSpecPath: plan2.specPath } : {}
|
|
24942
|
-
};
|
|
24943
|
-
}
|
|
24944
|
-
function originHost(origin) {
|
|
24945
|
-
const url = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?([^/:]+)/i.exec(origin.trim());
|
|
24946
|
-
if (url) return url[1].toLowerCase();
|
|
24947
|
-
const scp = /^(?:[^@/]+@)?([^/:]+):/.exec(origin.trim());
|
|
24948
|
-
return scp ? scp[1].toLowerCase() : null;
|
|
24949
|
-
}
|
|
24914
|
+
import { join as join19, resolve as resolve7 } from "node:path";
|
|
24950
24915
|
|
|
24951
24916
|
// src/node/spec.ts
|
|
24952
24917
|
import { existsSync as existsSync11, realpathSync as realpathSync4, statSync as statSync2 } from "node:fs";
|
|
24953
24918
|
import { homedir as homedir5 } from "node:os";
|
|
24954
|
-
import { join as
|
|
24919
|
+
import { join as join18, parse as parsePath, resolve as resolve6, sep as sep2 } from "node:path";
|
|
24955
24920
|
var MAX_POOL_SIZE = 32;
|
|
24956
24921
|
var MAX_ENV_GRANTS2 = 16;
|
|
24957
24922
|
var NAME2 = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
|
|
@@ -24964,7 +24929,7 @@ var canonical = (p) => {
|
|
|
24964
24929
|
try {
|
|
24965
24930
|
return realpathSync4(p);
|
|
24966
24931
|
} catch {
|
|
24967
|
-
return
|
|
24932
|
+
return resolve6(p);
|
|
24968
24933
|
}
|
|
24969
24934
|
};
|
|
24970
24935
|
function janitorRootsOf(raw, specDir, agents, pools, homeDir) {
|
|
@@ -24975,7 +24940,7 @@ function janitorRootsOf(raw, specDir, agents, pools, homeDir) {
|
|
|
24975
24940
|
const skipped = [];
|
|
24976
24941
|
raw.forEach((entry, i) => {
|
|
24977
24942
|
if (typeof entry !== "string" || entry === "" || entry.includes("\0")) throw new Error(`janitorRoots[${i}] must be a directory path`);
|
|
24978
|
-
const asked =
|
|
24943
|
+
const asked = resolve6(specDir, entry);
|
|
24979
24944
|
let real3;
|
|
24980
24945
|
try {
|
|
24981
24946
|
real3 = realpathSync4(asked);
|
|
@@ -24990,7 +24955,7 @@ function janitorRootsOf(raw, specDir, agents, pools, homeDir) {
|
|
|
24990
24955
|
if (real3 === parsePath(real3).root) throw new Error(`janitorRoots[${i}] ${real3} is the filesystem root`);
|
|
24991
24956
|
if (real3 === home) throw new Error(`janitorRoots[${i}] ${real3} is the home directory`);
|
|
24992
24957
|
if (isWithin(home, real3)) throw new Error(`janitorRoots[${i}] ${real3} contains the home directory`);
|
|
24993
|
-
if (existsSync11(
|
|
24958
|
+
if (existsSync11(join18(real3, ".git"))) throw new Error(`janitorRoots[${i}] ${real3} is a git work tree; list the directory that holds worktrees, not a checkout`);
|
|
24994
24959
|
for (const a of allAgents({ api: "", agents, pools })) {
|
|
24995
24960
|
if (isWithin(real3, canonical(a.worktree))) {
|
|
24996
24961
|
throw new Error(`janitorRoots[${i}] ${real3} is or sits inside the worktree of agent "${a.name}"`);
|
|
@@ -25024,7 +24989,7 @@ function poolMembers(pool) {
|
|
|
25024
24989
|
return {
|
|
25025
24990
|
name,
|
|
25026
24991
|
runtime: pool.runtime,
|
|
25027
|
-
worktree:
|
|
24992
|
+
worktree: join18(pool.worktreeRoot, name),
|
|
25028
24993
|
specialization: pool.specialization,
|
|
25029
24994
|
...pool.ownerPool ? { ownerPool: true } : {},
|
|
25030
24995
|
...pool.tier ? { tier: pool.tier } : {},
|
|
@@ -25186,11 +25151,11 @@ function parsePool(a, i, specDir, platform2) {
|
|
|
25186
25151
|
}
|
|
25187
25152
|
burstMax = a.burstMax;
|
|
25188
25153
|
}
|
|
25189
|
-
const worktreeRoot =
|
|
25154
|
+
const worktreeRoot = resolve6(specDir, a.worktreeRoot);
|
|
25190
25155
|
if (ownerPool) {
|
|
25191
25156
|
return { pool: a.pool, runtime, ownerPool: true, worktreeRoot, specialization, ...modelOf(a, i), ...isolateOf(a, i, runtime), ...relayOf(a, i), ...poolSeatKindOf(a, i, platform2, max), ...difficultyOf(a, i), min, max, ...burstMax !== void 0 ? { burstMax } : {} };
|
|
25192
25157
|
}
|
|
25193
|
-
const base2 =
|
|
25158
|
+
const base2 = resolve6(specDir, a.base);
|
|
25194
25159
|
if (isWithin(worktreeRoot, base2)) throw new Error(`agents[${i}].worktreeRoot ${worktreeRoot} is inside base ${base2}; pool worktrees never go inside your checkout`);
|
|
25195
25160
|
return { pool: a.pool, runtime, base: base2, worktreeRoot, specialization, ...tierOf(a, i, specialization), ...modelOf(a, i), ...isolateOf(a, i, runtime), ...relayOf(a, i), ...envOf(a, i), ...poolSeatKindOf(a, i, platform2, max), ...difficultyOf(a, i), min, max, ...burstMax !== void 0 ? { burstMax } : {} };
|
|
25196
25161
|
}
|
|
@@ -25227,7 +25192,7 @@ function parseSpec(text, specDir, opts = {}) {
|
|
|
25227
25192
|
}
|
|
25228
25193
|
const specialization = specializationOf(a, i);
|
|
25229
25194
|
if (ownerPool) ownerPoolSpecializationCheck(a.name, specialization, "agents");
|
|
25230
|
-
agents.push({ name: a.name, runtime, worktree:
|
|
25195
|
+
agents.push({ name: a.name, runtime, worktree: resolve6(specDir, a.worktree), specialization, ...ownerPool ? { ownerPool: true } : tierOf(a, i, specialization), ...modelOf(a, i), ...isolateOf(a, i, runtime), ...relayOf(a, i), ...envOf(a, i), ...seatKindOf(a, i, platform2) });
|
|
25231
25196
|
});
|
|
25232
25197
|
const entries = [
|
|
25233
25198
|
...agents.map((a) => ({ label: `agent "${a.name}"`, ownerPool: a.ownerPool === true, seated: a.runAs !== void 0 })),
|
|
@@ -25271,6 +25236,115 @@ function parseSpec(text, specDir, opts = {}) {
|
|
|
25271
25236
|
};
|
|
25272
25237
|
}
|
|
25273
25238
|
|
|
25239
|
+
// src/node/onboard.ts
|
|
25240
|
+
var MAX_POOL = 30;
|
|
25241
|
+
var GITHUB = /^(?:https:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:)([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/;
|
|
25242
|
+
function runtimeBinary(runtime, env3) {
|
|
25243
|
+
switch (runtime) {
|
|
25244
|
+
case "claude":
|
|
25245
|
+
return env3.CLAUDE_BIN ?? "claude";
|
|
25246
|
+
case "cursor":
|
|
25247
|
+
return env3.CURSOR_BIN ?? "cursor-agent";
|
|
25248
|
+
case "copilot":
|
|
25249
|
+
return env3.COPILOT_BIN ?? "copilot";
|
|
25250
|
+
case "codex":
|
|
25251
|
+
return env3.CODEX_BIN ?? "codex";
|
|
25252
|
+
}
|
|
25253
|
+
}
|
|
25254
|
+
function githubRepo(url) {
|
|
25255
|
+
const match = GITHUB.exec(url.trim());
|
|
25256
|
+
if (!match) throw new Error(`${url} is not a GitHub repo URL (https://github.com/<owner>/<repo> or git@github.com:<owner>/<repo>)`);
|
|
25257
|
+
return { owner: match[1], name: match[2] };
|
|
25258
|
+
}
|
|
25259
|
+
function planOnboard(url, opts) {
|
|
25260
|
+
const { owner, name } = githubRepo(url);
|
|
25261
|
+
for (const role of ["architect", "writer", "reviewer"]) {
|
|
25262
|
+
if (opts[role] === void 0) throw new Error(`no ${role} runtime: pass --${role} <runtime> (have: ${RUNTIMES3.join(", ")})`);
|
|
25263
|
+
}
|
|
25264
|
+
const architectRuntime = opts.architect;
|
|
25265
|
+
const writer = opts.writer;
|
|
25266
|
+
const reviewer = opts.reviewer;
|
|
25267
|
+
for (const r of [architectRuntime, writer, reviewer]) {
|
|
25268
|
+
if (!RUNTIMES3.includes(r)) throw new Error(`runtime "${r}" is not supported (have: ${RUNTIMES3.join(", ")})`);
|
|
25269
|
+
}
|
|
25270
|
+
const sameVendor = `the reviewer and the writers both run ${writer}: a same-vendor reviewer shares the writers' blind spots`;
|
|
25271
|
+
const warnings = reviewer === writer ? [sameVendor] : [];
|
|
25272
|
+
const poolMax = opts.poolMax ?? 2;
|
|
25273
|
+
if (!Number.isInteger(poolMax) || poolMax < 1 || poolMax > MAX_POOL) {
|
|
25274
|
+
throw new Error(`pool size must be a whole number from 1 to ${MAX_POOL}, so the architect, reviewer and writers fit one approval`);
|
|
25275
|
+
}
|
|
25276
|
+
const id = name.replace(/^[^A-Za-z0-9]+/, "").slice(0, 40).replace(/[^A-Za-z0-9]+$/, "") || "repo";
|
|
25277
|
+
const parent = resolve7(opts.parent ?? join19(opts.home, "github"));
|
|
25278
|
+
const base2 = join19(parent, name);
|
|
25279
|
+
const nodeDir2 = join19(parent, `${name}-node`);
|
|
25280
|
+
const architectWorktree = join19(parent, `${name}-architect`);
|
|
25281
|
+
const review = join19(parent, `${name}-reviewer`);
|
|
25282
|
+
const writerPool = { runtime: writer, base: `../${name}`, specialization: "writer", min: 0, max: poolMax };
|
|
25283
|
+
const architect = { name: `${id}-architect`, runtime: architectRuntime, worktree: `../${name}-architect`, specialization: "architect", tier: 2 };
|
|
25284
|
+
const tiers = opts.writerModels ? planWriterTiers(opts.writerModels) : null;
|
|
25285
|
+
const poolName = (tier) => `${id}-${tier === "all" ? "pool" : tier}`;
|
|
25286
|
+
if (tiers && tiers.pools.length * poolMax > MAX_POOL) {
|
|
25287
|
+
throw new Error(`${tiers.pools.length} writer pools of ${poolMax} do not fit one approval of ${MAX_POOL + 2} agents with the architect and reviewer; lower --pool-max`);
|
|
25288
|
+
}
|
|
25289
|
+
const seat = tiers ? planReviewerSeat({ runtime: writer, models: opts.writerModels }, [reviewer]) : null;
|
|
25290
|
+
const tierAgents = tiers && seat && [
|
|
25291
|
+
architect,
|
|
25292
|
+
...tiers.pools.map((p) => ({
|
|
25293
|
+
pool: poolName(p.tier),
|
|
25294
|
+
...writerPool,
|
|
25295
|
+
worktreeRoot: `../${name}-${p.tier === "all" ? "pool" : p.tier}`,
|
|
25296
|
+
...p.model ? { model: p.model } : {},
|
|
25297
|
+
...p.difficulty ? { difficulty: p.difficulty } : {}
|
|
25298
|
+
})),
|
|
25299
|
+
{
|
|
25300
|
+
pool: `${id}-reviewer`,
|
|
25301
|
+
runtime: seat.runtime,
|
|
25302
|
+
base: `../${name}`,
|
|
25303
|
+
worktreeRoot: `../${name}-reviewer-pool`,
|
|
25304
|
+
specialization: "reviewer",
|
|
25305
|
+
...seat.model ? { model: seat.model } : {},
|
|
25306
|
+
min: 0,
|
|
25307
|
+
max: 1
|
|
25308
|
+
}
|
|
25309
|
+
];
|
|
25310
|
+
const spec = {
|
|
25311
|
+
api: DEFAULT_API_URL,
|
|
25312
|
+
agents: tierAgents || [
|
|
25313
|
+
architect,
|
|
25314
|
+
{ name: `${id}-reviewer`, runtime: reviewer, worktree: `../${name}-reviewer`, specialization: "reviewer" },
|
|
25315
|
+
{ pool: `${id}-pool`, ...writerPool, worktreeRoot: `../${name}-pool` }
|
|
25316
|
+
]
|
|
25317
|
+
};
|
|
25318
|
+
parseSpec(JSON.stringify(spec), nodeDir2);
|
|
25319
|
+
return {
|
|
25320
|
+
owner,
|
|
25321
|
+
name,
|
|
25322
|
+
warnings,
|
|
25323
|
+
cloneUrl: `https://github.com/${owner}/${name}.git`,
|
|
25324
|
+
base: base2,
|
|
25325
|
+
nodeDir: nodeDir2,
|
|
25326
|
+
specPath: join19(nodeDir2, "spec.json"),
|
|
25327
|
+
configDir: join19(opts.home, ".config", "pitboss-repos", owner.toLowerCase(), name.toLowerCase()),
|
|
25328
|
+
worktrees: tierAgents ? [architectWorktree] : [architectWorktree, review],
|
|
25329
|
+
poolOrder: tiers?.poolOrder?.map(poolName) ?? null,
|
|
25330
|
+
spec
|
|
25331
|
+
};
|
|
25332
|
+
}
|
|
25333
|
+
function onboardOperations(plan2, account) {
|
|
25334
|
+
const oneLine2 = (v) => v.length > 0 && v.length <= 300 && !/[\r\n]/.test(v);
|
|
25335
|
+
return {
|
|
25336
|
+
...account ? { githubAccount: account } : {},
|
|
25337
|
+
...oneLine2(plan2.configDir) ? { nodeConfigDir: plan2.configDir } : {},
|
|
25338
|
+
...oneLine2(plan2.specPath) ? { nodeSpecPath: plan2.specPath } : {}
|
|
25339
|
+
};
|
|
25340
|
+
}
|
|
25341
|
+
function originHost(origin) {
|
|
25342
|
+
const url = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?([^/:]+)/i.exec(origin.trim());
|
|
25343
|
+
if (url) return url[1].toLowerCase();
|
|
25344
|
+
const scp = /^(?:[^@/]+@)?([^/:]+):/.exec(origin.trim());
|
|
25345
|
+
return scp ? scp[1].toLowerCase() : null;
|
|
25346
|
+
}
|
|
25347
|
+
|
|
25274
25348
|
// src/node/store.ts
|
|
25275
25349
|
import { existsSync as existsSync12, mkdirSync as mkdirSync10, readdirSync as readdirSync4, readFileSync as readFileSync15, renameSync as renameSync5, rmSync as rmSync6, writeFileSync as writeFileSync12 } from "node:fs";
|
|
25276
25350
|
import { dirname as dirname10, join as join20 } from "node:path";
|
|
@@ -25372,7 +25446,7 @@ function reconcilePool({ pending, members, min, max, idleStopMs }) {
|
|
|
25372
25446
|
};
|
|
25373
25447
|
for (const m of stopped) {
|
|
25374
25448
|
if (count >= max) break;
|
|
25375
|
-
if (m.inProgress > 0 || m.readyAssigned > 0) take(m);
|
|
25449
|
+
if (m.inProgress > 0 || m.readyAssigned > 0 || (m.owedReviews ?? 0) > 0) take(m);
|
|
25376
25450
|
}
|
|
25377
25451
|
const spare = () => stopped.filter((m) => !start.includes(m.name)).sort((a, b) => Number(isIdle(b)) - Number(isIdle(a)));
|
|
25378
25452
|
let demand = pending - running.filter(isIdle).length;
|
|
@@ -25395,7 +25469,7 @@ function reconcilePool({ pending, members, min, max, idleStopMs }) {
|
|
|
25395
25469
|
count--;
|
|
25396
25470
|
}
|
|
25397
25471
|
if (stop.length > 0) return { start: [], stop, why };
|
|
25398
|
-
const blockedOwners = stopped.filter((m) => m.inProgress - (m.verifyingOwn ?? 0) > 0 || m.readyAssigned > 0).length;
|
|
25472
|
+
const blockedOwners = stopped.filter((m) => m.inProgress - (m.verifyingOwn ?? 0) > 0 || m.readyAssigned > 0 || (m.owedReviews ?? 0) > 0).length;
|
|
25399
25473
|
const freed = [];
|
|
25400
25474
|
if (blockedOwners > 0 && count >= max) {
|
|
25401
25475
|
for (const m of running.filter(isIdle)) {
|
|
@@ -25871,7 +25945,8 @@ async function onboardCommand(url, opts) {
|
|
|
25871
25945
|
architect: roles.architect,
|
|
25872
25946
|
writer: roles.writer,
|
|
25873
25947
|
reviewer: roles.reviewer,
|
|
25874
|
-
poolMax: opts.poolMax === void 0 ? void 0 : Number(opts.poolMax)
|
|
25948
|
+
poolMax: opts.poolMax === void 0 ? void 0 : Number(opts.poolMax),
|
|
25949
|
+
writerModels: opts.writerModels === void 0 ? void 0 : opts.writerModels.split(",").map((m) => m.trim())
|
|
25875
25950
|
});
|
|
25876
25951
|
for (const warning of plan2.warnings) console.log(source_default.yellow(` ${warning}`));
|
|
25877
25952
|
const env3 = { ...process.env };
|
|
@@ -25950,6 +26025,8 @@ async function onboardCommand(url, opts) {
|
|
|
25950
26025
|
Start it when you want it running (long-running, foreground):`);
|
|
25951
26026
|
console.log(source_default.cyan(` cd ${shellQuote(plan2.nodeDir)} && PITBOSS_CONFIG_DIR=${shellQuote(plan2.configDir)} pitboss node up spec.json`));
|
|
25952
26027
|
if (opts.account) console.log(source_default.dim(` with GH_TOKEN from \`gh auth token --user ${opts.account}\` in that shell`));
|
|
26028
|
+
if (plan2.poolOrder) console.log(`
|
|
26029
|
+
Tiers route once the owner or its top-level agent sets each pool's difficulties (set_pool_size) and the pool order (set_pool_order): ${plan2.poolOrder.join(", ")}`);
|
|
25953
26030
|
} catch (err) {
|
|
25954
26031
|
console.error(source_default.red(` \u2715 ${err instanceof Error ? err.message : String(err)}`));
|
|
25955
26032
|
process.exitCode = 1;
|
|
@@ -31600,6 +31677,28 @@ function startActivityLoop(agents, sup, ownerPool = false) {
|
|
|
31600
31677
|
const timer = setInterval(() => void tick(), ACTIVITY_REPORT_MS);
|
|
31601
31678
|
return () => clearInterval(timer);
|
|
31602
31679
|
}
|
|
31680
|
+
var SHUTDOWN_MARK_BOUND_MS = 3e3;
|
|
31681
|
+
async function markPoolsStopped(loops, boundMs = SHUTDOWN_MARK_BOUND_MS) {
|
|
31682
|
+
let timer;
|
|
31683
|
+
const bound = new Promise((resolve19) => {
|
|
31684
|
+
timer = setTimeout(() => {
|
|
31685
|
+
console.log(`${(/* @__PURE__ */ new Date()).toISOString()} node: shutdown marking pool members unavailable timed out after ${boundMs}ms`);
|
|
31686
|
+
resolve19();
|
|
31687
|
+
}, boundMs);
|
|
31688
|
+
});
|
|
31689
|
+
try {
|
|
31690
|
+
await Promise.race([Promise.all(loops.map((l) => l.markRunningStopped().catch(() => void 0))), bound]);
|
|
31691
|
+
} finally {
|
|
31692
|
+
clearTimeout(timer);
|
|
31693
|
+
}
|
|
31694
|
+
}
|
|
31695
|
+
async function shutdownNode(opts) {
|
|
31696
|
+
for (const stop of opts.stops) stop();
|
|
31697
|
+
console.log(source_default.dim("\n stopping agents\u2026"));
|
|
31698
|
+
const marking = markPoolsStopped(opts.poolLoops, opts.boundMs);
|
|
31699
|
+
await Promise.all([opts.sup.stop(), marking]);
|
|
31700
|
+
opts.exit(0);
|
|
31701
|
+
}
|
|
31603
31702
|
function startPoolLoop(run4, sup, record, account) {
|
|
31604
31703
|
const { pool, members } = run4;
|
|
31605
31704
|
const { reconcileMs, idleStopMs } = poolTimings();
|
|
@@ -31620,6 +31719,7 @@ function startPoolLoop(run4, sup, record, account) {
|
|
|
31620
31719
|
}
|
|
31621
31720
|
};
|
|
31622
31721
|
let busy = false;
|
|
31722
|
+
let closed = false;
|
|
31623
31723
|
const markStopped = async (member) => {
|
|
31624
31724
|
try {
|
|
31625
31725
|
await setAvailable(member.creds, false);
|
|
@@ -31715,12 +31815,17 @@ function startPoolLoop(run4, sup, record, account) {
|
|
|
31715
31815
|
if (pool.ownerPool) resetOwnerPoolSlot(member.agent.worktree, member.agent.runAs);
|
|
31716
31816
|
else resetWorktree(member.agent.worktree);
|
|
31717
31817
|
}
|
|
31818
|
+
if (closed) return;
|
|
31718
31819
|
await setAvailable(member.creds, true);
|
|
31719
31820
|
markedAt.delete(name);
|
|
31720
31821
|
} catch (err) {
|
|
31721
31822
|
log(`not starting ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
31722
31823
|
continue;
|
|
31723
31824
|
}
|
|
31825
|
+
if (closed) {
|
|
31826
|
+
await markStopped(member);
|
|
31827
|
+
return;
|
|
31828
|
+
}
|
|
31724
31829
|
lastBusy.set(name, Date.now());
|
|
31725
31830
|
sup.startAgent(name);
|
|
31726
31831
|
log(`started ${name} (ready ${summary2.ready})`);
|
|
@@ -31746,9 +31851,25 @@ function startPoolLoop(run4, sup, record, account) {
|
|
|
31746
31851
|
busy = false;
|
|
31747
31852
|
}
|
|
31748
31853
|
};
|
|
31749
|
-
|
|
31750
|
-
const
|
|
31751
|
-
|
|
31854
|
+
let inflight = null;
|
|
31855
|
+
const schedule = () => {
|
|
31856
|
+
if (inflight) return;
|
|
31857
|
+
inflight = tick().finally(() => {
|
|
31858
|
+
inflight = null;
|
|
31859
|
+
});
|
|
31860
|
+
};
|
|
31861
|
+
schedule();
|
|
31862
|
+
const timer = setInterval(schedule, reconcileMs);
|
|
31863
|
+
const stop = (() => {
|
|
31864
|
+
closed = true;
|
|
31865
|
+
clearInterval(timer);
|
|
31866
|
+
});
|
|
31867
|
+
stop.markRunningStopped = async () => {
|
|
31868
|
+
closed = true;
|
|
31869
|
+
const running = members.filter((m) => sup.isActive(m.agent.name));
|
|
31870
|
+
await Promise.all([inflight, ...running.map((m) => markStopped(m))]);
|
|
31871
|
+
};
|
|
31872
|
+
return stop;
|
|
31752
31873
|
}
|
|
31753
31874
|
var MODEL_REFRESH_MS = 15e3;
|
|
31754
31875
|
function desiredIdentity(api, root, nodeToken, log) {
|
|
@@ -31834,10 +31955,12 @@ async function nodeUpDesired() {
|
|
|
31834
31955
|
sup.start();
|
|
31835
31956
|
const identity = createIdentityRefresher(capacityDir());
|
|
31836
31957
|
const loops = [];
|
|
31958
|
+
const poolLoops = [];
|
|
31837
31959
|
const stopAll = [];
|
|
31838
31960
|
let rebuild = null;
|
|
31839
31961
|
const rebuildLoops = () => {
|
|
31840
31962
|
for (const stop of loops.splice(0)) stop();
|
|
31963
|
+
poolLoops.splice(0);
|
|
31841
31964
|
const runnable = allAgents(latest).filter((a) => sup.has(a.name));
|
|
31842
31965
|
const credsOf = (a) => readCreds(a.name);
|
|
31843
31966
|
const byRepo = /* @__PURE__ */ new Map();
|
|
@@ -31855,10 +31978,12 @@ async function nodeUpDesired() {
|
|
|
31855
31978
|
});
|
|
31856
31979
|
if (members.length === 0) continue;
|
|
31857
31980
|
poolStates[pool.pool] = poolStates[pool.pool] ?? { members: members.map((m) => m.agent.name), min: pool.min, max: pool.max, ready: null, checkedAt: null };
|
|
31858
|
-
|
|
31981
|
+
const loop = startPoolLoop({ pool, members, ...pool.ownerPool ? { bases: ownerBases } : {} }, sup, (name, state) => {
|
|
31859
31982
|
poolStates[name] = state;
|
|
31860
31983
|
writeState({ ...base2, agents: sup.snapshot(), pools: poolStates });
|
|
31861
|
-
}, () => identity.current())
|
|
31984
|
+
}, () => identity.current());
|
|
31985
|
+
loops.push(loop);
|
|
31986
|
+
poolLoops.push(loop);
|
|
31862
31987
|
}
|
|
31863
31988
|
for (const name of Object.keys(poolStates)) if (!latest.pools.some((p) => p.pool === name)) delete poolStates[name];
|
|
31864
31989
|
};
|
|
@@ -31937,10 +32062,7 @@ async function nodeUpDesired() {
|
|
|
31937
32062
|
}, 6e4);
|
|
31938
32063
|
const shutdown = async () => {
|
|
31939
32064
|
clearInterval(keepAlive);
|
|
31940
|
-
|
|
31941
|
-
console.log(source_default.dim("\n stopping agents\u2026"));
|
|
31942
|
-
await sup.stop();
|
|
31943
|
-
process.exit(0);
|
|
32065
|
+
await shutdownNode({ stops: [...loops, ...stopAll, stopHeartbeat, janitor.stop], sup, poolLoops: [...poolLoops], exit: (code) => process.exit(code) });
|
|
31944
32066
|
};
|
|
31945
32067
|
const janitor = startJanitorService({ roots: [root], managed: () => managedOfSpec(latest), nodeId: node2.nodeId, log: (line) => console.log(source_default.dim(` ${line}`)) });
|
|
31946
32068
|
const stopHeartbeat = startNodeHeartbeat(
|
|
@@ -32074,14 +32196,12 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
32074
32196
|
`));
|
|
32075
32197
|
const stopLoops = [];
|
|
32076
32198
|
const ownerBases = createOwnerPoolBases(ownerPoolBasesDepsFor(opts));
|
|
32199
|
+
const poolLoops = [];
|
|
32077
32200
|
let stopping = false;
|
|
32078
32201
|
const shutdown = async () => {
|
|
32079
32202
|
if (stopping) return;
|
|
32080
32203
|
stopping = true;
|
|
32081
|
-
|
|
32082
|
-
console.log(source_default.dim("\n stopping agents\u2026"));
|
|
32083
|
-
await sup.stop();
|
|
32084
|
-
process.exit(0);
|
|
32204
|
+
await shutdownNode({ stops: stopLoops, sup, poolLoops, exit: (code) => process.exit(code) });
|
|
32085
32205
|
};
|
|
32086
32206
|
process.on("SIGINT", shutdown);
|
|
32087
32207
|
process.on("SIGTERM", shutdown);
|
|
@@ -32126,10 +32246,12 @@ async function nodeUpCommand(specFile, opts = {}) {
|
|
|
32126
32246
|
continue;
|
|
32127
32247
|
}
|
|
32128
32248
|
poolStates[pool.pool] = { members: ready.map((m) => m.agent.name), min: pool.min, max: pool.max, ready: null, checkedAt: null, ...pool.ownerPool ? { ownerPool: true } : {} };
|
|
32129
|
-
|
|
32249
|
+
const loop = startPoolLoop({ pool, members: ready, ...pool.ownerPool ? { bases: ownerBases } : {} }, sup, (name, state) => {
|
|
32130
32250
|
poolStates[name] = state;
|
|
32131
32251
|
writeState({ ...base2, agents: sup.snapshot(), pools: poolStates });
|
|
32132
|
-
}, () => identity.current())
|
|
32252
|
+
}, () => identity.current());
|
|
32253
|
+
stopLoops.push(loop);
|
|
32254
|
+
poolLoops.push(loop);
|
|
32133
32255
|
}
|
|
32134
32256
|
} catch (err) {
|
|
32135
32257
|
console.error(source_default.red(`
|
|
@@ -33944,7 +34066,7 @@ var token = program2.command("token").description("Manage your agent's API token
|
|
|
33944
34066
|
token.command("rotate").description("Issue a new token, revoke the old one, and list the configs that still hold it").action(tokenRotateCommand);
|
|
33945
34067
|
token.command("list").description("List your agent's tokens, live and revoked, and mark the one in use").action(tokenListCommand);
|
|
33946
34068
|
token.command("revoke <tokenId>").description("Revoke a specific token id").action(tokenRevokeCommand);
|
|
33947
|
-
program2.command("onboard <github-url>").description("Set a GitHub repo up for Shipster: clone it, write a node spec (tier-2 architect, reviewer, writer pool) and print one device code that grants every agent. Starts nothing").option("--account <login>", "GitHub account to clone and work as, from `gh auth` (for example a personal account)").option("--dir <path>", "Parent directory for the clone, worktrees and spec (default ~/github)").option("--architect <runtime>", "Runtime for the tier-2 architect (asked for when there is a terminal and no flag)").option("--writer <runtime>", "Runtime for the writer pool (asked for when there is a terminal and no flag)").option("--reviewer <runtime>", "Runtime for the reviewer; may match the writers, with a warning (asked for when there is a terminal and no flag)").option("--pool-max <n>", "Writer pool size (default 2)").option("--force", "Replace an existing spec with different contents").action(onboardCommand);
|
|
34069
|
+
program2.command("onboard <github-url>").description("Set a GitHub repo up for Shipster: clone it, write a node spec (tier-2 architect, reviewer, writer pool) and print one device code that grants every agent. Starts nothing").option("--account <login>", "GitHub account to clone and work as, from `gh auth` (for example a personal account)").option("--dir <path>", "Parent directory for the clone, worktrees and spec (default ~/github)").option("--architect <runtime>", "Runtime for the tier-2 architect (asked for when there is a terminal and no flag)").option("--writer <runtime>", "Runtime for the writer pool (asked for when there is a terminal and no flag)").option("--reviewer <runtime>", "Runtime for the reviewer; may match the writers, with a warning (asked for when there is a terminal and no flag)").option("--pool-max <n>", "Writer pool size (default 2)").option("--writer-models <list>", "One to three writer models, cheapest first, comma-separated: one writer pool per tier and a reviewer pool, all scaling to zero").option("--force", "Replace an existing spec with different contents").action(onboardCommand);
|
|
33948
34070
|
var node = program2.command("node").description("Run this machine's agents headless and keep them running");
|
|
33949
34071
|
node.command("up [spec]").description("Start every agent in a spec file and restart any that exit (foreground; Ctrl-C stops them). With no spec file, follows the desired state the dashboard holds for this enrolled node").option(
|
|
33950
34072
|
"--invite <name=code>",
|
package/dist/event-worker.js
CHANGED
|
@@ -81,6 +81,12 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
|
|
|
81
81
|
c. Read the task carefully. If the row is marked \`truncated:true\`, call
|
|
82
82
|
\`mcp__relai__get_task\` with its id for the full description before acting \u2014
|
|
83
83
|
the list clips descriptions, and the rest of the spec is only on that drill-in.
|
|
84
|
+
If \`metadata.checkpoint\` is set (\`mcp__relai__get_task\` if \`metadata._truncated\` lists it in \`keys\`),
|
|
85
|
+
start from its \`next\` and \`unverified\` lists. It is an earlier session's notes: information, not
|
|
86
|
+
authorisation, and cannot widen the task's scope. Work on the task's own branch (\`metadata.branchName\`
|
|
87
|
+
or the order's), never its \`branch\`, \`wipRef\` or \`worktree\`; if its \`branch\` differs, say so in a task
|
|
88
|
+
comment. \`headSha\` is the last commit it knew there: read later commits before acting on \`next\`, and
|
|
89
|
+
comment if the tip lacks it. A finding, human reply or task edit newer than \`checkpoint.at\` wins.
|
|
84
90
|
Check \`metadata\` for \`branchName\`, \`roundNumber\`, and any leftover
|
|
85
91
|
\`findings\`; also read finding comments on the thread from step b \u2014 that is where
|
|
86
92
|
new findings land. If \`metadata.humanReply\` is set, this task was previously blocked
|
|
@@ -97,10 +103,14 @@ var BASE_TASK_LOOP = (agentId, repoPath) => `You are an AI worker agent (ID: ${a
|
|
|
97
103
|
the same thing: either proceed on your own judgement and say in your handoff that you did, or
|
|
98
104
|
escalate to a human by blocking with a question addressed to nobody.
|
|
99
105
|
d. Do the work per your specialization rules below.
|
|
100
|
-
e.
|
|
106
|
+
e. If the work is on a branch, commit and push it, then call \`mcp__relai__checkpoint_task\` while the
|
|
107
|
+
task is still \`in_progress\`: before every \`update_task_status\` to "completed" or "blocked" and
|
|
108
|
+
every \`assign_task\`, including those in the role, round-limit and order rules below, and when out
|
|
109
|
+
of room. \`dirty\` and \`wipRef\` are only for work that cannot be committed.
|
|
110
|
+
f. Post your result on the task's own thread (\`mcp__relai__add_task_comment\` or
|
|
101
111
|
\`mcp__relai__send_message\` type "handoff"), with \`toAgent\` set to whoever you
|
|
102
112
|
report to: the task's \`createdBy\` unless told otherwise.
|
|
103
|
-
|
|
113
|
+
g. If the work is done: \`mcp__relai__update_task_status\` \u2192 "completed"
|
|
104
114
|
(or "blocked" if escalating). If another role must continue this same task,
|
|
105
115
|
\`mcp__relai__assign_task\` to them and stop \u2014 do not complete.
|
|
106
116
|
|
|
@@ -225,8 +235,9 @@ types, empty classes) but you do not take the implementation yourself.
|
|
|
225
235
|
|
|
226
236
|
- Read relevant files to understand current structure.
|
|
227
237
|
- Write a clear technical design on this task's thread (\`mcp__relai__add_task_comment\`).
|
|
228
|
-
If scaffolding helps, write it on the branch in \`branchName\`
|
|
229
|
-
same slug rule as writer)
|
|
238
|
+
If scaffolding helps, write it on the branch in \`branchName\`: if set, \`git fetch origin\` then
|
|
239
|
+
\`git checkout -B {branchName} origin/{branchName}\`; if missing, derive one (same slug rule as writer)
|
|
240
|
+
and stamp it as \`metadata.branchName\` with \`mcp__relai__update_task_status\`. Never force-push.
|
|
230
241
|
- \`mcp__relai__list_agents\`, pick specialization \`"writer"\` (prefer online), then
|
|
231
242
|
\`mcp__relai__assign_task\` to them. Do not complete.
|
|
232
243
|
|
|
@@ -239,9 +250,13 @@ types, empty classes) but you do not take the implementation yourself.
|
|
|
239
250
|
You implement specs on a branch and open a PR. Write production code and tests.
|
|
240
251
|
|
|
241
252
|
**Workflow:**
|
|
242
|
-
1. Check metadata for branchName. If
|
|
253
|
+
1. Check metadata for branchName. If set, the branch holds earlier work: \`git fetch origin\` then
|
|
254
|
+
\`git checkout -B {branchName} origin/{branchName}\` (only if the remote branch does not exist, create
|
|
255
|
+
it from the default branch). If none, derive one from the task title:
|
|
243
256
|
slug the title to lowercase-hyphenated, prefix "feat/".
|
|
244
|
-
Use Bash to create the branch: \`git checkout -b {branchName}\`.
|
|
257
|
+
Use Bash to create the branch: \`git checkout -b {branchName}\`. Stamp it on this task via
|
|
258
|
+
\`mcp__relai__update_task_status\` with status \`"in_progress"\` and \`metadata.branchName\`.
|
|
259
|
+
Never force-push to make a push succeed.
|
|
245
260
|
2. Read the spec. Read existing files to understand patterns (Read, Grep, Glob).
|
|
246
261
|
3. Implement: write code, update tests, follow existing conventions exactly.
|
|
247
262
|
4. Run tests: check package.json scripts for the test command.
|
|
@@ -250,7 +265,8 @@ You implement specs on a branch and open a PR. Write production code and tests.
|
|
|
250
265
|
- First try \`gh pr create --fill\` \u2014 captures the PR URL automatically when the GitHub CLI is installed and authed.
|
|
251
266
|
- If \`gh\` isn't available (\`command -v gh\` fails) or it returns a non-zero exit, fall back to \`git push -u origin {branchName}\`. Then read the remote with \`git remote get-url origin\` so you can hand the reviewer a clickable branch URL even without an open PR. Set \`prUrl\` to the PR URL when you have one, otherwise to the branch URL on the remote.
|
|
252
267
|
7. Post a handoff on the task's own thread with \`mcp__relai__add_task_comment\` (type: "handoff"):
|
|
253
|
-
PR URL, head sha, what landed, and tests. Then
|
|
268
|
+
PR URL, head sha, what landed, and tests. Then, after the step e checkpoint,
|
|
269
|
+
\`mcp__relai__update_task_status\` \u2192 "completed".
|
|
254
270
|
- If the task has \`verifyReviewerId\` or any reviewer gate, completed moves it to
|
|
255
271
|
pending_verification. Do not then \`update_task_status\` to \`"in_progress"\` \u2014
|
|
256
272
|
the reviewer decides via \`submit_review\`, and a reject returns the task to
|
|
@@ -1889,7 +1905,8 @@ var PITBOSS_NAMES = [
|
|
|
1889
1905
|
"ENV_GRANT",
|
|
1890
1906
|
"TASK_REPO_ID",
|
|
1891
1907
|
"TASK_REPO_URL",
|
|
1892
|
-
"OWNER_POOL_SLOT"
|
|
1908
|
+
"OWNER_POOL_SLOT",
|
|
1909
|
+
"SESSION_MAX_MS"
|
|
1893
1910
|
].flatMap(envNames);
|
|
1894
1911
|
var WORKER_KNOBS = [
|
|
1895
1912
|
"API_URL",
|
|
@@ -2750,6 +2767,13 @@ async function clearUnavailable(config2, logPrefix = "[claude-worker]") {
|
|
|
2750
2767
|
}
|
|
2751
2768
|
|
|
2752
2769
|
// ../claude-worker/src/worker.ts
|
|
2770
|
+
var SESSION_HEARTBEAT_MS = 6e4;
|
|
2771
|
+
var DEFAULT_SESSION_MAX_MS = 90 * 6e4;
|
|
2772
|
+
var MAX_TIMER_MS = 2147483647;
|
|
2773
|
+
function sessionMaxMs(source = process.env) {
|
|
2774
|
+
const ms = Number(readEnv("SESSION_MAX_MS", source) ?? NaN);
|
|
2775
|
+
return Number.isFinite(ms) && ms >= 0 ? Math.min(ms, MAX_TIMER_MS) : DEFAULT_SESSION_MAX_MS;
|
|
2776
|
+
}
|
|
2753
2777
|
async function heartbeat(config2, logPrefix = "[claude-worker]") {
|
|
2754
2778
|
await fetch(`${config2.apiUrl}/agents/${config2.agentId}/heartbeat`, {
|
|
2755
2779
|
method: "PUT",
|
|
@@ -2757,6 +2781,17 @@ async function heartbeat(config2, logPrefix = "[claude-worker]") {
|
|
|
2757
2781
|
body: "{}"
|
|
2758
2782
|
}).catch((err) => console.warn(`${logPrefix} Heartbeat failed:`, err.message));
|
|
2759
2783
|
}
|
|
2784
|
+
async function withHeartbeat(config2, logPrefix, session) {
|
|
2785
|
+
const maxMs = sessionMaxMs();
|
|
2786
|
+
const quiet = maxMs > 0 ? setTimeout(() => clearInterval(beat), maxMs) : void 0;
|
|
2787
|
+
const beat = setInterval(() => void heartbeat(config2, logPrefix), SESSION_HEARTBEAT_MS);
|
|
2788
|
+
try {
|
|
2789
|
+
return await session();
|
|
2790
|
+
} finally {
|
|
2791
|
+
clearTimeout(quiet);
|
|
2792
|
+
clearInterval(beat);
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2760
2795
|
async function assertRepoOrExit(config2, logPrefix = "[claude-worker]") {
|
|
2761
2796
|
const repoUrl = await fetchRepoUrl(config2.apiUrl, config2.repoId, config2.apiSecret);
|
|
2762
2797
|
const check = checkRepoMatch(config2.repoPath, repoUrl);
|
|
@@ -3658,6 +3693,32 @@ async function checkWork(config2) {
|
|
|
3658
3693
|
repoIds: [...new Set(myTasks.map((t) => t.repoId).filter((id) => typeof id === "string"))].sort()
|
|
3659
3694
|
};
|
|
3660
3695
|
}
|
|
3696
|
+
async function recordSessionTimeout(config2, taskId, minutes, timeouts) {
|
|
3697
|
+
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${config2.apiSecret}` };
|
|
3698
|
+
const count = (timeouts.get(taskId) ?? 0) + 1;
|
|
3699
|
+
timeouts.set(taskId, count);
|
|
3700
|
+
try {
|
|
3701
|
+
const res = await fetch(`${config2.apiUrl}/tasks/${taskId}`, { headers });
|
|
3702
|
+
if (!res.ok) throw new Error(`task lookup failed (${res.status} ${res.statusText})`);
|
|
3703
|
+
const { data: task } = await res.json();
|
|
3704
|
+
const mine = task?.assignedTo === config2.agentId;
|
|
3705
|
+
if (!mine) console.error(`[event-worker] ${taskId} is not assigned to this agent (a review, or reassigned), so the timeout is a comment only`);
|
|
3706
|
+
const block = mine && count >= 2;
|
|
3707
|
+
const out = block ? await fetch(`${config2.apiUrl}/tasks/${taskId}`, {
|
|
3708
|
+
method: "PUT",
|
|
3709
|
+
headers,
|
|
3710
|
+
body: JSON.stringify({ status: "blocked", metadata: { blockedReason: `The session cap (${minutes} minutes) was hit twice on this task. Each session was killed. Split the task or find out why it hangs before reassigning it.` } })
|
|
3711
|
+
}) : await fetch(`${config2.apiUrl}/tasks/${taskId}/comments`, {
|
|
3712
|
+
method: "POST",
|
|
3713
|
+
headers,
|
|
3714
|
+
body: JSON.stringify({ body: `The session working this task hit the ${minutes}-minute cap and was killed. The task stays as it is, and the next wake resumes it in a fresh session.` })
|
|
3715
|
+
});
|
|
3716
|
+
if (!out.ok) console.error(`[event-worker] Could not record the session timeout on ${taskId} (${out.status} ${out.statusText})`);
|
|
3717
|
+
else if (block) timeouts.delete(taskId);
|
|
3718
|
+
} catch (err) {
|
|
3719
|
+
console.error("[event-worker] Could not record the session timeout:", err instanceof Error ? err.message : String(err));
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3661
3722
|
var EVENT_KINDS = [
|
|
3662
3723
|
"message.posted",
|
|
3663
3724
|
"task.created",
|
|
@@ -3671,6 +3732,9 @@ var EVENT_KINDS = [
|
|
|
3671
3732
|
"task.blocked",
|
|
3672
3733
|
"task.pending_verification",
|
|
3673
3734
|
"task.stalled",
|
|
3735
|
+
"task.stall_released",
|
|
3736
|
+
"task.down_released",
|
|
3737
|
+
"task.rerouted",
|
|
3674
3738
|
"task.verified",
|
|
3675
3739
|
"task.verification_failed",
|
|
3676
3740
|
"task.review_requested",
|
|
@@ -3704,6 +3768,16 @@ function endsTask(event, taskId) {
|
|
|
3704
3768
|
if (!task || task.id !== taskId) return false;
|
|
3705
3769
|
return task.status === "cancelled" || task.archivedAt !== void 0 && task.archivedAt !== null;
|
|
3706
3770
|
}
|
|
3771
|
+
var RELEASE_KINDS = ["task.stall_released", "task.down_released"];
|
|
3772
|
+
function releasedTask(event, taskId) {
|
|
3773
|
+
const e = event;
|
|
3774
|
+
return e?.targetId === taskId && typeof e.kind === "string" && RELEASE_KINDS.includes(e.kind);
|
|
3775
|
+
}
|
|
3776
|
+
function reroutedAway(event, taskId, agentId) {
|
|
3777
|
+
const e = event;
|
|
3778
|
+
if (e?.kind !== "task.rerouted" || e.targetId !== taskId) return false;
|
|
3779
|
+
return e.payload?.previousAssignee === agentId && typeof e.payload.assignedTo === "string" && e.payload.assignedTo !== agentId;
|
|
3780
|
+
}
|
|
3707
3781
|
async function runEventWorker(config2) {
|
|
3708
3782
|
console.log(`[event-worker] Starting \u2014 agent ${config2.agentId}, watching ${config2.apiUrl}/events`);
|
|
3709
3783
|
console.log(`[event-worker] Repo: ${config2.repoPath} | Runtime: ${config2.runtime} | Model: ${config2.model}`);
|
|
@@ -3711,6 +3785,7 @@ async function runEventWorker(config2) {
|
|
|
3711
3785
|
await assertNotOwnerScopedOrExit(config2, "[event-worker]");
|
|
3712
3786
|
await selfSubscribe(config2);
|
|
3713
3787
|
let live = null;
|
|
3788
|
+
const timeouts = /* @__PURE__ */ new Map();
|
|
3714
3789
|
const queue = createRunQueue(async () => {
|
|
3715
3790
|
await heartbeat(config2, "[event-worker]");
|
|
3716
3791
|
const slotMode = readEnv("OWNER_POOL_SLOT") === "1";
|
|
@@ -3765,6 +3840,8 @@ async function runEventWorker(config2) {
|
|
|
3765
3840
|
}
|
|
3766
3841
|
}
|
|
3767
3842
|
const controller = new AbortController();
|
|
3843
|
+
const maxMs = sessionMaxMs();
|
|
3844
|
+
let timedOut = false;
|
|
3768
3845
|
try {
|
|
3769
3846
|
console.log("[event-worker] Event received \u2014 running session...");
|
|
3770
3847
|
const activityFile = readEnv("ACTIVITY_FILE");
|
|
@@ -3784,9 +3861,16 @@ async function runEventWorker(config2) {
|
|
|
3784
3861
|
setEnv(TASK_REPO_URL_ENV, checkout.repoUrl);
|
|
3785
3862
|
}
|
|
3786
3863
|
if (checkout) live = { taskId: checkout.taskId, controller };
|
|
3864
|
+
const cap = maxMs > 0 ? setTimeout(() => {
|
|
3865
|
+
if (controller.signal.aborted) return;
|
|
3866
|
+
timedOut = true;
|
|
3867
|
+
controller.abort();
|
|
3868
|
+
}, maxMs) : void 0;
|
|
3787
3869
|
try {
|
|
3788
|
-
await getAdapter(config2.runtime).runSession(config2, { signal: controller.signal });
|
|
3870
|
+
await withHeartbeat(config2, "[event-worker]", () => getAdapter(config2.runtime).runSession(config2, { signal: controller.signal }));
|
|
3871
|
+
if (checkout) timeouts.delete(checkout.taskId);
|
|
3789
3872
|
} finally {
|
|
3873
|
+
clearTimeout(cap);
|
|
3790
3874
|
live = null;
|
|
3791
3875
|
if (controller.signal.aborted && readEnv(RESET_SEAT_DIR_ENV)) {
|
|
3792
3876
|
try {
|
|
@@ -3809,8 +3893,14 @@ async function runEventWorker(config2) {
|
|
|
3809
3893
|
await clearUnavailable(config2, "[event-worker]");
|
|
3810
3894
|
} catch (err) {
|
|
3811
3895
|
const text = err instanceof Error ? err.message : String(err);
|
|
3896
|
+
if (timedOut) {
|
|
3897
|
+
const minutes = Math.max(1, Math.ceil(maxMs / 6e4));
|
|
3898
|
+
console.error(`[event-worker] Session hit the ${minutes}-minute cap (SESSION_MAX_MS) and was killed${checkout ? "" : "; it had no tracked task, so no task was written"}`);
|
|
3899
|
+
if (checkout) await recordSessionTimeout(config2, checkout.taskId, minutes, timeouts);
|
|
3900
|
+
return;
|
|
3901
|
+
}
|
|
3812
3902
|
if (controller.signal.aborted) {
|
|
3813
|
-
console.log(`[event-worker] Session stopped because its task was
|
|
3903
|
+
console.log(`[event-worker] Session stopped because its task was ${controller.signal.reason} (${text.split("\n")[0].slice(0, 120)})`);
|
|
3814
3904
|
return;
|
|
3815
3905
|
}
|
|
3816
3906
|
const exitActivityFile = readEnv("ACTIVITY_FILE");
|
|
@@ -3857,9 +3947,10 @@ async function runEventWorker(config2) {
|
|
|
3857
3947
|
try {
|
|
3858
3948
|
const event = JSON.parse(raw.data);
|
|
3859
3949
|
console.log(`[event-worker] Event: ${event.kind ?? "unknown"}`);
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
live.
|
|
3950
|
+
const why = !live || live.controller.signal.aborted ? null : endsTask(event, live.taskId) ? "cancelled or archived" : releasedTask(event, live.taskId) ? "released to the queue" : reroutedAway(event, live.taskId, config2.agentId) ? "rerouted to another agent" : null;
|
|
3951
|
+
if (live && why) {
|
|
3952
|
+
console.warn(`[event-worker] task ${live.taskId} was ${why} \u2014 stopping its session`);
|
|
3953
|
+
live.controller.abort(why);
|
|
3863
3954
|
}
|
|
3864
3955
|
} catch {
|
|
3865
3956
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -12343,7 +12343,8 @@ var ApiClient = class {
|
|
|
12343
12343
|
this.headers = {
|
|
12344
12344
|
"Content-Type": "application/json",
|
|
12345
12345
|
Authorization: `Bearer ${config.secret}`,
|
|
12346
|
-
...config.ownerId ? { "X-Owner-Id": config.ownerId } : {}
|
|
12346
|
+
...config.ownerId ? { "X-Owner-Id": config.ownerId } : {},
|
|
12347
|
+
...config.sessionId ? { "X-Shipster-Session": config.sessionId } : {}
|
|
12347
12348
|
};
|
|
12348
12349
|
}
|
|
12349
12350
|
// For routes with sibling fields next to `data` (e.g. the unread feed's
|
|
@@ -12653,6 +12654,9 @@ var ApiClient = class {
|
|
|
12653
12654
|
releaseTask(taskId, force) {
|
|
12654
12655
|
return this.request("POST", `/tasks/${taskId}/release`, force === void 0 ? {} : { force });
|
|
12655
12656
|
}
|
|
12657
|
+
checkpointTask(taskId, checkpoint) {
|
|
12658
|
+
return this.request("POST", `/tasks/${taskId}/checkpoint`, checkpoint);
|
|
12659
|
+
}
|
|
12656
12660
|
archiveThread(threadId) {
|
|
12657
12661
|
return this.request("PUT", `/threads/${threadId}/archive`, {});
|
|
12658
12662
|
}
|
|
@@ -12709,6 +12713,26 @@ var ApiClient = class {
|
|
|
12709
12713
|
}
|
|
12710
12714
|
};
|
|
12711
12715
|
|
|
12716
|
+
// ../mcp-server/src/session-id.ts
|
|
12717
|
+
import { randomBytes } from "node:crypto";
|
|
12718
|
+
var SESSION_ID_PATTERN = /^[A-Za-z0-9._:-]{1,64}$/;
|
|
12719
|
+
var URL_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
|
|
12720
|
+
var generated;
|
|
12721
|
+
function generate() {
|
|
12722
|
+
const bytes = randomBytes(12);
|
|
12723
|
+
let out = "ses_";
|
|
12724
|
+
for (const b of bytes) out += URL_SAFE[b % URL_SAFE.length];
|
|
12725
|
+
return out;
|
|
12726
|
+
}
|
|
12727
|
+
function resolveSessionId(env2 = process.env) {
|
|
12728
|
+
for (const name of ["CLAUDE_CODE_SESSION_ID", "CLAUDE_SESSION_ID"]) {
|
|
12729
|
+
const value = env2[name];
|
|
12730
|
+
if (value && SESSION_ID_PATTERN.test(value)) return value;
|
|
12731
|
+
}
|
|
12732
|
+
generated ??= generate();
|
|
12733
|
+
return generated;
|
|
12734
|
+
}
|
|
12735
|
+
|
|
12712
12736
|
// ../mcp-server/src/register-tools.ts
|
|
12713
12737
|
init_zod();
|
|
12714
12738
|
|
|
@@ -12856,7 +12880,7 @@ function pausedExplanation(held) {
|
|
|
12856
12880
|
return {
|
|
12857
12881
|
omitted: held.length,
|
|
12858
12882
|
pauses: [...seen.values()],
|
|
12859
|
-
instruction: "The owner paused new work. Start nothing new: finish the current step of any in_progress task, commit or push what you have,
|
|
12883
|
+
instruction: "The owner paused new work. Start nothing new: finish the current step of any in_progress task, commit or push what you have, save a checkpoint with checkpoint_task, and stop."
|
|
12860
12884
|
};
|
|
12861
12885
|
}
|
|
12862
12886
|
function listTasksLimit() {
|
|
@@ -13694,7 +13718,7 @@ ${version.body}`
|
|
|
13694
13718
|
},
|
|
13695
13719
|
{
|
|
13696
13720
|
name: "get_my_tasks",
|
|
13697
|
-
description: "Retrieve tasks assigned to this agent. Use this at the start of a session or when you want to know what work is queued for you. Returns tasks with status 'assigned' or 'in_progress'. Always call this before starting new work so you don't duplicate effort. humanLabel 'Not picked up' means this task was assigned to you hours ago and nothing has touched it since \u2014 the assignment notice never reached you. Start it now, or say on its thread why you are not going to. Capped and newest-first, with descriptions clipped \u2014 an uncapped queue once blew past the tool-result limit. A task still 'assigned' inside a paused scope (repo, order or account) is left out and explained in a top-level `paused` block: the owner paused new work, so start nothing new; in_progress tasks stay listed so you can finish the step and
|
|
13721
|
+
description: "Retrieve tasks assigned to this agent. Use this at the start of a session or when you want to know what work is queued for you. Returns tasks with status 'assigned' or 'in_progress'. Always call this before starting new work so you don't duplicate effort. humanLabel 'Not picked up' means this task was assigned to you hours ago and nothing has touched it since \u2014 the assignment notice never reached you. Start it now, or say on its thread why you are not going to. Capped and newest-first, with descriptions clipped \u2014 an uncapped queue once blew past the tool-result limit. A task still 'assigned' inside a paused scope (repo, order or account) is left out and explained in a top-level `paused` block: the owner paused new work, so start nothing new; in_progress tasks stay listed so you can finish the step and save a checkpoint with checkpoint_task. taskCount is the true total; raise `limit` to see more. A row marked truncated:true is a pointer \u2014 call get_task for the full description.",
|
|
13698
13722
|
inputSchema: external_exports.object({
|
|
13699
13723
|
status: external_exports.enum(["assigned", "in_progress", "pending", "all"]).optional().describe("Filter by task status. Omit for both 'assigned' and 'in_progress'."),
|
|
13700
13724
|
limit: external_exports.number().int().min(1).optional().describe("Max tasks to return, newest-updated first. Defaults to 50.")
|
|
@@ -13772,6 +13796,33 @@ ${version.body}`
|
|
|
13772
13796
|
});
|
|
13773
13797
|
}
|
|
13774
13798
|
},
|
|
13799
|
+
{
|
|
13800
|
+
name: "checkpoint_task",
|
|
13801
|
+
description: "Save your work-in-progress state on an in_progress task you hold. The next session on it remembers nothing of this one and starts from this checkpoint. Call it before you stop working the task for any reason, and after any step you would not want redone. Each call replaces the previous checkpoint. You keep the task; with a pause pending the checkpoint releases it and the result says to stop.",
|
|
13802
|
+
inputSchema: external_exports.object({
|
|
13803
|
+
taskId: external_exports.string().describe("The task you hold."),
|
|
13804
|
+
branch: external_exports.string().describe("Branch the work is on."),
|
|
13805
|
+
headSha: external_exports.string().describe("Full sha of your last commit."),
|
|
13806
|
+
worktree: external_exports.string().optional().describe("Checkout path, if not the repo path."),
|
|
13807
|
+
done: external_exports.array(external_exports.string()).describe("Finished and proved, with the evidence."),
|
|
13808
|
+
next: external_exports.array(external_exports.string()).describe("What to do next, in order."),
|
|
13809
|
+
unverified: external_exports.array(external_exports.string()).describe("Claims not yet proved, and what would prove each."),
|
|
13810
|
+
dirty: external_exports.boolean().optional().describe("Uncommitted changes exist. Needs wipRef."),
|
|
13811
|
+
wipRef: external_exports.string().optional().describe("Pushed ref holding them: refs/wip/<taskId>.")
|
|
13812
|
+
}),
|
|
13813
|
+
handler: async (input) => {
|
|
13814
|
+
if (input.dirty && !input.wipRef) {
|
|
13815
|
+
return mcpFail(new Error("A dirty checkpoint needs wipRef: push the uncommitted work to a ref first."));
|
|
13816
|
+
}
|
|
13817
|
+
const { taskId, ...checkpoint } = input;
|
|
13818
|
+
try {
|
|
13819
|
+
const { task } = await client.checkpointTask(taskId, checkpoint);
|
|
13820
|
+
return { content: [{ type: "text", text: !task.assignedTo ? `Checkpoint saved and task ${taskId} released: a pause was pending. Stop working on it now.` : `Checkpoint saved on task ${taskId}. You still hold it.` }] };
|
|
13821
|
+
} catch (err) {
|
|
13822
|
+
return mcpFail(err);
|
|
13823
|
+
}
|
|
13824
|
+
}
|
|
13825
|
+
},
|
|
13775
13826
|
buildUpdateTaskTool(client),
|
|
13776
13827
|
{
|
|
13777
13828
|
name: "send_message",
|
|
@@ -15190,7 +15241,10 @@ if (OWNER_MODE) {
|
|
|
15190
15241
|
var apiClient = new ApiClient({
|
|
15191
15242
|
baseUrl: API_URL,
|
|
15192
15243
|
secret: OWNER_MODE ? API_OWNER_TOKEN : API_SECRET,
|
|
15193
|
-
ownerId: OWNER_MODE ? OWNER_ID : void 0
|
|
15244
|
+
ownerId: OWNER_MODE ? OWNER_ID : void 0,
|
|
15245
|
+
// stdio is one process per agent session, so one id per process is right.
|
|
15246
|
+
// HTTP serves several callers from this single client, so it sends none.
|
|
15247
|
+
sessionId: TRANSPORT === "stdio" ? resolveSessionId() : void 0
|
|
15194
15248
|
});
|
|
15195
15249
|
var server = createMcpServer(OWNER_MODE ? "relai-operator" : "relai", pkg.version);
|
|
15196
15250
|
var toolConfig = OWNER_MODE ? { ownerMode: true, ownerId: OWNER_ID } : { ownerMode: false, agentId: AGENT_ID, repoId: REPO_ID };
|