@shipers-dev/multi 0.49.0 → 0.52.0
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 +410 -127
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -33133,25 +33133,146 @@ var init_workspace_mutex = __esm(() => {
|
|
|
33133
33133
|
]);
|
|
33134
33134
|
});
|
|
33135
33135
|
|
|
33136
|
+
// src/_impl/adapter-pidfile.ts
|
|
33137
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
33138
|
+
import { homedir } from "os";
|
|
33139
|
+
import { join as join7 } from "path";
|
|
33140
|
+
import { spawnSync } from "child_process";
|
|
33141
|
+
function ensureDir() {
|
|
33142
|
+
if (!existsSync5(ADAPTERS_DIR))
|
|
33143
|
+
mkdirSync5(ADAPTERS_DIR, { recursive: true });
|
|
33144
|
+
}
|
|
33145
|
+
function pidfilePath(pid) {
|
|
33146
|
+
return join7(ADAPTERS_DIR, `${pid}.json`);
|
|
33147
|
+
}
|
|
33148
|
+
function writeAdapterPidfile(entry) {
|
|
33149
|
+
ensureDir();
|
|
33150
|
+
const full = {
|
|
33151
|
+
...entry,
|
|
33152
|
+
daemon_pid: entry.daemon_pid ?? process.pid,
|
|
33153
|
+
started_at: Date.now()
|
|
33154
|
+
};
|
|
33155
|
+
const path = pidfilePath(entry.pid);
|
|
33156
|
+
try {
|
|
33157
|
+
writeFileSync5(path, JSON.stringify(full) + `
|
|
33158
|
+
`, "utf8");
|
|
33159
|
+
} catch {}
|
|
33160
|
+
return path;
|
|
33161
|
+
}
|
|
33162
|
+
function removeAdapterPidfile(pid) {
|
|
33163
|
+
try {
|
|
33164
|
+
unlinkSync3(pidfilePath(pid));
|
|
33165
|
+
} catch {}
|
|
33166
|
+
}
|
|
33167
|
+
function inspectPid(pid) {
|
|
33168
|
+
try {
|
|
33169
|
+
const r = spawnSync("ps", ["-o", "ppid=,command=", "-p", String(pid)], { encoding: "utf8" });
|
|
33170
|
+
if (r.status !== 0)
|
|
33171
|
+
return { alive: false, ppid: 0, command: "" };
|
|
33172
|
+
const line = (r.stdout || "").trim();
|
|
33173
|
+
if (!line)
|
|
33174
|
+
return { alive: false, ppid: 0, command: "" };
|
|
33175
|
+
const m = line.match(/^\s*(\d+)\s+(.*)$/);
|
|
33176
|
+
if (!m)
|
|
33177
|
+
return { alive: false, ppid: 0, command: "" };
|
|
33178
|
+
return { alive: true, ppid: parseInt(m[1], 10), command: m[2] };
|
|
33179
|
+
} catch {
|
|
33180
|
+
return { alive: false, ppid: 0, command: "" };
|
|
33181
|
+
}
|
|
33182
|
+
}
|
|
33183
|
+
function tryKill(pid, signal) {
|
|
33184
|
+
try {
|
|
33185
|
+
process.kill(pid, signal);
|
|
33186
|
+
return true;
|
|
33187
|
+
} catch {
|
|
33188
|
+
return false;
|
|
33189
|
+
}
|
|
33190
|
+
}
|
|
33191
|
+
function isPidAlive(pid) {
|
|
33192
|
+
try {
|
|
33193
|
+
process.kill(pid, 0);
|
|
33194
|
+
return true;
|
|
33195
|
+
} catch {
|
|
33196
|
+
return false;
|
|
33197
|
+
}
|
|
33198
|
+
}
|
|
33199
|
+
function reapStaleAdapters(log3 = () => {}) {
|
|
33200
|
+
ensureDir();
|
|
33201
|
+
let entries2 = [];
|
|
33202
|
+
try {
|
|
33203
|
+
entries2 = readdirSync2(ADAPTERS_DIR);
|
|
33204
|
+
} catch {
|
|
33205
|
+
return 0;
|
|
33206
|
+
}
|
|
33207
|
+
let reaped = 0;
|
|
33208
|
+
for (const name of entries2) {
|
|
33209
|
+
if (!name.endsWith(".json"))
|
|
33210
|
+
continue;
|
|
33211
|
+
const path = join7(ADAPTERS_DIR, name);
|
|
33212
|
+
let parsed = null;
|
|
33213
|
+
try {
|
|
33214
|
+
parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
33215
|
+
} catch {}
|
|
33216
|
+
if (!parsed || typeof parsed.pid !== "number") {
|
|
33217
|
+
try {
|
|
33218
|
+
unlinkSync3(path);
|
|
33219
|
+
} catch {}
|
|
33220
|
+
continue;
|
|
33221
|
+
}
|
|
33222
|
+
const info = inspectPid(parsed.pid);
|
|
33223
|
+
if (!info.alive) {
|
|
33224
|
+
try {
|
|
33225
|
+
unlinkSync3(path);
|
|
33226
|
+
} catch {}
|
|
33227
|
+
continue;
|
|
33228
|
+
}
|
|
33229
|
+
if (parsed.daemon_pid && isPidAlive(parsed.daemon_pid))
|
|
33230
|
+
continue;
|
|
33231
|
+
if (parsed.bin && !info.command.includes(parsed.bin)) {
|
|
33232
|
+
try {
|
|
33233
|
+
unlinkSync3(path);
|
|
33234
|
+
} catch {}
|
|
33235
|
+
continue;
|
|
33236
|
+
}
|
|
33237
|
+
if (info.ppid !== 1)
|
|
33238
|
+
continue;
|
|
33239
|
+
log3(`[reap] killing orphan ${parsed.kind} pid=${parsed.pid} bin=${parsed.bin} session=${parsed.session_id ?? "?"}`);
|
|
33240
|
+
tryKill(parsed.pid, "SIGTERM");
|
|
33241
|
+
setTimeout(() => {
|
|
33242
|
+
if (isPidAlive(parsed.pid))
|
|
33243
|
+
tryKill(parsed.pid, "SIGKILL");
|
|
33244
|
+
}, 1500).unref?.();
|
|
33245
|
+
try {
|
|
33246
|
+
unlinkSync3(path);
|
|
33247
|
+
} catch {}
|
|
33248
|
+
reaped++;
|
|
33249
|
+
}
|
|
33250
|
+
return reaped;
|
|
33251
|
+
}
|
|
33252
|
+
var ADAPTERS_DIR;
|
|
33253
|
+
var init_adapter_pidfile = __esm(() => {
|
|
33254
|
+
ADAPTERS_DIR = join7(homedir(), ".multi", "adapters");
|
|
33255
|
+
});
|
|
33256
|
+
|
|
33136
33257
|
// src/_impl/acp-runner.ts
|
|
33137
|
-
import { readFileSync as
|
|
33138
|
-
import { dirname as dirname6, join as
|
|
33258
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as existsSync6 } from "fs";
|
|
33259
|
+
import { dirname as dirname6, join as join8 } from "path";
|
|
33139
33260
|
function ensureBypassPermissions(cwd) {
|
|
33140
33261
|
try {
|
|
33141
|
-
const dir =
|
|
33142
|
-
if (!
|
|
33143
|
-
|
|
33144
|
-
const path =
|
|
33262
|
+
const dir = join8(cwd, ".claude");
|
|
33263
|
+
if (!existsSync6(dir))
|
|
33264
|
+
mkdirSync6(dir, { recursive: true });
|
|
33265
|
+
const path = join8(dir, "settings.local.json");
|
|
33145
33266
|
let json2 = {};
|
|
33146
|
-
if (
|
|
33267
|
+
if (existsSync6(path)) {
|
|
33147
33268
|
try {
|
|
33148
|
-
json2 = JSON.parse(
|
|
33269
|
+
json2 = JSON.parse(readFileSync6(path, "utf8"));
|
|
33149
33270
|
} catch {
|
|
33150
33271
|
json2 = {};
|
|
33151
33272
|
}
|
|
33152
33273
|
}
|
|
33153
33274
|
json2.permissions = { ...json2.permissions || {}, defaultMode: "bypassPermissions" };
|
|
33154
|
-
|
|
33275
|
+
writeFileSync6(path, JSON.stringify(json2, null, 2) + `
|
|
33155
33276
|
`, "utf8");
|
|
33156
33277
|
} catch {}
|
|
33157
33278
|
}
|
|
@@ -33195,6 +33316,16 @@ async function runAcp(opts) {
|
|
|
33195
33316
|
try {
|
|
33196
33317
|
opts.onSpawn?.(child);
|
|
33197
33318
|
} catch {}
|
|
33319
|
+
const pidfileBin = argv[0]?.split("/").pop() || "claude-code-acp";
|
|
33320
|
+
if (typeof child.pid === "number") {
|
|
33321
|
+
writeAdapterPidfile({
|
|
33322
|
+
pid: child.pid,
|
|
33323
|
+
bin: pidfileBin,
|
|
33324
|
+
kind: "acp",
|
|
33325
|
+
session_id: opts.sessionId ?? null,
|
|
33326
|
+
issue_id: opts.issueId ?? null
|
|
33327
|
+
});
|
|
33328
|
+
}
|
|
33198
33329
|
const output = new WritableStream({
|
|
33199
33330
|
write(chunk2) {
|
|
33200
33331
|
child.stdin.write(chunk2);
|
|
@@ -33224,7 +33355,7 @@ async function runAcp(opts) {
|
|
|
33224
33355
|
},
|
|
33225
33356
|
async readTextFile(params) {
|
|
33226
33357
|
try {
|
|
33227
|
-
const content =
|
|
33358
|
+
const content = readFileSync6(params.path, "utf8");
|
|
33228
33359
|
const sliced = typeof params.line === "number" && typeof params.limit === "number" ? content.split(`
|
|
33229
33360
|
`).slice(params.line, params.line + params.limit).join(`
|
|
33230
33361
|
`) : content;
|
|
@@ -33236,9 +33367,9 @@ async function runAcp(opts) {
|
|
|
33236
33367
|
async writeTextFile(params) {
|
|
33237
33368
|
try {
|
|
33238
33369
|
const dir = dirname6(params.path);
|
|
33239
|
-
if (!
|
|
33240
|
-
|
|
33241
|
-
|
|
33370
|
+
if (!existsSync6(dir))
|
|
33371
|
+
mkdirSync6(dir, { recursive: true });
|
|
33372
|
+
writeFileSync6(params.path, params.content, "utf8");
|
|
33242
33373
|
return {};
|
|
33243
33374
|
} catch (e) {
|
|
33244
33375
|
throw new Error(`writeTextFile failed: ${fmtErr(e)}`);
|
|
@@ -33327,6 +33458,8 @@ async function runAcp(opts) {
|
|
|
33327
33458
|
try {
|
|
33328
33459
|
child.kill();
|
|
33329
33460
|
} catch {}
|
|
33461
|
+
if (typeof child.pid === "number")
|
|
33462
|
+
removeAdapterPidfile(child.pid);
|
|
33330
33463
|
}
|
|
33331
33464
|
async function handleSessionUpdate(params, o) {
|
|
33332
33465
|
const u = params.update;
|
|
@@ -33479,11 +33612,12 @@ var init_acp_runner = __esm(() => {
|
|
|
33479
33612
|
init_acp();
|
|
33480
33613
|
init_client();
|
|
33481
33614
|
init_workspace_mutex();
|
|
33615
|
+
init_adapter_pidfile();
|
|
33482
33616
|
});
|
|
33483
33617
|
|
|
33484
33618
|
// src/_impl/acpx-runner.ts
|
|
33485
33619
|
import { appendFileSync as appendFileSync3 } from "fs";
|
|
33486
|
-
import { join as
|
|
33620
|
+
import { join as join9 } from "path";
|
|
33487
33621
|
function dlog(msg) {
|
|
33488
33622
|
try {
|
|
33489
33623
|
appendFileSync3(LOG_PATH2, `[${new Date().toISOString()}] ${msg}
|
|
@@ -33502,6 +33636,9 @@ async function spawnAcpxPrompt(agentType, cwd, sessionName, prompt, collectAssis
|
|
|
33502
33636
|
args2.push(prompt);
|
|
33503
33637
|
dlog(`[acpx] prompt: ${args2.slice(0, 10).join(" ")} ... (prompt len=${prompt.length})`);
|
|
33504
33638
|
const proc = Bun.spawn(args2, { stdout: "pipe", stderr: "pipe", stdin: "ignore" });
|
|
33639
|
+
if (typeof proc.pid === "number") {
|
|
33640
|
+
writeAdapterPidfile({ pid: proc.pid, bin: "acpx", kind: "acpx", session_id: sessionName ?? null });
|
|
33641
|
+
}
|
|
33505
33642
|
let stopReason = "end_turn";
|
|
33506
33643
|
(async () => {
|
|
33507
33644
|
try {
|
|
@@ -33570,6 +33707,8 @@ async function spawnAcpxPrompt(agentType, cwd, sessionName, prompt, collectAssis
|
|
|
33570
33707
|
}
|
|
33571
33708
|
}
|
|
33572
33709
|
const code = await proc.exited;
|
|
33710
|
+
if (typeof proc.pid === "number")
|
|
33711
|
+
removeAdapterPidfile(proc.pid);
|
|
33573
33712
|
if (code !== 0 && code !== 130) {
|
|
33574
33713
|
if (!collectAssistantText)
|
|
33575
33714
|
await forward({ event_type: "error", payload: { message: `acpx exited with code ${code}` } });
|
|
@@ -33605,6 +33744,9 @@ async function runAcpx(opts) {
|
|
|
33605
33744
|
try {
|
|
33606
33745
|
opts.onSpawn?.(proc);
|
|
33607
33746
|
} catch {}
|
|
33747
|
+
if (typeof proc.pid === "number") {
|
|
33748
|
+
writeAdapterPidfile({ pid: proc.pid, bin: "acpx", kind: "acpx", session_id: opts.sessionName ?? null });
|
|
33749
|
+
}
|
|
33608
33750
|
let stopReason = "end_turn";
|
|
33609
33751
|
(async () => {
|
|
33610
33752
|
try {
|
|
@@ -33661,6 +33803,8 @@ async function runAcpx(opts) {
|
|
|
33661
33803
|
await opts.onEvent(ev);
|
|
33662
33804
|
}
|
|
33663
33805
|
const code = await proc.exited;
|
|
33806
|
+
if (typeof proc.pid === "number")
|
|
33807
|
+
removeAdapterPidfile(proc.pid);
|
|
33664
33808
|
if (code !== 0 && code !== 130) {
|
|
33665
33809
|
await opts.onEvent({ event_type: "error", payload: { message: `acpx exited with code ${code}` } });
|
|
33666
33810
|
}
|
|
@@ -33776,8 +33920,9 @@ function extractText2(content) {
|
|
|
33776
33920
|
}
|
|
33777
33921
|
var HOME3, LOG_PATH2;
|
|
33778
33922
|
var init_acpx_runner = __esm(() => {
|
|
33923
|
+
init_adapter_pidfile();
|
|
33779
33924
|
HOME3 = process.env.HOME || process.env.USERPROFILE || ".";
|
|
33780
|
-
LOG_PATH2 =
|
|
33925
|
+
LOG_PATH2 = join9(HOME3, ".multi", "logs", "agent.log");
|
|
33781
33926
|
});
|
|
33782
33927
|
|
|
33783
33928
|
// ../../node_modules/zod/index.js
|
|
@@ -34003,12 +34148,12 @@ function parsePlanBlocks(text) {
|
|
|
34003
34148
|
}
|
|
34004
34149
|
return { actions, errors: errors3 };
|
|
34005
34150
|
}
|
|
34006
|
-
var PLAN_SCHEMA_VERSION =
|
|
34151
|
+
var PLAN_SCHEMA_VERSION = 7, Priority, AssigneeType, IssueStatus, SessionRole, SkillFile, EvalPolicy, PlanActionSchema, PlanEnvelopeSchema, UiBlockSchema, UI_FENCE_RE, FENCE_RE;
|
|
34007
34152
|
var init_plans = __esm(() => {
|
|
34008
34153
|
init_zod();
|
|
34009
34154
|
Priority = exports_external.enum(["low", "medium", "high"]);
|
|
34010
34155
|
AssigneeType = exports_external.enum(["human", "agent"]);
|
|
34011
|
-
IssueStatus = exports_external.enum(["todo", "in_progress", "done", "failed", "stopped", "cancelled"]);
|
|
34156
|
+
IssueStatus = exports_external.enum(["todo", "in_progress", "blocked", "done", "archived", "failed", "stopped", "cancelled"]);
|
|
34012
34157
|
SessionRole = exports_external.enum(["implementer", "reviewer", "test-fixer"]);
|
|
34013
34158
|
SkillFile = exports_external.object({ path: exports_external.string().min(1), content: exports_external.string() });
|
|
34014
34159
|
EvalPolicy = exports_external.object({
|
|
@@ -34123,6 +34268,20 @@ var init_plans = __esm(() => {
|
|
|
34123
34268
|
score: exports_external.number().min(0).max(1),
|
|
34124
34269
|
feedback: exports_external.string().min(1).max(8000),
|
|
34125
34270
|
scores: exports_external.record(exports_external.string(), exports_external.number().min(0).max(1)).optional()
|
|
34271
|
+
}),
|
|
34272
|
+
exports_external.object({
|
|
34273
|
+
type: exports_external.literal("memory.search"),
|
|
34274
|
+
project_id: exports_external.string().optional(),
|
|
34275
|
+
query: exports_external.string().min(1).max(1000),
|
|
34276
|
+
limit: exports_external.number().int().min(1).max(50).optional()
|
|
34277
|
+
}),
|
|
34278
|
+
exports_external.object({
|
|
34279
|
+
type: exports_external.literal("memory.write"),
|
|
34280
|
+
project_id: exports_external.string().optional(),
|
|
34281
|
+
text: exports_external.string().min(1).max(20000),
|
|
34282
|
+
summary: exports_external.string().max(2000).optional(),
|
|
34283
|
+
kind: exports_external.string().min(1).max(64).optional(),
|
|
34284
|
+
source_id: exports_external.string().min(1).max(256).optional()
|
|
34126
34285
|
})
|
|
34127
34286
|
]);
|
|
34128
34287
|
PlanEnvelopeSchema = exports_external.object({
|
|
@@ -34220,8 +34379,8 @@ var init_lib = __esm(() => {
|
|
|
34220
34379
|
|
|
34221
34380
|
// src/_impl/git-enforce.ts
|
|
34222
34381
|
import { spawn as spawn2 } from "node:child_process";
|
|
34223
|
-
import { existsSync as
|
|
34224
|
-
import { join as
|
|
34382
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
34383
|
+
import { join as join10 } from "node:path";
|
|
34225
34384
|
function run4(cwd, cmd, args2) {
|
|
34226
34385
|
return new Promise((resolve2) => {
|
|
34227
34386
|
const p = spawn2(cmd, args2, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -34241,7 +34400,7 @@ function normalizeKey2(issueKey) {
|
|
|
34241
34400
|
return issueKey.toLowerCase().replace(/[^a-z0-9\-_\/]/g, "-");
|
|
34242
34401
|
}
|
|
34243
34402
|
function worktreePath(baseWorkingDir, issueKey) {
|
|
34244
|
-
return
|
|
34403
|
+
return join10(baseWorkingDir, ".multi", "worktrees", normalizeKey2(issueKey));
|
|
34245
34404
|
}
|
|
34246
34405
|
function branchFor(issueKey) {
|
|
34247
34406
|
return `multi/${normalizeKey2(issueKey)}`;
|
|
@@ -34260,7 +34419,7 @@ function enforceCommitAndPush(baseWorkingDir, issueKey, mode = "enforce") {
|
|
|
34260
34419
|
};
|
|
34261
34420
|
if (mode === "off")
|
|
34262
34421
|
return result;
|
|
34263
|
-
if (!
|
|
34422
|
+
if (!existsSync10(wt))
|
|
34264
34423
|
return result;
|
|
34265
34424
|
if (!(yield* isGitRepo2(wt)))
|
|
34266
34425
|
return result;
|
|
@@ -34348,7 +34507,7 @@ var git = (cwd, args2, op) => exports_Effect.tryPromise({
|
|
|
34348
34507
|
try: () => run4(cwd, "git", args2),
|
|
34349
34508
|
catch: (cause3) => new GitError({ op, message: `git ${args2.join(" ")} threw`, cause: cause3 })
|
|
34350
34509
|
}), isGitRepo2 = (dir) => exports_Effect.gen(function* () {
|
|
34351
|
-
if (!
|
|
34510
|
+
if (!existsSync10(dir))
|
|
34352
34511
|
return false;
|
|
34353
34512
|
const r = yield* git(dir, ["rev-parse", "--is-inside-work-tree"], "rev-parse");
|
|
34354
34513
|
return r.code === 0 && r.stdout === "true";
|
|
@@ -34360,14 +34519,14 @@ var init_git_enforce = __esm(() => {
|
|
|
34360
34519
|
|
|
34361
34520
|
// src/_impl/outbox.ts
|
|
34362
34521
|
import { Database as Database2 } from "bun:sqlite";
|
|
34363
|
-
import { existsSync as
|
|
34364
|
-
import { homedir } from "os";
|
|
34365
|
-
import { join as
|
|
34522
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8 } from "fs";
|
|
34523
|
+
import { homedir as homedir2 } from "os";
|
|
34524
|
+
import { join as join11 } from "path";
|
|
34366
34525
|
function ensureDb() {
|
|
34367
34526
|
if (db)
|
|
34368
34527
|
return db;
|
|
34369
|
-
if (!
|
|
34370
|
-
|
|
34528
|
+
if (!existsSync11(MULTI_DIR3))
|
|
34529
|
+
mkdirSync8(MULTI_DIR3, { recursive: true });
|
|
34371
34530
|
db = new Database2(TASKS_DB_PATH2);
|
|
34372
34531
|
db.exec(`
|
|
34373
34532
|
CREATE TABLE IF NOT EXISTS stream_outbox (
|
|
@@ -34500,17 +34659,17 @@ function startOutboxFlusher(apiUrl, wsId) {
|
|
|
34500
34659
|
var MULTI_DIR3, TASKS_DB_PATH2, BATCH_SIZE = 100, FLUSH_INTERVAL_MS = 2000, MAX_BACKOFF_MS = 30000, db = null;
|
|
34501
34660
|
var init_outbox = __esm(() => {
|
|
34502
34661
|
init_client();
|
|
34503
|
-
MULTI_DIR3 =
|
|
34504
|
-
TASKS_DB_PATH2 =
|
|
34662
|
+
MULTI_DIR3 = join11(homedir2(), ".multi");
|
|
34663
|
+
TASKS_DB_PATH2 = join11(MULTI_DIR3, "tasks.db");
|
|
34505
34664
|
});
|
|
34506
34665
|
|
|
34507
34666
|
// src/_impl/run-task.ts
|
|
34508
|
-
import { mkdirSync as
|
|
34509
|
-
import { join as
|
|
34667
|
+
import { mkdirSync as mkdirSync9, existsSync as existsSync12, writeFileSync as writeFileSync7, readFileSync as readFileSync10, appendFileSync as appendFileSync4, unlinkSync as unlinkSync6, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
34668
|
+
import { join as join12, dirname as dirname9 } from "path";
|
|
34510
34669
|
function ensureDirs() {
|
|
34511
|
-
for (const d of [MULTI_DIR4,
|
|
34512
|
-
if (!
|
|
34513
|
-
|
|
34670
|
+
for (const d of [MULTI_DIR4, join12(MULTI_DIR4, "logs"), SKILLS_DIR2]) {
|
|
34671
|
+
if (!existsSync12(d))
|
|
34672
|
+
mkdirSync9(d, { recursive: true });
|
|
34514
34673
|
}
|
|
34515
34674
|
}
|
|
34516
34675
|
function log3(msg) {
|
|
@@ -34589,7 +34748,7 @@ async function writeTaskMemory(apiUrl, task, text) {
|
|
|
34589
34748
|
async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
|
|
34590
34749
|
const issueId = task.issue_id;
|
|
34591
34750
|
const isFollowup = !!task.followup;
|
|
34592
|
-
const baseWorkingDir = task.working_dir &&
|
|
34751
|
+
const baseWorkingDir = task.working_dir && existsSync12(task.working_dir) ? task.working_dir : undefined;
|
|
34593
34752
|
const tenantWsId = task.tenant_workspace_id ?? null;
|
|
34594
34753
|
const projectId = task.project_id ?? null;
|
|
34595
34754
|
const ISSUE_BASE = tenantWsId && projectId ? `${apiUrl}/api/workspaces/${tenantWsId}/projects/${projectId}/issues/${issueId}` : null;
|
|
@@ -34656,13 +34815,13 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
|
|
|
34656
34815
|
await postStream(apiUrl, issueId, "progress", { message: `Device ${deviceId} picked up ${isFollowup ? "follow-up" : "task"}` });
|
|
34657
34816
|
let attachmentRefs = [];
|
|
34658
34817
|
if (task.from_comment_id && task.tenant_workspace_id && task.project_id) {
|
|
34659
|
-
const baseDir = workingDir ||
|
|
34660
|
-
const inDir =
|
|
34818
|
+
const baseDir = workingDir || join12(MULTI_DIR4, "tmp", issueId);
|
|
34819
|
+
const inDir = join12(baseDir, ".multi-in", task.from_comment_id);
|
|
34661
34820
|
attachmentRefs = await downloadCommentAttachments(apiUrl, task.tenant_workspace_id, task.project_id, issueId, task.from_comment_id, inDir);
|
|
34662
34821
|
if (attachmentRefs.length)
|
|
34663
34822
|
log3(` fetched ${attachmentRefs.length} attachment(s) → ${inDir}`);
|
|
34664
34823
|
}
|
|
34665
|
-
const outDir =
|
|
34824
|
+
const outDir = join12(workingDir || join12(MULTI_DIR4, "tmp", issueId), ".multi-out");
|
|
34666
34825
|
let liveCommentId;
|
|
34667
34826
|
let liveBody = "";
|
|
34668
34827
|
let hadError = false;
|
|
@@ -35172,7 +35331,12 @@ async function buildPlanningPreamble(apiUrl, task, _wsId) {
|
|
|
35172
35331
|
if (depth >= PLANNING_DEPTH_LIMIT) {
|
|
35173
35332
|
return `# Planning (sub-task context)
|
|
35174
35333
|
|
|
35175
|
-
You are acting on a sub-issue spawned by another agent. You MAY emit a \`multi-plan\` block to update your own issue's status (e.g. mark done/failed) but CANNOT create child issues or delegate further.
|
|
35334
|
+
You are acting on a sub-issue spawned by another agent. You MAY emit a \`multi-plan\` block to update your own issue's status (e.g. mark done/blocked/failed) but CANNOT create child issues or delegate further.
|
|
35335
|
+
|
|
35336
|
+
Status guidance:
|
|
35337
|
+
- \`done\` — work is finished; no further human or agent action needed.
|
|
35338
|
+
- \`blocked\` — you need a human decision before continuing (confirmation, choice between approaches, missing info). Do NOT mark \`done\` when waiting on review.
|
|
35339
|
+
- \`failed\` — work could not be completed; explain in your reply.
|
|
35176
35340
|
|
|
35177
35341
|
\`\`\`multi-plan
|
|
35178
35342
|
{"actions":[{"type":"update","id":"<this issue id>","status":"done"}]}
|
|
@@ -35216,13 +35380,19 @@ Issue actions:
|
|
|
35216
35380
|
{"type":"issue.delete","id":"<issue id or key>"},
|
|
35217
35381
|
{"type":"issue.delete_where","status":"todo"},
|
|
35218
35382
|
{"type":"issue.list","status":"todo","assignee_id":"<agent id>","limit":20},
|
|
35219
|
-
{"type":"issue.search","query":"flaky tests","limit":10}
|
|
35383
|
+
{"type":"issue.search","query":"flaky tests","limit":10},
|
|
35384
|
+
{"type":"memory.search","query":"how does the deploy pipeline work","limit":10},
|
|
35385
|
+
{"type":"memory.write","text":"Long-form note worth remembering across sessions...","summary":"short index hint","kind":"agent_note"}
|
|
35220
35386
|
]}
|
|
35221
35387
|
\`\`\`
|
|
35222
35388
|
|
|
35389
|
+
Status values: \`todo\` | \`in_progress\` | \`blocked\` | \`done\` | \`archived\` | \`failed\`. **Use \`blocked\` (NOT \`done\`) when you are pausing to wait on a human decision** — confirmation, a choice between approaches, or missing context. Marking such an issue \`done\` is wrong: the work isn't finished, you're waiting on review. Use \`archived\` to hide a completed/abandoned issue from default views.
|
|
35390
|
+
|
|
35223
35391
|
Prefer the bulk \`issue.delete_where\` over \`issue.list\` + per-issue \`issue.delete\` when the user's intent matches a filter ("delete all todo issues", "remove anything assigned to agent X"). It runs in one turn instead of two.
|
|
35224
35392
|
|
|
35225
|
-
Read actions (\`issue.list\`, \`issue.search\`) return the matched rows in the action summary comment. Use them to look up issue ids/keys before \`update\` / \`delegate\` / \`issue.delete\` ONLY when the per-row filter isn't enough (e.g. you need to inspect titles before acting).
|
|
35393
|
+
Read actions (\`issue.list\`, \`issue.search\`, \`memory.search\`) return the matched rows in the action summary comment. Use them to look up issue ids/keys before \`update\` / \`delegate\` / \`issue.delete\` ONLY when the per-row filter isn't enough (e.g. you need to inspect titles before acting).
|
|
35394
|
+
|
|
35395
|
+
Memory actions are project-scoped persistent storage (FTS5 + vector). Use \`memory.search\` to recall facts learned in past sessions; the hits land in the next-turn context like \`issue.search\`. Use \`memory.write\` sparingly to record durable notes (decisions, gotchas, references) that will be useful to future agents on this project — not transient task state. \`kind\` defaults to \`agent_note\`; use a stable kind string (e.g. \`decision\`, \`gotcha\`) if you want to filter later.
|
|
35226
35396
|
|
|
35227
35397
|
Agent + skill self-service (use sparingly — only when you genuinely need a new capability that isn't covered by an existing agent or skill):
|
|
35228
35398
|
|
|
@@ -35272,7 +35442,7 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx, parseErrors
|
|
|
35272
35442
|
results.push({ type: "note", status: "note", message: `${blocked3} non-update action(s) blocked (planning depth limit ${PLANNING_DEPTH_LIMIT})` });
|
|
35273
35443
|
}
|
|
35274
35444
|
}
|
|
35275
|
-
const SUBCAPS = { "agent.create": 2, "skill.create": 3, "skill.attach": 5, "skill.detach": 5, "agent.update": 5, "session.create": 3 };
|
|
35445
|
+
const SUBCAPS = { "agent.create": 2, "skill.create": 3, "skill.attach": 5, "skill.detach": 5, "agent.update": 5, "session.create": 3, "memory.search": 5, "memory.write": 5 };
|
|
35276
35446
|
const counts = {};
|
|
35277
35447
|
actions = actions.filter((a) => {
|
|
35278
35448
|
const cap = SUBCAPS[a.type];
|
|
@@ -35338,7 +35508,7 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx, parseErrors
|
|
|
35338
35508
|
}
|
|
35339
35509
|
lines.push(`- [ok] updated ${res.data.key}`);
|
|
35340
35510
|
results.push({ type: "update", status: "ok", issue_id: res.data.id, key: res.data.key, status_to: a.status ?? null, title_to: a.title ?? null });
|
|
35341
|
-
if ((a.status === "done" || a.status === "cancelled") && parentTask.working_dir &&
|
|
35511
|
+
if ((a.status === "done" || a.status === "cancelled") && parentTask.working_dir && existsSync12(parentTask.working_dir)) {
|
|
35342
35512
|
const targetKey = res.data?.key;
|
|
35343
35513
|
if (targetKey) {
|
|
35344
35514
|
const targetIssueId = res.data?.id || a.id;
|
|
@@ -35454,6 +35624,57 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx, parseErrors
|
|
|
35454
35624
|
lines.push(` - **${r.key}** [${r.status}] ${r.title}`);
|
|
35455
35625
|
}
|
|
35456
35626
|
results.push({ type: "issue.search", status: "ok", count: rows.length, query: a.query });
|
|
35627
|
+
} else if (a.type === "memory.search") {
|
|
35628
|
+
const projectId = a.project_id || parentProjectId;
|
|
35629
|
+
if (!projectId) {
|
|
35630
|
+
lines.push(`- [err] memory.search "${a.query}": no project_id`);
|
|
35631
|
+
results.push({ type: "memory.search", status: "error", error: "no project_id", label: a.query });
|
|
35632
|
+
continue;
|
|
35633
|
+
}
|
|
35634
|
+
const limit = a.limit ?? 10;
|
|
35635
|
+
const qs = new URLSearchParams({ project_id: projectId, q: a.query, limit: String(limit) });
|
|
35636
|
+
const res = await apiClient.get(`${apiUrl}/api/memory/search?${qs.toString()}`);
|
|
35637
|
+
if (!res.success) {
|
|
35638
|
+
lines.push(`- [err] memory.search "${a.query}": ${res.error || res.status}`);
|
|
35639
|
+
results.push({ type: "memory.search", status: "error", error: String(res.error || res.status), label: a.query });
|
|
35640
|
+
continue;
|
|
35641
|
+
}
|
|
35642
|
+
const rows = Array.isArray(res.data?.rows) ? res.data.rows : [];
|
|
35643
|
+
if (!rows.length) {
|
|
35644
|
+
lines.push(`- [ok] memory.search "${a.query}": 0 hits`);
|
|
35645
|
+
results.push({ type: "memory.search", status: "ok", count: 0, query: a.query });
|
|
35646
|
+
continue;
|
|
35647
|
+
}
|
|
35648
|
+
lines.push(`- [ok] memory.search "${a.query}": ${rows.length} hit(s)`);
|
|
35649
|
+
for (const r of rows) {
|
|
35650
|
+
const summary5 = (r.summary || r.text || "").replace(/\s+/g, " ").slice(0, 200);
|
|
35651
|
+
lines.push(` - [${r.source_kind || "memory"}] ${summary5}`);
|
|
35652
|
+
}
|
|
35653
|
+
results.push({ type: "memory.search", status: "ok", count: rows.length, query: a.query });
|
|
35654
|
+
} else if (a.type === "memory.write") {
|
|
35655
|
+
const projectId = a.project_id || parentProjectId;
|
|
35656
|
+
if (!projectId) {
|
|
35657
|
+
lines.push(`- [err] memory.write: no project_id`);
|
|
35658
|
+
results.push({ type: "memory.write", status: "error", error: "no project_id" });
|
|
35659
|
+
continue;
|
|
35660
|
+
}
|
|
35661
|
+
const body = {
|
|
35662
|
+
project_id: projectId,
|
|
35663
|
+
source_kind: a.kind || "agent_note",
|
|
35664
|
+
source_id: a.source_id || `${parentTask.agent_id || "agent"}:${parentTask.issue_id || ""}:${Date.now()}`,
|
|
35665
|
+
agent_id: parentTask.agent_id || null,
|
|
35666
|
+
text: a.text,
|
|
35667
|
+
summary: a.summary || null
|
|
35668
|
+
};
|
|
35669
|
+
const res = await apiClient.post(`${apiUrl}/api/memory/write`, body);
|
|
35670
|
+
if (!res.success) {
|
|
35671
|
+
lines.push(`- [err] memory.write: ${res.error || res.status}`);
|
|
35672
|
+
results.push({ type: "memory.write", status: "error", error: String(res.error || res.status) });
|
|
35673
|
+
continue;
|
|
35674
|
+
}
|
|
35675
|
+
const row = res.data?.row || {};
|
|
35676
|
+
lines.push(`- [ok] memory.write -> ${row.id || "(no id)"}${row.embedding_id ? " (embedded)" : ""}`);
|
|
35677
|
+
results.push({ type: "memory.write", status: "ok", memory_id: row.id, embedded: !!row.embedding_id });
|
|
35457
35678
|
} else if (a.type === "agent.create") {
|
|
35458
35679
|
if (!parentWsId) {
|
|
35459
35680
|
lines.push(`- [err] agent.create "${a.name}": no tenant workspace id`);
|
|
@@ -35619,26 +35840,26 @@ function statusIcon(status3) {
|
|
|
35619
35840
|
}
|
|
35620
35841
|
}
|
|
35621
35842
|
async function resolveAcpAdapter(agentType, detectedPath) {
|
|
35622
|
-
if (agentType === "pi" && detectedPath &&
|
|
35843
|
+
if (agentType === "pi" && detectedPath && existsSync12(detectedPath)) {
|
|
35623
35844
|
return [detectedPath, "--mode", "rpc"];
|
|
35624
35845
|
}
|
|
35625
35846
|
const override = process.env.MULTI_ACP_ADAPTER?.trim();
|
|
35626
35847
|
const adapterNames = override ? [override] : ["claude-code-acp", "claude-agent-acp"];
|
|
35627
35848
|
for (const name of adapterNames) {
|
|
35628
|
-
if (name.startsWith("/") &&
|
|
35849
|
+
if (name.startsWith("/") && existsSync12(name))
|
|
35629
35850
|
return [name];
|
|
35630
35851
|
try {
|
|
35631
35852
|
const here = new URL(import.meta.url).pathname;
|
|
35632
35853
|
let dir = here;
|
|
35633
35854
|
for (let i = 0;i < 8; i++) {
|
|
35634
35855
|
dir = dirname9(dir);
|
|
35635
|
-
const bin =
|
|
35636
|
-
if (
|
|
35856
|
+
const bin = join12(dir, "node_modules", ".bin", name);
|
|
35857
|
+
if (existsSync12(bin))
|
|
35637
35858
|
return [bin];
|
|
35638
35859
|
}
|
|
35639
35860
|
} catch {}
|
|
35640
|
-
const global =
|
|
35641
|
-
if (
|
|
35861
|
+
const global = join12(HOME4, ".bun", "install", "global", "node_modules", ".bin", name);
|
|
35862
|
+
if (existsSync12(global))
|
|
35642
35863
|
return [global];
|
|
35643
35864
|
}
|
|
35644
35865
|
return null;
|
|
@@ -35647,7 +35868,7 @@ async function postStream(_apiUrl, issueId, event_type, payload) {
|
|
|
35647
35868
|
try {
|
|
35648
35869
|
ensureDirs();
|
|
35649
35870
|
const date6 = new Date().toISOString().slice(0, 10);
|
|
35650
|
-
const path =
|
|
35871
|
+
const path = join12(MULTI_DIR4, "logs", `events-${date6}.ndjson`);
|
|
35651
35872
|
appendFileSync4(path, JSON.stringify({ ts: Date.now(), issue_id: issueId, event_type, payload }) + `
|
|
35652
35873
|
`);
|
|
35653
35874
|
} catch {}
|
|
@@ -35666,7 +35887,7 @@ async function downloadCommentAttachments(apiUrl, wsId, projectId, issueId, comm
|
|
|
35666
35887
|
const items = list.data?.results || list.data || [];
|
|
35667
35888
|
if (!Array.isArray(items) || items.length === 0)
|
|
35668
35889
|
return [];
|
|
35669
|
-
|
|
35890
|
+
mkdirSync9(destDir, { recursive: true });
|
|
35670
35891
|
const token = authTokenHeader();
|
|
35671
35892
|
const out = [];
|
|
35672
35893
|
for (const it of items) {
|
|
@@ -35675,8 +35896,8 @@ async function downloadCommentAttachments(apiUrl, wsId, projectId, issueId, comm
|
|
|
35675
35896
|
continue;
|
|
35676
35897
|
const buf = new Uint8Array(await res.arrayBuffer());
|
|
35677
35898
|
const safe = it.filename.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
35678
|
-
const p =
|
|
35679
|
-
|
|
35899
|
+
const p = join12(destDir, safe);
|
|
35900
|
+
writeFileSync7(p, buf);
|
|
35680
35901
|
out.push({ filename: it.filename, path: p });
|
|
35681
35902
|
}
|
|
35682
35903
|
return out;
|
|
@@ -35686,9 +35907,9 @@ async function downloadCommentAttachments(apiUrl, wsId, projectId, issueId, comm
|
|
|
35686
35907
|
}
|
|
35687
35908
|
function authTokenHeader() {
|
|
35688
35909
|
try {
|
|
35689
|
-
if (!
|
|
35910
|
+
if (!existsSync12(CONFIG_PATH2))
|
|
35690
35911
|
return null;
|
|
35691
|
-
const raw = JSON.parse(
|
|
35912
|
+
const raw = JSON.parse(readFileSync10(CONFIG_PATH2, "utf8"));
|
|
35692
35913
|
const token = raw.token ?? raw.authToken;
|
|
35693
35914
|
return token ? `Bearer ${token}` : null;
|
|
35694
35915
|
} catch {
|
|
@@ -35696,14 +35917,14 @@ function authTokenHeader() {
|
|
|
35696
35917
|
}
|
|
35697
35918
|
}
|
|
35698
35919
|
async function uploadOutputDir(apiUrl, wsId, projectId, issueId, commentId, dir) {
|
|
35699
|
-
if (!
|
|
35920
|
+
if (!existsSync12(dir))
|
|
35700
35921
|
return 0;
|
|
35701
35922
|
const files = [];
|
|
35702
35923
|
const walk = (d, depth = 0) => {
|
|
35703
35924
|
if (depth > 3)
|
|
35704
35925
|
return;
|
|
35705
|
-
for (const name of
|
|
35706
|
-
const p =
|
|
35926
|
+
for (const name of readdirSync3(d)) {
|
|
35927
|
+
const p = join12(d, name);
|
|
35707
35928
|
try {
|
|
35708
35929
|
const st = statSync2(p);
|
|
35709
35930
|
if (st.isDirectory())
|
|
@@ -35721,7 +35942,7 @@ async function uploadOutputDir(apiUrl, wsId, projectId, issueId, commentId, dir)
|
|
|
35721
35942
|
let uploaded = 0;
|
|
35722
35943
|
for (const f of files) {
|
|
35723
35944
|
try {
|
|
35724
|
-
const data =
|
|
35945
|
+
const data = readFileSync10(f);
|
|
35725
35946
|
const form = new FormData;
|
|
35726
35947
|
const blob = new Blob([data]);
|
|
35727
35948
|
form.append("file", blob, f.split("/").pop() || "file");
|
|
@@ -35736,7 +35957,7 @@ async function uploadOutputDir(apiUrl, wsId, projectId, issueId, commentId, dir)
|
|
|
35736
35957
|
if (res.ok) {
|
|
35737
35958
|
uploaded++;
|
|
35738
35959
|
try {
|
|
35739
|
-
|
|
35960
|
+
unlinkSync6(f);
|
|
35740
35961
|
} catch {}
|
|
35741
35962
|
}
|
|
35742
35963
|
} catch (e) {
|
|
@@ -36012,10 +36233,10 @@ var init_run_task = __esm(() => {
|
|
|
36012
36233
|
init_git_enforce();
|
|
36013
36234
|
init_outbox();
|
|
36014
36235
|
HOME4 = process.env.HOME || process.env.USERPROFILE || ".";
|
|
36015
|
-
MULTI_DIR4 =
|
|
36016
|
-
CONFIG_PATH2 =
|
|
36017
|
-
LOG_PATH3 =
|
|
36018
|
-
SKILLS_DIR2 =
|
|
36236
|
+
MULTI_DIR4 = join12(HOME4, ".multi");
|
|
36237
|
+
CONFIG_PATH2 = join12(MULTI_DIR4, "config.json");
|
|
36238
|
+
LOG_PATH3 = join12(MULTI_DIR4, "logs", "agent.log");
|
|
36239
|
+
SKILLS_DIR2 = join12(MULTI_DIR4, "skills");
|
|
36019
36240
|
});
|
|
36020
36241
|
|
|
36021
36242
|
// ../lib/chat-doc.ts
|
|
@@ -36061,12 +36282,12 @@ function patchMessage(doc2, mapId, patch9) {
|
|
|
36061
36282
|
map20.set(k, v);
|
|
36062
36283
|
}
|
|
36063
36284
|
}
|
|
36064
|
-
function appendText(doc2, mapId,
|
|
36285
|
+
function appendText(doc2, mapId, chunk3) {
|
|
36065
36286
|
const map20 = doc2.getContainerById(mapId);
|
|
36066
36287
|
if (!map20)
|
|
36067
36288
|
return;
|
|
36068
36289
|
const cur = map20.get("text") ?? "";
|
|
36069
|
-
map20.set("text", cur +
|
|
36290
|
+
map20.set("text", cur + chunk3);
|
|
36070
36291
|
}
|
|
36071
36292
|
function finalizeMessage(doc2, mapId) {
|
|
36072
36293
|
const map20 = doc2.getContainerById(mapId);
|
|
@@ -36107,8 +36328,8 @@ var init_chat_doc = __esm(() => {
|
|
|
36107
36328
|
});
|
|
36108
36329
|
|
|
36109
36330
|
// src/_impl/chat-peer.ts
|
|
36110
|
-
import { existsSync as
|
|
36111
|
-
import { dirname as dirname10, join as
|
|
36331
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
|
|
36332
|
+
import { dirname as dirname10, join as join13 } from "path";
|
|
36112
36333
|
import { LoroDoc as LoroDoc2 } from "loro-crdt";
|
|
36113
36334
|
|
|
36114
36335
|
class ChatPeer {
|
|
@@ -36125,24 +36346,24 @@ class ChatPeer {
|
|
|
36125
36346
|
constructor(opts) {
|
|
36126
36347
|
this.opts = opts;
|
|
36127
36348
|
this.chatId = opts.chatId;
|
|
36128
|
-
this.snapshotPath =
|
|
36349
|
+
this.snapshotPath = join13(MULTI_DIR, "chats", `${opts.chatId}.loro`);
|
|
36129
36350
|
this.doc = this.loadFromDisk();
|
|
36130
36351
|
for (const m of listMessages(this.doc))
|
|
36131
36352
|
this.seenIds.add(m.id);
|
|
36132
36353
|
}
|
|
36133
36354
|
loadFromDisk() {
|
|
36134
36355
|
try {
|
|
36135
|
-
if (
|
|
36136
|
-
const bytes =
|
|
36356
|
+
if (existsSync13(this.snapshotPath)) {
|
|
36357
|
+
const bytes = readFileSync11(this.snapshotPath);
|
|
36137
36358
|
return importSnapshot(new Uint8Array(bytes));
|
|
36138
36359
|
}
|
|
36139
36360
|
} catch {}
|
|
36140
36361
|
return new LoroDoc2;
|
|
36141
36362
|
}
|
|
36142
36363
|
persist() {
|
|
36143
|
-
if (!
|
|
36144
|
-
|
|
36145
|
-
|
|
36364
|
+
if (!existsSync13(dirname10(this.snapshotPath)))
|
|
36365
|
+
mkdirSync10(dirname10(this.snapshotPath), { recursive: true });
|
|
36366
|
+
writeFileSync8(this.snapshotPath, exportSnapshot(this.doc));
|
|
36146
36367
|
this.dirtySinceWrite = 0;
|
|
36147
36368
|
}
|
|
36148
36369
|
appendAndPush(msg) {
|
|
@@ -36177,8 +36398,8 @@ class ChatPeer {
|
|
|
36177
36398
|
this.flush();
|
|
36178
36399
|
return { msgId: msg.id, containerId };
|
|
36179
36400
|
}
|
|
36180
|
-
appendPartialText(containerId,
|
|
36181
|
-
appendText(this.doc, containerId,
|
|
36401
|
+
appendPartialText(containerId, chunk3) {
|
|
36402
|
+
appendText(this.doc, containerId, chunk3);
|
|
36182
36403
|
this.flush();
|
|
36183
36404
|
}
|
|
36184
36405
|
finalizePartialMessage(containerId) {
|
|
@@ -36685,6 +36906,58 @@ async function executeChatPlanActions(actionsIn, parseErrors, ctx) {
|
|
|
36685
36906
|
lines.push(` - **${r.key}** [${r.status}] ${r.title}`);
|
|
36686
36907
|
results.push({ type: "issue.search", status: "ok", count: rows.length, query: a.query });
|
|
36687
36908
|
tally(true);
|
|
36909
|
+
} else if (a.type === "memory.search") {
|
|
36910
|
+
const project_id = a.project_id || ctx.projectId;
|
|
36911
|
+
if (!project_id) {
|
|
36912
|
+
lines.push(`- [err] memory.search "${a.query}": project_id required (chat has no pinned project)`);
|
|
36913
|
+
results.push({ type: "memory.search", status: "error", error: "project_id required (chat has no pinned project)", label: a.query });
|
|
36914
|
+
tally(false);
|
|
36915
|
+
continue;
|
|
36916
|
+
}
|
|
36917
|
+
const limit = a.limit ?? 10;
|
|
36918
|
+
const qs = new URLSearchParams({ project_id, q: a.query, limit: String(limit) });
|
|
36919
|
+
const res = await apiClient.get(`${ctx.apiUrl}/api/memory/search?${qs.toString()}`);
|
|
36920
|
+
if (!res.success) {
|
|
36921
|
+
lines.push(`- [err] memory.search "${a.query}": ${res.error || res.status}`);
|
|
36922
|
+
results.push({ type: "memory.search", status: "error", error: String(res.error || res.status), label: a.query });
|
|
36923
|
+
tally(false);
|
|
36924
|
+
continue;
|
|
36925
|
+
}
|
|
36926
|
+
const rows = Array.isArray(res.data?.rows) ? res.data.rows : [];
|
|
36927
|
+
lines.push(`- [ok] memory.search "${a.query}": ${rows.length} hit(s)`);
|
|
36928
|
+
for (const r of rows) {
|
|
36929
|
+
const summary5 = (r.summary || r.text || "").replace(/\s+/g, " ").slice(0, 200);
|
|
36930
|
+
lines.push(` - [${r.source_kind || "memory"}] ${summary5}`);
|
|
36931
|
+
}
|
|
36932
|
+
results.push({ type: "memory.search", status: "ok", count: rows.length, query: a.query });
|
|
36933
|
+
tally(true);
|
|
36934
|
+
} else if (a.type === "memory.write") {
|
|
36935
|
+
const project_id = a.project_id || ctx.projectId;
|
|
36936
|
+
if (!project_id) {
|
|
36937
|
+
lines.push(`- [err] memory.write: project_id required (chat has no pinned project)`);
|
|
36938
|
+
results.push({ type: "memory.write", status: "error", error: "project_id required (chat has no pinned project)" });
|
|
36939
|
+
tally(false);
|
|
36940
|
+
continue;
|
|
36941
|
+
}
|
|
36942
|
+
const body = {
|
|
36943
|
+
project_id,
|
|
36944
|
+
source_kind: a.kind || "agent_note",
|
|
36945
|
+
source_id: a.source_id || `chat:${ctx.chatId}:${Date.now()}`,
|
|
36946
|
+
agent_id: ctx.agentId || null,
|
|
36947
|
+
text: a.text,
|
|
36948
|
+
summary: a.summary || null
|
|
36949
|
+
};
|
|
36950
|
+
const res = await apiClient.post(`${ctx.apiUrl}/api/memory/write`, body);
|
|
36951
|
+
if (!res.success) {
|
|
36952
|
+
lines.push(`- [err] memory.write: ${res.error || res.status}`);
|
|
36953
|
+
results.push({ type: "memory.write", status: "error", error: String(res.error || res.status) });
|
|
36954
|
+
tally(false);
|
|
36955
|
+
continue;
|
|
36956
|
+
}
|
|
36957
|
+
const row = res.data?.row || {};
|
|
36958
|
+
lines.push(`- [ok] memory.write -> ${row.id || "(no id)"}${row.embedding_id ? " (embedded)" : ""}`);
|
|
36959
|
+
results.push({ type: "memory.write", status: "ok", memory_id: row.id, embedded: !!row.embedding_id });
|
|
36960
|
+
tally(true);
|
|
36688
36961
|
} else if (a.type === "agent.create") {
|
|
36689
36962
|
const res = await apiClient.post(agentsMutateUrl, {
|
|
36690
36963
|
action: "create",
|
|
@@ -36800,7 +37073,9 @@ var init_chat_plan_actions = __esm(() => {
|
|
|
36800
37073
|
"skill.detach": 5,
|
|
36801
37074
|
"agent.update": 5,
|
|
36802
37075
|
"session.create": 3,
|
|
36803
|
-
"issue.comment": 5
|
|
37076
|
+
"issue.comment": 5,
|
|
37077
|
+
"memory.search": 5,
|
|
37078
|
+
"memory.write": 5
|
|
36804
37079
|
};
|
|
36805
37080
|
});
|
|
36806
37081
|
|
|
@@ -37178,6 +37453,8 @@ Action vocabulary:
|
|
|
37178
37453
|
{"type":"issue.delete_where","status":"todo","limit":50},
|
|
37179
37454
|
{"type":"issue.list","status":"todo","limit":20},
|
|
37180
37455
|
{"type":"issue.search","query":"flaky tests","limit":10},
|
|
37456
|
+
{"type":"memory.search","query":"deploy pipeline","limit":10},
|
|
37457
|
+
{"type":"memory.write","text":"durable note worth recalling next session","summary":"short hint","kind":"agent_note"},
|
|
37181
37458
|
{"type":"agent.create","name":"refactor-bot","prompt":"...","allowed_tools":["Read","Edit","Bash"]},
|
|
37182
37459
|
{"type":"agent.update","id":"ag_xxx","prompt":"..."},
|
|
37183
37460
|
{"type":"skill.create","name":"run-tests","description":"...","body":"---\\nname: run-tests\\n---\\n..."},
|
|
@@ -37188,7 +37465,9 @@ Action vocabulary:
|
|
|
37188
37465
|
|
|
37189
37466
|
Rules:
|
|
37190
37467
|
- Omit the block entirely if no actions are needed. Don't emit empty arrays.
|
|
37191
|
-
-
|
|
37468
|
+
- Status values for \`update\`: \`todo\` | \`in_progress\` | \`blocked\` | \`done\` | \`archived\` | \`failed\`. **Use \`blocked\` (NOT \`done\`) when the issue is paused waiting on a human decision** — confirmation, choice, or missing context. Marking such an issue \`done\` misrepresents finished work. Use \`archived\` to hide a completed/abandoned issue from default views.
|
|
37469
|
+
- Max 10 actions per turn. Sub-caps: agent.create=2, skill.create=3, agent.update=5, skill.attach/detach=5, session.create=3, issue.comment=5, memory.search=5, memory.write=5.
|
|
37470
|
+
- Memory actions are project-scoped persistent notes (FTS5 + vector). \`memory.search\` returns hits in the action summary; the agent reads them next turn. \`memory.write\` records durable facts for future sessions — use sparingly, not for transient task state. Both require a pinned project.
|
|
37192
37471
|
- Chat-initiated agent.create / agent.update / skill.attach are auto-approved (the user is reading this reply right now). skill.create still queues for human review.
|
|
37193
37472
|
- Use \`issue.comment\` with \`@<agent name>\` mention to dispatch an agent on an existing issue. Plain comments without an @mention are recorded but do not trigger a run. Issues whose autonomy is \`manual\` will not dispatch.
|
|
37194
37473
|
- To create an issue AND assign/dispatch it to an agent in the same turn, set \`assignee_type\` + \`assignee_id\` directly on the \`create\` action. Do NOT emit a separate \`delegate\` or \`issue.comment\` action referring to a brand-new issue in the same plan block — actions execute as a flat list and the new issue's id/key is not available to later actions in the same block. \`delegate\` and \`issue.comment\` are for issues that already exist before this turn.
|
|
@@ -37962,7 +38241,7 @@ import { parseArgs } from "util";
|
|
|
37962
38241
|
// package.json
|
|
37963
38242
|
var package_default = {
|
|
37964
38243
|
name: "@shipers-dev/multi",
|
|
37965
|
-
version: "0.
|
|
38244
|
+
version: "0.52.0",
|
|
37966
38245
|
type: "module",
|
|
37967
38246
|
bin: {
|
|
37968
38247
|
"multi-agent": "./dist/index.js"
|
|
@@ -38476,7 +38755,7 @@ class Runners extends exports_Effect.Service()("cli/Runners", {
|
|
|
38476
38755
|
// src/commands/simple.ts
|
|
38477
38756
|
init_esm();
|
|
38478
38757
|
init_paths();
|
|
38479
|
-
import { existsSync as
|
|
38758
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
38480
38759
|
|
|
38481
38760
|
// src/services/Config.ts
|
|
38482
38761
|
init_esm();
|
|
@@ -38571,7 +38850,7 @@ var statusCmd = exports_Effect.fn("statusCmd")(function* () {
|
|
|
38571
38850
|
process.exit(1);
|
|
38572
38851
|
}
|
|
38573
38852
|
const d = res.data;
|
|
38574
|
-
const pid =
|
|
38853
|
+
const pid = existsSync7(PID_PATH) ? readFileSync7(PID_PATH, "utf8").trim() : null;
|
|
38575
38854
|
const daemon = pid && isRunning3(Number(pid)) ? `running (pid ${pid})` : "stopped";
|
|
38576
38855
|
console.log(`
|
|
38577
38856
|
Device Status
|
|
@@ -38586,11 +38865,11 @@ Daemon: ${daemon}
|
|
|
38586
38865
|
});
|
|
38587
38866
|
var stopCmd = exports_Effect.fn("stopCmd")(function* () {
|
|
38588
38867
|
const fs3 = yield* FileSystem;
|
|
38589
|
-
if (!
|
|
38868
|
+
if (!existsSync7(PID_PATH)) {
|
|
38590
38869
|
console.log("No daemon running.");
|
|
38591
38870
|
return;
|
|
38592
38871
|
}
|
|
38593
|
-
const pid = Number(
|
|
38872
|
+
const pid = Number(readFileSync7(PID_PATH, "utf8").trim());
|
|
38594
38873
|
if (!pid || !isRunning3(pid)) {
|
|
38595
38874
|
yield* fs3.remove(PID_PATH);
|
|
38596
38875
|
console.log("Cleaned stale pidfile.");
|
|
@@ -38603,11 +38882,11 @@ var stopCmd = exports_Effect.fn("stopCmd")(function* () {
|
|
|
38603
38882
|
} catch {}
|
|
38604
38883
|
});
|
|
38605
38884
|
var logsCmd = exports_Effect.fn("logsCmd")(function* () {
|
|
38606
|
-
if (!
|
|
38885
|
+
if (!existsSync7(LOG_PATH)) {
|
|
38607
38886
|
console.log("No logs yet.");
|
|
38608
38887
|
return;
|
|
38609
38888
|
}
|
|
38610
|
-
const content =
|
|
38889
|
+
const content = readFileSync7(LOG_PATH, "utf8");
|
|
38611
38890
|
console.log(content.split(`
|
|
38612
38891
|
`).slice(-100).join(`
|
|
38613
38892
|
`));
|
|
@@ -38616,10 +38895,10 @@ var resetCmd = exports_Effect.fn("resetCmd")(function* (issueId) {
|
|
|
38616
38895
|
if (!issueId) {
|
|
38617
38896
|
return yield* exports_Effect.fail(new UsageError({ message: "reset requires --issue <id>" }));
|
|
38618
38897
|
}
|
|
38619
|
-
if (!
|
|
38898
|
+
if (!existsSync7(PORT_PATH)) {
|
|
38620
38899
|
return yield* exports_Effect.fail(new DaemonError({ message: "Daemon not running (no port file)." }));
|
|
38621
38900
|
}
|
|
38622
|
-
const port = Number(
|
|
38901
|
+
const port = Number(readFileSync7(PORT_PATH, "utf8").trim());
|
|
38623
38902
|
const config2 = yield* Config4;
|
|
38624
38903
|
const cfg = yield* config2.load;
|
|
38625
38904
|
if (!cfg.dispatchSecret) {
|
|
@@ -38780,10 +39059,10 @@ var linkCmd = exports_Effect.fn("linkCmd")(function* (apiUrl, agentId) {
|
|
|
38780
39059
|
// src/commands/restart.ts
|
|
38781
39060
|
init_esm();
|
|
38782
39061
|
init_paths();
|
|
38783
|
-
import { existsSync as
|
|
39062
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, unlinkSync as unlinkSync5 } from "fs";
|
|
38784
39063
|
|
|
38785
39064
|
// src/_impl/pid-lock.ts
|
|
38786
|
-
import { closeSync, existsSync as
|
|
39065
|
+
import { closeSync, existsSync as existsSync8, mkdirSync as mkdirSync7, openSync, readFileSync as readFileSync8, unlinkSync as unlinkSync4, writeSync } from "fs";
|
|
38787
39066
|
import { dirname as dirname8 } from "path";
|
|
38788
39067
|
var isAlive = (pid) => {
|
|
38789
39068
|
if (!Number.isFinite(pid) || pid <= 0)
|
|
@@ -38796,7 +39075,7 @@ var isAlive = (pid) => {
|
|
|
38796
39075
|
}
|
|
38797
39076
|
};
|
|
38798
39077
|
var writeExclusive = (path, pid) => {
|
|
38799
|
-
|
|
39078
|
+
mkdirSync7(dirname8(path), { recursive: true });
|
|
38800
39079
|
const fd = openSync(path, "wx");
|
|
38801
39080
|
try {
|
|
38802
39081
|
writeSync(fd, String(pid));
|
|
@@ -38815,7 +39094,7 @@ var acquirePidLock = (path) => {
|
|
|
38815
39094
|
}
|
|
38816
39095
|
const raw = (() => {
|
|
38817
39096
|
try {
|
|
38818
|
-
return
|
|
39097
|
+
return readFileSync8(path, "utf8").trim();
|
|
38819
39098
|
} catch {
|
|
38820
39099
|
return "";
|
|
38821
39100
|
}
|
|
@@ -38827,7 +39106,7 @@ var acquirePidLock = (path) => {
|
|
|
38827
39106
|
return { ok: false, existingPid };
|
|
38828
39107
|
}
|
|
38829
39108
|
try {
|
|
38830
|
-
|
|
39109
|
+
unlinkSync4(path);
|
|
38831
39110
|
} catch {}
|
|
38832
39111
|
try {
|
|
38833
39112
|
writeExclusive(path, ourPid);
|
|
@@ -38837,7 +39116,7 @@ var acquirePidLock = (path) => {
|
|
|
38837
39116
|
throw e;
|
|
38838
39117
|
const racingRaw = (() => {
|
|
38839
39118
|
try {
|
|
38840
|
-
return
|
|
39119
|
+
return readFileSync8(path, "utf8").trim();
|
|
38841
39120
|
} catch {
|
|
38842
39121
|
return "";
|
|
38843
39122
|
}
|
|
@@ -38848,12 +39127,12 @@ var acquirePidLock = (path) => {
|
|
|
38848
39127
|
};
|
|
38849
39128
|
var releasePidLock = (path) => {
|
|
38850
39129
|
try {
|
|
38851
|
-
if (!
|
|
39130
|
+
if (!existsSync8(path))
|
|
38852
39131
|
return;
|
|
38853
|
-
const raw =
|
|
39132
|
+
const raw = readFileSync8(path, "utf8").trim();
|
|
38854
39133
|
if (Number(raw) !== process.pid)
|
|
38855
39134
|
return;
|
|
38856
|
-
|
|
39135
|
+
unlinkSync4(path);
|
|
38857
39136
|
} catch {}
|
|
38858
39137
|
};
|
|
38859
39138
|
var killStaleConnects = () => {
|
|
@@ -38918,8 +39197,8 @@ var isRunning4 = (pid) => {
|
|
|
38918
39197
|
var restartCmd = exports_Effect.fn("restartCmd")(function* (apiUrl) {
|
|
38919
39198
|
const fs3 = yield* FileSystem;
|
|
38920
39199
|
yield* (yield* Config4).ensureDirs;
|
|
38921
|
-
if (
|
|
38922
|
-
const pid = Number(
|
|
39200
|
+
if (existsSync9(PID_PATH)) {
|
|
39201
|
+
const pid = Number(readFileSync9(PID_PATH, "utf8").trim());
|
|
38923
39202
|
if (pid && isRunning4(pid)) {
|
|
38924
39203
|
yield* fs3.writeText(STOP_PATH, "1");
|
|
38925
39204
|
try {
|
|
@@ -38938,13 +39217,13 @@ var restartCmd = exports_Effect.fn("restartCmd")(function* (apiUrl) {
|
|
|
38938
39217
|
}
|
|
38939
39218
|
}
|
|
38940
39219
|
try {
|
|
38941
|
-
if (
|
|
38942
|
-
|
|
39220
|
+
if (existsSync9(PID_PATH))
|
|
39221
|
+
unlinkSync5(PID_PATH);
|
|
38943
39222
|
} catch {}
|
|
38944
39223
|
}
|
|
38945
39224
|
try {
|
|
38946
|
-
if (
|
|
38947
|
-
|
|
39225
|
+
if (existsSync9(STOP_PATH))
|
|
39226
|
+
unlinkSync5(STOP_PATH);
|
|
38948
39227
|
} catch {}
|
|
38949
39228
|
const killed = killStaleConnects();
|
|
38950
39229
|
if (killed.length)
|
|
@@ -39040,21 +39319,22 @@ init_client();
|
|
|
39040
39319
|
init_detect();
|
|
39041
39320
|
init_run_task();
|
|
39042
39321
|
import { Database as Database3 } from "bun:sqlite";
|
|
39043
|
-
import { existsSync as
|
|
39044
|
-
import { homedir as
|
|
39045
|
-
import { join as
|
|
39322
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync7 } from "fs";
|
|
39323
|
+
import { homedir as homedir3 } from "os";
|
|
39324
|
+
import { join as join14 } from "path";
|
|
39325
|
+
init_adapter_pidfile();
|
|
39046
39326
|
init_errors();
|
|
39047
|
-
var MULTI_DIR5 =
|
|
39048
|
-
var PID_PATH2 =
|
|
39049
|
-
var PORT_PATH2 =
|
|
39050
|
-
var STOP_PATH2 =
|
|
39051
|
-
var TASKS_DB_PATH3 =
|
|
39327
|
+
var MULTI_DIR5 = join14(homedir3(), ".multi");
|
|
39328
|
+
var PID_PATH2 = join14(MULTI_DIR5, "agent.pid");
|
|
39329
|
+
var PORT_PATH2 = join14(MULTI_DIR5, "agent.port");
|
|
39330
|
+
var STOP_PATH2 = join14(MULTI_DIR5, "stop.flag");
|
|
39331
|
+
var TASKS_DB_PATH3 = join14(MULTI_DIR5, "tasks.db");
|
|
39052
39332
|
function ensureDirs2() {
|
|
39053
|
-
if (!
|
|
39054
|
-
|
|
39055
|
-
const logs =
|
|
39056
|
-
if (!
|
|
39057
|
-
|
|
39333
|
+
if (!existsSync14(MULTI_DIR5))
|
|
39334
|
+
mkdirSync11(MULTI_DIR5, { recursive: true });
|
|
39335
|
+
const logs = join14(MULTI_DIR5, "logs");
|
|
39336
|
+
if (!existsSync14(logs))
|
|
39337
|
+
mkdirSync11(logs, { recursive: true });
|
|
39058
39338
|
}
|
|
39059
39339
|
function isLocalApi(url2) {
|
|
39060
39340
|
try {
|
|
@@ -39346,8 +39626,11 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
|
|
|
39346
39626
|
if (cfg.authToken)
|
|
39347
39627
|
setAuthToken(cfg.authToken);
|
|
39348
39628
|
ensureDirs2();
|
|
39349
|
-
if (
|
|
39350
|
-
|
|
39629
|
+
if (existsSync14(STOP_PATH2))
|
|
39630
|
+
unlinkSync7(STOP_PATH2);
|
|
39631
|
+
const reaped = reapStaleAdapters((m) => log3(m));
|
|
39632
|
+
if (reaped > 0)
|
|
39633
|
+
log3(`reaped ${reaped} stale adapter${reaped === 1 ? "" : "s"}`);
|
|
39351
39634
|
const detected = yield* exports_Effect.promise(() => detectAgents());
|
|
39352
39635
|
const localMode = isLocalApi(apiUrl);
|
|
39353
39636
|
log3(`daemon device=${cfg.deviceId} pid=${process.pid}`);
|
|
@@ -39481,10 +39764,10 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
|
|
|
39481
39764
|
});
|
|
39482
39765
|
log3(`Local server: http://127.0.0.1:${port}`);
|
|
39483
39766
|
try {
|
|
39484
|
-
|
|
39767
|
+
writeFileSync9(PORT_PATH2, String(port));
|
|
39485
39768
|
} catch {}
|
|
39486
39769
|
try {
|
|
39487
|
-
|
|
39770
|
+
writeFileSync9(PID_PATH2, String(process.pid));
|
|
39488
39771
|
} catch {}
|
|
39489
39772
|
let tunnel;
|
|
39490
39773
|
if (localMode) {
|
|
@@ -39548,7 +39831,7 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
|
|
|
39548
39831
|
let probeFailures = 0;
|
|
39549
39832
|
while (true) {
|
|
39550
39833
|
yield* exports_Effect.sleep(exports_Duration.seconds(120));
|
|
39551
|
-
if (
|
|
39834
|
+
if (existsSync14(STOP_PATH2)) {
|
|
39552
39835
|
yield* exports_Deferred.succeed(stopDeferred, "stop flag");
|
|
39553
39836
|
return;
|
|
39554
39837
|
}
|
|
@@ -39573,7 +39856,7 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
|
|
|
39573
39856
|
yield* exports_Effect.forkIn(daemonScope)(exports_Effect.gen(function* () {
|
|
39574
39857
|
while (true) {
|
|
39575
39858
|
yield* exports_Effect.sleep(exports_Duration.seconds(5));
|
|
39576
|
-
if (
|
|
39859
|
+
if (existsSync14(STOP_PATH2)) {
|
|
39577
39860
|
yield* exports_Deferred.succeed(stopDeferred, "stop flag");
|
|
39578
39861
|
return;
|
|
39579
39862
|
}
|
|
@@ -39613,12 +39896,12 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
|
|
|
39613
39896
|
yield* exports_Effect.race(exports_Fiber.interruptAll(inFlight), exports_Effect.sleep(exports_Duration.seconds(10)));
|
|
39614
39897
|
}
|
|
39615
39898
|
yield* exports_Scope.close(daemonScope, exports_Exit.void).pipe(exports_Effect.catchAll(() => exports_Effect.void));
|
|
39616
|
-
if (
|
|
39617
|
-
|
|
39618
|
-
if (
|
|
39619
|
-
|
|
39620
|
-
if (
|
|
39621
|
-
|
|
39899
|
+
if (existsSync14(PID_PATH2))
|
|
39900
|
+
unlinkSync7(PID_PATH2);
|
|
39901
|
+
if (existsSync14(STOP_PATH2))
|
|
39902
|
+
unlinkSync7(STOP_PATH2);
|
|
39903
|
+
if (existsSync14(PORT_PATH2))
|
|
39904
|
+
unlinkSync7(PORT_PATH2);
|
|
39622
39905
|
db2.close();
|
|
39623
39906
|
log3("disconnected");
|
|
39624
39907
|
});
|