@nanopm/cli 0.1.11 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +514 -296
- package/package.json +1 -1
- package/skills/build/skill.json +1 -1
package/dist/index.js
CHANGED
|
@@ -391,7 +391,7 @@ var SkillManifestSchema = z2.object({
|
|
|
391
391
|
/**
|
|
392
392
|
* Run only when the founder asks for it on one item, by a click or a command, never by Nano on
|
|
393
393
|
* its own: the autonomy dial, which governs what Nano does unasked, does not cap it
|
|
394
|
-
* (`capCapabilities`). A build is the one today (docs/⏳nanopm-
|
|
394
|
+
* (`capCapabilities`). A build is the one today (docs/⏳nanopm-76-build-it.md, settled 2).
|
|
395
395
|
*/
|
|
396
396
|
asked: z2.boolean().default(false),
|
|
397
397
|
/** The first user message sent to the harness, unless the job params provide one. */
|
|
@@ -673,6 +673,18 @@ function formatStreamEvent(event) {
|
|
|
673
673
|
return "\u2298 cancel requested";
|
|
674
674
|
case "error":
|
|
675
675
|
return `\u2717 ${String(p.code ?? "error")}: ${String(p.message ?? "")}`;
|
|
676
|
+
// A build (docs/⏳nanopm-76-build-it.md): its plan, each step with its commit, a pause, the pull request.
|
|
677
|
+
case "plan":
|
|
678
|
+
return ["The plan:", ...(Array.isArray(p.steps) ? p.steps : []).map((s, i) => ` ${i + 1}. ${String(s)}`)].join("\n");
|
|
679
|
+
case "step":
|
|
680
|
+
return `\u2713 ${String(p.step ?? "")}. ${String(p.says ?? "")}${p.commit ? ` \xB7 ${String(p.commit)}` : ""}`;
|
|
681
|
+
case "paused": {
|
|
682
|
+
const at = new Date(String(p.resets_at ?? ""));
|
|
683
|
+
const when = Number.isNaN(at.getTime()) ? "when it resets" : `at ${at.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`;
|
|
684
|
+
return `\u25CC paused: your Claude limit is reached. Nano carries on by itself ${when}.`;
|
|
685
|
+
}
|
|
686
|
+
case "pr":
|
|
687
|
+
return `\u2387 draft pull request #${String(p.number ?? "")} \xB7 ${String(p.url ?? "")}`;
|
|
676
688
|
default:
|
|
677
689
|
return null;
|
|
678
690
|
}
|
|
@@ -3673,7 +3685,7 @@ function buildSandboxSettings(home) {
|
|
|
3673
3685
|
}
|
|
3674
3686
|
return { permissions: { deny, defaultMode: "default" } };
|
|
3675
3687
|
}
|
|
3676
|
-
function sandboxSettingsFor(home, grantedTools, network = []) {
|
|
3688
|
+
function sandboxSettingsFor(home, grantedTools, network = [], answerFile) {
|
|
3677
3689
|
const settings = buildSandboxSettings(home);
|
|
3678
3690
|
const granted = new Set(grantedTools);
|
|
3679
3691
|
settings.permissions.deny = settings.permissions.deny.filter((rule) => !granted.has(rule));
|
|
@@ -3685,8 +3697,14 @@ function sandboxSettingsFor(home, grantedTools, network = []) {
|
|
|
3685
3697
|
network: { allowedDomains: network.filter((h) => !/(^|\.)github(usercontent)?\.com$/.test(h)) }
|
|
3686
3698
|
};
|
|
3687
3699
|
}
|
|
3700
|
+
if (answerFile) settings.hooks = { PreToolUse: [{ matcher: "mcp__nanopm__ask_user", hooks: [{ type: "command", command: askHook(answerFile) }] }] };
|
|
3688
3701
|
return settings;
|
|
3689
3702
|
}
|
|
3703
|
+
function askHook(answerFile) {
|
|
3704
|
+
const quote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
3705
|
+
const decision = (d) => quote(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: d } }));
|
|
3706
|
+
return `if [ -f ${quote(answerFile)} ]; then mv ${quote(answerFile)} ${quote(`${answerFile}.taken`)}; printf '%s' ${decision("allow")}; else printf '%s' ${decision("defer")}; fi`;
|
|
3707
|
+
}
|
|
3690
3708
|
|
|
3691
3709
|
// ../daemon/src/adapters/claude-code/stream.ts
|
|
3692
3710
|
var RESULT_FAILURES = {
|
|
@@ -3704,6 +3722,8 @@ var StreamParser = class {
|
|
|
3704
3722
|
model = "unknown";
|
|
3705
3723
|
toolCalls = /* @__PURE__ */ new Map();
|
|
3706
3724
|
sawResult = false;
|
|
3725
|
+
/** When the Claude window opens again, from the last rejected `rate_limit_event`. */
|
|
3726
|
+
resetsAt;
|
|
3707
3727
|
// The read size, counted here because the stream is the one place every call passes.
|
|
3708
3728
|
contextPeak = 0;
|
|
3709
3729
|
readCalls = {};
|
|
@@ -3714,6 +3734,9 @@ var StreamParser = class {
|
|
|
3714
3734
|
get currentSessionId() {
|
|
3715
3735
|
return this.sessionId;
|
|
3716
3736
|
}
|
|
3737
|
+
get limitResetsAt() {
|
|
3738
|
+
return this.resetsAt;
|
|
3739
|
+
}
|
|
3717
3740
|
/** Feed one NDJSON line; get zero or more events. Non-JSON lines are ignored. */
|
|
3718
3741
|
parseLine(line) {
|
|
3719
3742
|
const trimmed = line.trim();
|
|
@@ -3765,10 +3788,18 @@ var StreamParser = class {
|
|
|
3765
3788
|
}
|
|
3766
3789
|
return events;
|
|
3767
3790
|
}
|
|
3791
|
+
case "rate_limit_event": {
|
|
3792
|
+
const info = obj.rate_limit_info;
|
|
3793
|
+
if (info?.status === "rejected" && typeof info.resetsAt === "number") this.resetsAt = info.resetsAt;
|
|
3794
|
+
return [];
|
|
3795
|
+
}
|
|
3768
3796
|
case "result": {
|
|
3769
3797
|
this.sawResult = true;
|
|
3770
3798
|
this.sessionId = obj.session_id ?? this.sessionId;
|
|
3771
3799
|
const usage = this.usageFrom(obj);
|
|
3800
|
+
if (obj.stop_reason === "tool_deferred" && obj.deferred_tool_use?.name) {
|
|
3801
|
+
return [{ type: "deferred", sessionId: this.sessionId ?? "", tool: obj.deferred_tool_use.name, input: obj.deferred_tool_use.input, usage }];
|
|
3802
|
+
}
|
|
3772
3803
|
if (obj.subtype === "success" && !obj.is_error) {
|
|
3773
3804
|
if (this.structuredOutput && obj.structured_output === void 0) {
|
|
3774
3805
|
return [{ type: "failed", sessionId: this.sessionId, code: "harness_error", message: "Claude omitted the required structured output." }];
|
|
@@ -3777,7 +3808,7 @@ var StreamParser = class {
|
|
|
3777
3808
|
}
|
|
3778
3809
|
const message = obj.result || `claude ended with ${obj.subtype ?? "an unknown result"}`;
|
|
3779
3810
|
const code = isRateLimit(message) ? "rate_limit" : RESULT_FAILURES[obj.subtype ?? ""] ?? "harness_error";
|
|
3780
|
-
return [{ type: "failed", sessionId: this.sessionId, code, message }];
|
|
3811
|
+
return [{ type: "failed", sessionId: this.sessionId, code, message, ...code === "rate_limit" && this.resetsAt ? { resetsAt: this.resetsAt } : {} }];
|
|
3781
3812
|
}
|
|
3782
3813
|
default:
|
|
3783
3814
|
return [];
|
|
@@ -3993,7 +4024,7 @@ var ClaudeCodeAdapter = class {
|
|
|
3993
4024
|
await writeFile2(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 384 });
|
|
3994
4025
|
const settingsPath = join3(spec2.runDir, "settings.json");
|
|
3995
4026
|
const flags = claudeCodeToolFlags(spec2.capabilities, { workspace: spec2.cwd, harnessWeb: spec2.harnessWeb, without: spec2.withoutTools });
|
|
3996
|
-
await writeFile2(settingsPath, JSON.stringify(sandboxSettingsFor(spec2.env.HOME ?? "", flags.tools, spec2.network)), { mode: 384 });
|
|
4027
|
+
await writeFile2(settingsPath, JSON.stringify(sandboxSettingsFor(spec2.env.HOME ?? "", flags.tools, spec2.network, spec2.answerFile)), { mode: 384 });
|
|
3997
4028
|
const child = spawn(this.claudePath, this.buildArgs(spec2, mcpConfigPath, settingsPath), {
|
|
3998
4029
|
cwd: spec2.cwd,
|
|
3999
4030
|
env: spec2.env,
|
|
@@ -4024,7 +4055,8 @@ var ClaudeCodeAdapter = class {
|
|
|
4024
4055
|
const { code } = await exit;
|
|
4025
4056
|
if (!parser.finished) {
|
|
4026
4057
|
const failure2 = classifyExit(stderr, aborted);
|
|
4027
|
-
|
|
4058
|
+
const resetsAt = failure2.code === "rate_limit" ? parser.limitResetsAt : void 0;
|
|
4059
|
+
yield { type: "failed", sessionId: parser.currentSessionId, ...failure2, message: appendExit(failure2.message, code), ...resetsAt ? { resetsAt } : {} };
|
|
4028
4060
|
}
|
|
4029
4061
|
} finally {
|
|
4030
4062
|
spec2.signal.removeEventListener("abort", onAbort);
|
|
@@ -4052,7 +4084,7 @@ function parsePrintResult(stdout) {
|
|
|
4052
4084
|
import { mkdir as mkdir6, readdir as readdir3, readFile as readFile9, rm as rm5, writeFile as writeFile6 } from "fs/promises";
|
|
4053
4085
|
import { openSync, closeSync, unlinkSync, writeFileSync, readFileSync as readFileSyncNode, mkdirSync } from "fs";
|
|
4054
4086
|
import { hostname as hostname2 } from "os";
|
|
4055
|
-
import { join as
|
|
4087
|
+
import { join as join14 } from "path";
|
|
4056
4088
|
import { setTimeout as sleep } from "timers/promises";
|
|
4057
4089
|
|
|
4058
4090
|
// ../daemon/src/awake.ts
|
|
@@ -4137,6 +4169,7 @@ function shouldUpdate(s) {
|
|
|
4137
4169
|
}
|
|
4138
4170
|
|
|
4139
4171
|
// ../daemon/src/mcp.ts
|
|
4172
|
+
import { join as join5 } from "path";
|
|
4140
4173
|
function memoryMcpCommand(env = process.env) {
|
|
4141
4174
|
const override = env.NANOPM_MCP_COMMAND;
|
|
4142
4175
|
if (override) {
|
|
@@ -4163,13 +4196,17 @@ function memoryMcpServer(job, auth, askTimeoutMs, workspace) {
|
|
|
4163
4196
|
NANOPM_PROJECT_ID: job.project_id,
|
|
4164
4197
|
NANOPM_USER_ID: auth.userId,
|
|
4165
4198
|
NANOPM_ACTOR: job.skill,
|
|
4166
|
-
|
|
4199
|
+
// A build: its questions come back through the answer file the hook hands over (buildAnswerFile).
|
|
4200
|
+
...workspace ? { NANOPM_WORKSPACE: workspace, NANOPM_ANSWER_FILE: buildAnswerFile(job.id) } : {},
|
|
4167
4201
|
NANOPM_ASK_TIMEOUT_MS: String(askTimeoutMs),
|
|
4168
4202
|
PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
4169
4203
|
HOME: process.env.HOME ?? ""
|
|
4170
4204
|
}
|
|
4171
4205
|
};
|
|
4172
4206
|
}
|
|
4207
|
+
function buildAnswerFile(jobId) {
|
|
4208
|
+
return join5(nanopmHome(), "run", jobId, "answer");
|
|
4209
|
+
}
|
|
4173
4210
|
|
|
4174
4211
|
// ../connectors/src/postgres.ts
|
|
4175
4212
|
import postgres from "postgres";
|
|
@@ -5286,7 +5323,7 @@ import { promisify as promisify4 } from "util";
|
|
|
5286
5323
|
// ../daemon/src/repo.ts
|
|
5287
5324
|
import { execFile as execFile3 } from "child_process";
|
|
5288
5325
|
import { readFile as readFile2, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
5289
|
-
import { join as
|
|
5326
|
+
import { join as join6 } from "path";
|
|
5290
5327
|
|
|
5291
5328
|
// ../shared/src/build.ts
|
|
5292
5329
|
var BUILD_SKILL = "build";
|
|
@@ -5340,7 +5377,7 @@ async function queueBuild(client, ask7) {
|
|
|
5340
5377
|
if (readError) throw new Error(`could not read the solution: ${pgMessage(readError)}`);
|
|
5341
5378
|
if (!data) throw new Error(`No solution ${ask7.ref} on the Roadmap.`);
|
|
5342
5379
|
const solution = data;
|
|
5343
|
-
const refused = buildRefusal(solution, { anyway: ask7.anyway });
|
|
5380
|
+
const refused = buildRefusal(solution, { anyway: ask7.anyway || Boolean(ask7.resumeOf) });
|
|
5344
5381
|
if (refused) throw new Error(refused);
|
|
5345
5382
|
const { data: job, error } = await client.from("jobs").insert({
|
|
5346
5383
|
project_id: ask7.projectId,
|
|
@@ -5348,12 +5385,53 @@ async function queueBuild(client, ask7) {
|
|
|
5348
5385
|
requested_by: ask7.userId,
|
|
5349
5386
|
trigger: "user",
|
|
5350
5387
|
target_daemon_id: ask7.daemonId,
|
|
5351
|
-
|
|
5388
|
+
// `was`: where the solution goes back on the board if the build stops (`unstart_solution`).
|
|
5389
|
+
// `origin`: the run that made the copy of the code, which every continuation works in.
|
|
5390
|
+
params: { solution_id: solution.id, repo_path: ask7.repoPath, was: ask7.resumeOf?.was ?? solution.status, ...ask7.resumeOf ? { origin: ask7.resumeOf.origin } : {}, ...ask7.anyway ? { anyway: true } : {} },
|
|
5391
|
+
// Continuing a stopped build takes up its worktree, its history and its session.
|
|
5392
|
+
...ask7.resumeOf ? { resume_of: ask7.resumeOf.id } : {}
|
|
5352
5393
|
}).select("id").single();
|
|
5353
5394
|
if (error?.code === "23505") throw new Error("A build of this product is already running. One at a time: `nanopm status` says where it is.");
|
|
5354
5395
|
if (error || !job) throw new Error(`could not queue the build: ${error ? pgMessage(error) : "no row"}`);
|
|
5355
5396
|
return { jobId: job.id, solution };
|
|
5356
5397
|
}
|
|
5398
|
+
var DECIDE_FOR_ME = "Decide yourself: pick what you judge best for the users, then say what you decided and why under *Decisions I made*.";
|
|
5399
|
+
function answerForAgent(answer) {
|
|
5400
|
+
return /^\s*decide(\s+for\s+me)?\s*\.?\s*$/i.test(answer) ? DECIDE_FOR_ME : answer;
|
|
5401
|
+
}
|
|
5402
|
+
function trustedPlace(p, at) {
|
|
5403
|
+
const [branch, base, fork] = [p.branch, p.base, p.fork].map((v) => typeof v === "string" ? v : "");
|
|
5404
|
+
if (!/^nano\/[a-z0-9][a-z0-9-]*$/.test(branch) || !/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(base) || !/^[0-9a-f]{7,40}$/.test(fork)) return null;
|
|
5405
|
+
return { dir: buildDir(at.home, at.jobId), branch, base, fork };
|
|
5406
|
+
}
|
|
5407
|
+
function buildHistory(events, at) {
|
|
5408
|
+
let place = null;
|
|
5409
|
+
let plan = null;
|
|
5410
|
+
const done = /* @__PURE__ */ new Set();
|
|
5411
|
+
let last = null;
|
|
5412
|
+
const answers = /* @__PURE__ */ new Map();
|
|
5413
|
+
for (const e of events) {
|
|
5414
|
+
if (e.kind === "worktree") place = trustedPlace(e.payload, at);
|
|
5415
|
+
else if (e.kind === "plan" && Array.isArray(e.payload.steps)) plan = e.payload.steps;
|
|
5416
|
+
else if (e.kind === "step" && typeof e.payload.step === "number") done.add(e.payload.step - 1);
|
|
5417
|
+
else if (e.kind === "answer") answers.set(e.seq, String(e.payload.answer ?? ""));
|
|
5418
|
+
if (e.kind === "question" || e.kind === "paused") last = e;
|
|
5419
|
+
else if (e.kind === "plan" || e.kind === "step") last = null;
|
|
5420
|
+
}
|
|
5421
|
+
if (!place) return { place, plan, done, resume: null };
|
|
5422
|
+
if (last?.kind === "question" && answers.has(last.seq) && last.payload.about === "usage") return { place, plan, done, resume: { why: "limit" } };
|
|
5423
|
+
if (last?.kind === "question" && answers.has(last.seq)) return { place, plan, done, resume: { why: "answer", answer: answers.get(last.seq) } };
|
|
5424
|
+
if (last?.kind === "paused") return { place, plan, done, resume: { why: "limit" } };
|
|
5425
|
+
return { place, plan, done, resume: { why: "lost" } };
|
|
5426
|
+
}
|
|
5427
|
+
function wasOf(params) {
|
|
5428
|
+
return params?.was === "proposed" ? "proposed" : "accepted";
|
|
5429
|
+
}
|
|
5430
|
+
async function loadBuildHistory(client, jobIds, at) {
|
|
5431
|
+
const { data, error } = await client.from("job_events").select("kind, seq, payload").in("job_id", [...jobIds]).in("kind", ["worktree", "plan", "step", "question", "answer", "paused"]).order("id");
|
|
5432
|
+
if (error) throw new Error(`could not read the build's history: ${error.message}`);
|
|
5433
|
+
return buildHistory((data ?? []).map((e) => ({ kind: e.kind, seq: e.seq, payload: e.payload ?? {} })), at);
|
|
5434
|
+
}
|
|
5357
5435
|
|
|
5358
5436
|
// ../daemon/src/repo.ts
|
|
5359
5437
|
import { promisify as promisify3 } from "util";
|
|
@@ -5369,10 +5447,10 @@ var LOCKED = {
|
|
|
5369
5447
|
};
|
|
5370
5448
|
async function exec(cmd, repo, args, timeoutMs = TIMEOUT_MS) {
|
|
5371
5449
|
if (typeof repo === "string") return execFileAsync3(cmd, cmd === "git" ? ["-C", repo, ...args] : args, { cwd: repo, maxBuffer: MAX_BUFFER, timeout: timeoutMs });
|
|
5372
|
-
await writeFile4(
|
|
5450
|
+
await writeFile4(join6(repo.dir, ".git"), `gitdir: ${repo.admin}
|
|
5373
5451
|
`);
|
|
5374
|
-
await writeFile4(
|
|
5375
|
-
await rm3(
|
|
5452
|
+
await writeFile4(join6(repo.admin, "commondir"), repo.commondir);
|
|
5453
|
+
await rm3(join6(repo.admin, "config.worktree"), { force: true });
|
|
5376
5454
|
return execFileAsync3(cmd, args, { cwd: repo.dir, env: { ...process.env, GIT_DIR: repo.admin, GIT_WORK_TREE: repo.dir, ...LOCKED }, maxBuffer: MAX_BUFFER, timeout: timeoutMs });
|
|
5377
5455
|
}
|
|
5378
5456
|
async function git(repo, args, timeoutMs = TIMEOUT_MS) {
|
|
@@ -5441,7 +5519,7 @@ async function addWorktree(clone, dir, branch) {
|
|
|
5441
5519
|
const onto = await baseRef(clone, base);
|
|
5442
5520
|
await git(clone, ["worktree", "add", "-b", branch, dir, onto]);
|
|
5443
5521
|
const admin = await git(dir, ["rev-parse", "--absolute-git-dir"]);
|
|
5444
|
-
const worktree = { dir, admin, commondir: await readFile2(
|
|
5522
|
+
const worktree = { dir, admin, commondir: await readFile2(join6(admin, "commondir"), "utf8") };
|
|
5445
5523
|
return { base, fork: await git(worktree, ["rev-parse", "HEAD"]), worktree };
|
|
5446
5524
|
}
|
|
5447
5525
|
async function removeWorktree(clone, dir) {
|
|
@@ -5550,6 +5628,275 @@ async function reviewDeploys(client, daemonId, now = /* @__PURE__ */ new Date())
|
|
|
5550
5628
|
return results;
|
|
5551
5629
|
}
|
|
5552
5630
|
|
|
5631
|
+
// ../daemon/src/build-job.ts
|
|
5632
|
+
import { existsSync as existsSync2 } from "fs";
|
|
5633
|
+
import { mkdir as mkdir2, readdir, readFile as readFile3, writeFile as writeFile5 } from "fs/promises";
|
|
5634
|
+
import { homedir as homedir3 } from "os";
|
|
5635
|
+
import { basename, join as join7 } from "path";
|
|
5636
|
+
|
|
5637
|
+
// ../daemon/src/usage.ts
|
|
5638
|
+
function sumUsage(a, b) {
|
|
5639
|
+
if (!a) return { ...b };
|
|
5640
|
+
return {
|
|
5641
|
+
...b,
|
|
5642
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
5643
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
5644
|
+
cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
|
|
5645
|
+
cacheWriteTokens: (a.cacheWriteTokens ?? 0) + (b.cacheWriteTokens ?? 0),
|
|
5646
|
+
costUsd: (a.costUsd ?? 0) + (b.costUsd ?? 0),
|
|
5647
|
+
turns: a.turns + b.turns,
|
|
5648
|
+
durationMs: a.durationMs + b.durationMs,
|
|
5649
|
+
...a.contextPeakTokens || b.contextPeakTokens ? { contextPeakTokens: Math.max(a.contextPeakTokens ?? 0, b.contextPeakTokens ?? 0) } : {},
|
|
5650
|
+
...a.reads || b.reads ? { reads: sumReads(a.reads, b.reads) } : {}
|
|
5651
|
+
};
|
|
5652
|
+
}
|
|
5653
|
+
function sumReads(a, b) {
|
|
5654
|
+
const calls = { ...a?.calls ?? {} };
|
|
5655
|
+
for (const [tool, n] of Object.entries(b?.calls ?? {})) calls[tool] = (calls[tool] ?? 0) + n;
|
|
5656
|
+
return { files: (a?.files ?? 0) + (b?.files ?? 0), calls };
|
|
5657
|
+
}
|
|
5658
|
+
|
|
5659
|
+
// ../daemon/src/build-job.ts
|
|
5660
|
+
var PLAN_TOOL = "mcp__nanopm__build_plan";
|
|
5661
|
+
var ASK_TOOL = "mcp__nanopm__ask_user";
|
|
5662
|
+
var STEP_TOOL = "mcp__nanopm__build_step";
|
|
5663
|
+
var CLAUDE_CODE_MIN = "2.1.283";
|
|
5664
|
+
async function prepareBuild(client, job, clone, adapter) {
|
|
5665
|
+
const params = job.params ?? {};
|
|
5666
|
+
const dir = buildDir(homedir3(), job.id);
|
|
5667
|
+
let made = false;
|
|
5668
|
+
try {
|
|
5669
|
+
const [solution, machine, history] = await Promise.all([
|
|
5670
|
+
loadSolution(client, job.project_id, String(params.solution_id ?? "")),
|
|
5671
|
+
machineRefusal(adapter),
|
|
5672
|
+
loadHistory(client, job)
|
|
5673
|
+
]);
|
|
5674
|
+
const refused = buildRefusal(solution, { anyway: params.anyway === true || Boolean(history.place) }) ?? machine;
|
|
5675
|
+
if (refused) return { ok: false, outcome: failed(refused) };
|
|
5676
|
+
const network = async (at) => buildNetwork(await readdir(at));
|
|
5677
|
+
if (history.place) {
|
|
5678
|
+
const { place } = history;
|
|
5679
|
+
if (!existsSync2(place.dir)) return { ok: false, outcome: failed(`this build's copy of the code is gone (${place.dir}): build it again`) };
|
|
5680
|
+
const worktree2 = { dir: place.dir, admin: join7(await commonGitDir(clone), "worktrees", basename(place.dir)), commondir: "../..\n" };
|
|
5681
|
+
await startSolution(client, job.project_id, solution.ref, job.id);
|
|
5682
|
+
return { ok: true, build: { solution, clone, ...place, worktree: worktree2, network: await network(place.dir), history } };
|
|
5683
|
+
}
|
|
5684
|
+
const branch = buildBranch(solution.ref, solution.title);
|
|
5685
|
+
await mkdir2(join7(dir, ".."), { recursive: true, mode: 448 });
|
|
5686
|
+
const { base, fork, worktree } = await addWorktree(clone, dir, branch);
|
|
5687
|
+
made = true;
|
|
5688
|
+
await appendJobEvent(client, job.id, "worktree", { dir, branch, base, fork }, "nanopm");
|
|
5689
|
+
await handOverTheSpec(worktree, solution.agent_spec);
|
|
5690
|
+
await startSolution(client, job.project_id, solution.ref, job.id);
|
|
5691
|
+
return { ok: true, build: { solution, clone, dir, worktree, branch, base, fork, network: await network(dir), history } };
|
|
5692
|
+
} catch (err) {
|
|
5693
|
+
if (made) await removeWorktree(clone, dir);
|
|
5694
|
+
return { ok: false, outcome: failed(errorMessage(err)) };
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5697
|
+
async function runBuildJob(input) {
|
|
5698
|
+
const { client, job, adapter, spec: spec2, events, log, verifyCommand: verifyCommand2 } = input;
|
|
5699
|
+
const { solution, clone, dir, worktree, branch, base, fork, history } = input.build;
|
|
5700
|
+
const was = wasOf(job.params);
|
|
5701
|
+
let plan = history.plan;
|
|
5702
|
+
const done = new Set(history.done);
|
|
5703
|
+
let usage;
|
|
5704
|
+
let said = "";
|
|
5705
|
+
const stop2 = async (message, opts = { keep: true }) => {
|
|
5706
|
+
await unstartSolution(client, job.project_id, solution.ref, was).catch(() => void 0);
|
|
5707
|
+
if (opts.keep) await commitAll(worktree, `${solution.ref}: unfinished \u2014 ${solution.title}`).catch(() => null);
|
|
5708
|
+
else await removeWorktree(clone, dir);
|
|
5709
|
+
return { ...failed(opts.keep ? `${message}. What was done is on ${branch}, in ${dir}.` : message), usage };
|
|
5710
|
+
};
|
|
5711
|
+
const park = async (why) => {
|
|
5712
|
+
if ("ask" in why) {
|
|
5713
|
+
await events.write("question", { ...why.ask, ...plan ? {} : { about: "plan" } });
|
|
5714
|
+
await log("build.ask", `${solution.ref} waits on the founder`);
|
|
5715
|
+
} else if (why.resetsAt) {
|
|
5716
|
+
await events.write("paused", { why: "limit", resets_at: new Date(why.resetsAt * 1e3).toISOString() });
|
|
5717
|
+
await log("build.pause", `${solution.ref} paused until the Claude limit resets`);
|
|
5718
|
+
} else {
|
|
5719
|
+
await events.write("question", { question: "Your Claude usage is used up. Get more in Claude, or wait for it to come back, then answer to carry on.", options: ["Carry on"], about: "usage" });
|
|
5720
|
+
}
|
|
5721
|
+
return { status: "waiting", usage };
|
|
5722
|
+
};
|
|
5723
|
+
const session2 = async (prompt, resume3) => {
|
|
5724
|
+
let end = { stop: failed("the agent produced no result") };
|
|
5725
|
+
for await (const event of adapter.run({ ...spec2, prompt, resumeSessionId: resume3 ? spec2.sessionId : void 0 })) {
|
|
5726
|
+
await input.onEvent(event);
|
|
5727
|
+
if (event.type === "tool_use" && event.tool === PLAN_TOOL && !plan) {
|
|
5728
|
+
plan = stepsOf(event.input);
|
|
5729
|
+
if (plan) await events.write("plan", { steps: plan });
|
|
5730
|
+
} else if (event.type === "tool_result" && event.tool === STEP_TOOL && !event.isError) {
|
|
5731
|
+
const { step, summary } = event.input ?? {};
|
|
5732
|
+
const index = Number(step) - 1;
|
|
5733
|
+
if (Number.isInteger(index) && index >= 0 && !done.has(index)) {
|
|
5734
|
+
const says = plan?.[index] ?? `step ${index + 1}`;
|
|
5735
|
+
const commit = await commitAll(worktree, `${solution.ref}: ${says}`, summary);
|
|
5736
|
+
done.add(index);
|
|
5737
|
+
await events.write("step", { step: index + 1, says, commit: commit?.sha ?? null, files: commit?.files ?? 0 });
|
|
5738
|
+
}
|
|
5739
|
+
} else if (event.type === "finished") {
|
|
5740
|
+
usage = sumUsage(usage, event.usage);
|
|
5741
|
+
said = event.result;
|
|
5742
|
+
end = null;
|
|
5743
|
+
} else if (event.type === "deferred" && event.tool === ASK_TOOL) {
|
|
5744
|
+
usage = sumUsage(usage, event.usage);
|
|
5745
|
+
end = { park: { ask: askOf(event.input) } };
|
|
5746
|
+
} else if (event.type === "failed") {
|
|
5747
|
+
end = event.code === "rate_limit" ? { park: { resetsAt: event.resetsAt } } : { stop: { status: "failed", error: { code: event.code, message: event.message } } };
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5750
|
+
return end;
|
|
5751
|
+
};
|
|
5752
|
+
const resume2 = history.resume;
|
|
5753
|
+
const check = verifyCommand2 ? `
|
|
5754
|
+
|
|
5755
|
+
The project's own check is \`${verifyCommand2}\`. Run it before your last build_step, fix what it reports, and give its result under *How I checked it*.` : "";
|
|
5756
|
+
const finished = () => Boolean(plan && !stepsLeft(plan, done).length);
|
|
5757
|
+
if (!resume2) {
|
|
5758
|
+
await log("build.start", `building ${solution.ref} on ${branch}, in a copy of your code`);
|
|
5759
|
+
await events.write("text", { text: `Working on ${branch}, in a copy of your code. Your own checkout is not touched.` });
|
|
5760
|
+
} else {
|
|
5761
|
+
await events.write("text", { text: AGAIN[resume2.why].said });
|
|
5762
|
+
if (resume2.why === "answer" && spec2.answerFile) await writeFile5(spec2.answerFile, answerForAgent(resume2.answer), { mode: 384 });
|
|
5763
|
+
}
|
|
5764
|
+
let turn = !resume2 ? await session2(spec2.prompt + check, false) : resume2.why === "answer" ? await session2("", true) : finished() ? null : await session2(`${AGAIN[resume2.why].lead} ${carryOn(plan, done)}`, true);
|
|
5765
|
+
for (let resumes = 0; !turn && !finished(); resumes += 1) {
|
|
5766
|
+
if (resumes >= BUILD_RESUMES_MAX) {
|
|
5767
|
+
const left = stepsLeft(plan, done).length;
|
|
5768
|
+
return stop2(plan ? `Nano stopped with ${left} step${left === 1 ? "" : "s"} of its plan left` : "Nano stopped without saying its plan");
|
|
5769
|
+
}
|
|
5770
|
+
turn = await session2(carryOn(plan, done), true);
|
|
5771
|
+
}
|
|
5772
|
+
if (turn && "park" in turn) return park(turn.park);
|
|
5773
|
+
if (turn) return stop2(turn.stop.error?.message ?? "the build stopped");
|
|
5774
|
+
await commitAll(worktree, `${solution.ref}: ${solution.title}`);
|
|
5775
|
+
if (!await commitsSinceFork(worktree, fork)) return stop2("the build finished without changing anything", { keep: false });
|
|
5776
|
+
const rebased = await rebaseOnto(worktree, base);
|
|
5777
|
+
if (!rebased.ok) return stop2(`${base} moved while Nano was building, and the same files changed on both sides: ${rebased.files.join(", ") || "see git status"}`);
|
|
5778
|
+
const [pushed, noPr, page] = await Promise.all([pushBranch(worktree, branch), ghReady(worktree), solutionPage(client, job.project_id, solution.ref)]);
|
|
5779
|
+
if (!pushed.pushed) return stop2(`the push failed: ${pushed.reason}`);
|
|
5780
|
+
const pr = noPr ? null : await openPullRequest(worktree, { branch, base, title: `${solution.ref}: ${solution.title}`, body: prBody(said, page) });
|
|
5781
|
+
if (pr && !pr.ok) return { ...failed(`the branch ${branch} is pushed, but GitHub refused the pull request: ${pr.reason}${pushed.reviewUrl ? `. Open it yourself: ${pushed.reviewUrl}` : ""}`), usage };
|
|
5782
|
+
if (pr) {
|
|
5783
|
+
await events.write("pr", { number: pr.number, url: pr.url });
|
|
5784
|
+
await log("build.pr", `draft pull request #${pr.number} for ${solution.ref}`);
|
|
5785
|
+
}
|
|
5786
|
+
await removeWorktree(clone, dir);
|
|
5787
|
+
return {
|
|
5788
|
+
status: "succeeded",
|
|
5789
|
+
result: [
|
|
5790
|
+
said.trim(),
|
|
5791
|
+
"",
|
|
5792
|
+
pr ? `Draft pull request #${pr.number} \xB7 ${pr.url}` : `Pushed ${branch}. No pull request: ${noPr}.${pushed.reviewUrl ? ` ${pushed.reviewUrl}` : ""}`
|
|
5793
|
+
].filter(Boolean).join("\n"),
|
|
5794
|
+
data: { branch, session: spec2.sessionId, ...pr ? { pr: { number: pr.number, url: pr.url } } : { review_url: pushed.reviewUrl ?? null } },
|
|
5795
|
+
usage
|
|
5796
|
+
};
|
|
5797
|
+
}
|
|
5798
|
+
var failed = (message) => ({ status: "failed", error: { code: "harness_error", message } });
|
|
5799
|
+
var AGAIN = {
|
|
5800
|
+
answer: { said: "Your answer is in: carrying on.", lead: "" },
|
|
5801
|
+
limit: { said: "Your Claude limit has reset: carrying on.", lead: "Your Claude limit has reset." },
|
|
5802
|
+
lost: { said: "Picked up again where it stopped.", lead: "You were interrupted: the machine stopped." }
|
|
5803
|
+
};
|
|
5804
|
+
function askOf(input) {
|
|
5805
|
+
const { question, options, why } = input ?? {};
|
|
5806
|
+
return {
|
|
5807
|
+
question: String(question ?? ""),
|
|
5808
|
+
...Array.isArray(options) ? { options: options.map(String) } : {},
|
|
5809
|
+
...typeof why === "string" ? { why } : {}
|
|
5810
|
+
};
|
|
5811
|
+
}
|
|
5812
|
+
async function loadHistory(client, job) {
|
|
5813
|
+
const named = (job.params ?? {}).origin;
|
|
5814
|
+
const origin = typeof named === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(named) ? named : job.id;
|
|
5815
|
+
const { data: runs } = await client.from("jobs").select("id").eq("project_id", job.project_id).eq("skill", BUILD_SKILL).or(`id.eq.${origin},params->>origin.eq.${origin}`);
|
|
5816
|
+
return loadBuildHistory(client, [.../* @__PURE__ */ new Set([origin, job.id, ...(runs ?? []).map((r) => r.id)])], { home: homedir3(), jobId: origin });
|
|
5817
|
+
}
|
|
5818
|
+
async function loadSolution(client, projectId, id) {
|
|
5819
|
+
const { data, error } = await client.from("solutions").select(BUILD_COLUMNS).eq("id", id).eq("project_id", projectId).is("superseded_by", null).maybeSingle();
|
|
5820
|
+
if (error) throw new Error(`could not read the solution: ${error.message}`);
|
|
5821
|
+
if (!data) throw new Error("that solution is not on this project any more");
|
|
5822
|
+
return data;
|
|
5823
|
+
}
|
|
5824
|
+
async function machineRefusal(adapter) {
|
|
5825
|
+
const found = await adapter.detect();
|
|
5826
|
+
const version = /\d+\.\d+\.\d+/.exec(found.version ?? "")?.[0];
|
|
5827
|
+
if (version && !newer(CLAUDE_CODE_MIN, version)) return null;
|
|
5828
|
+
return `a build needs Claude Code ${CLAUDE_CODE_MIN} or later on this machine${found.version ? ` (it has ${found.version})` : ""}: run claude update`;
|
|
5829
|
+
}
|
|
5830
|
+
async function handOverTheSpec(worktree, agentSpec) {
|
|
5831
|
+
await mkdir2(join7(worktree.dir, ".nanopm"), { recursive: true });
|
|
5832
|
+
await writeFile5(join7(worktree.dir, ".nanopm", "SPEC.md"), agentSpec);
|
|
5833
|
+
const exclude = join7(await commonGitDir(worktree), "info", "exclude");
|
|
5834
|
+
const current = await readFile3(exclude, "utf8").catch(() => "");
|
|
5835
|
+
if (!current.split("\n").includes(".nanopm/")) {
|
|
5836
|
+
await mkdir2(join7(exclude, ".."), { recursive: true });
|
|
5837
|
+
await writeFile5(exclude, `${current}${current && !current.endsWith("\n") ? "\n" : ""}.nanopm/
|
|
5838
|
+
`);
|
|
5839
|
+
}
|
|
5840
|
+
}
|
|
5841
|
+
function stepsOf(input) {
|
|
5842
|
+
const steps = input?.steps;
|
|
5843
|
+
return Array.isArray(steps) && steps.length && steps.every((s) => typeof s === "string") ? steps : null;
|
|
5844
|
+
}
|
|
5845
|
+
function carryOn(plan, done) {
|
|
5846
|
+
if (!plan) return "You stopped before saying your plan. Call build_plan with it now, then build it step by step, calling build_step after each.";
|
|
5847
|
+
const left = stepsLeft(plan, done);
|
|
5848
|
+
return [
|
|
5849
|
+
`Steps ${left.map((i) => i + 1).join(", ")} of your plan are not done yet:`,
|
|
5850
|
+
...left.map((i) => `${i + 1}. ${plan[i]}`),
|
|
5851
|
+
"",
|
|
5852
|
+
"Check git log and git status to see where you are, then carry on: finish each step, run its checks, and call build_step after it. Do not run anything in the background."
|
|
5853
|
+
].join("\n");
|
|
5854
|
+
}
|
|
5855
|
+
async function solutionPage(client, projectId, ref) {
|
|
5856
|
+
const { data } = await client.from("projects").select("slug").eq("id", projectId).maybeSingle();
|
|
5857
|
+
return data?.slug ? `${webUrl()}/p/${data.slug}/roadmap/${ref}` : null;
|
|
5858
|
+
}
|
|
5859
|
+
function prBody(said, page) {
|
|
5860
|
+
return [
|
|
5861
|
+
said.trim(),
|
|
5862
|
+
page ? `
|
|
5863
|
+
The solution and its spec: ${page}` : "",
|
|
5864
|
+
"\n\u2014 Built by NanoPM with Claude Code. A draft: review before merging."
|
|
5865
|
+
].filter(Boolean).join("\n");
|
|
5866
|
+
}
|
|
5867
|
+
async function startSolution(client, projectId, ref, jobId) {
|
|
5868
|
+
const { error } = await client.rpc("start_solution", { p_project_id: projectId, p_ref: ref, p_job_id: jobId });
|
|
5869
|
+
if (error) throw new Error(pgMessage(error));
|
|
5870
|
+
}
|
|
5871
|
+
async function unstartSolution(client, projectId, ref, was) {
|
|
5872
|
+
await client.rpc("unstart_solution", { p_project_id: projectId, p_ref: ref, p_to: was });
|
|
5873
|
+
}
|
|
5874
|
+
async function requeueLostBuild(client, job) {
|
|
5875
|
+
const params = job.params ?? {};
|
|
5876
|
+
if ((job.attempt ?? 1) < 2) {
|
|
5877
|
+
const { data } = await client.from("jobs").update({ status: "queued", attempt: (job.attempt ?? 1) + 1, assigned_daemon_id: null }).eq("id", job.id).eq("status", "running").select("id");
|
|
5878
|
+
if (data?.length) return true;
|
|
5879
|
+
}
|
|
5880
|
+
const { data: solution } = await client.from("solutions").select("ref").eq("id", params.solution_id ?? "").maybeSingle();
|
|
5881
|
+
if (job.project_id && solution) await unstartSolution(client, job.project_id, solution.ref, wasOf(job.params)).catch(() => void 0);
|
|
5882
|
+
return false;
|
|
5883
|
+
}
|
|
5884
|
+
async function resumePausedBuilds(client, daemonId, now = Date.now()) {
|
|
5885
|
+
const { data: parked } = await client.from("jobs").select("id").eq("skill", BUILD_SKILL).eq("status", "waiting").eq("assigned_daemon_id", daemonId);
|
|
5886
|
+
const ids = (parked ?? []).map((j) => j.id);
|
|
5887
|
+
if (!ids.length) return [];
|
|
5888
|
+
const { data: stops } = await client.from("job_events").select("job_id, kind, payload").in("job_id", ids).in("kind", ["paused", "question"]).order("id", { ascending: false });
|
|
5889
|
+
const last = /* @__PURE__ */ new Map();
|
|
5890
|
+
for (const e of stops ?? []) if (!last.has(e.job_id)) last.set(e.job_id, e);
|
|
5891
|
+
const due = ids.filter((id) => {
|
|
5892
|
+
const stop2 = last.get(id);
|
|
5893
|
+
return stop2?.kind === "paused" && Date.parse(String(stop2.payload?.resets_at ?? "")) <= now;
|
|
5894
|
+
});
|
|
5895
|
+
if (!due.length) return [];
|
|
5896
|
+
const { data } = await client.from("jobs").update({ status: "queued", assigned_daemon_id: null }).in("id", due).eq("status", "waiting").select("id");
|
|
5897
|
+
return (data ?? []).map((j) => j.id);
|
|
5898
|
+
}
|
|
5899
|
+
|
|
5553
5900
|
// ../daemon/src/review-preflight.ts
|
|
5554
5901
|
async function reviewWithoutData(client, job) {
|
|
5555
5902
|
if (job.skill !== "review" || !job.project_id) return null;
|
|
@@ -5579,17 +5926,17 @@ async function reviewWithoutData(client, job) {
|
|
|
5579
5926
|
}
|
|
5580
5927
|
|
|
5581
5928
|
// ../daemon/src/skills.ts
|
|
5582
|
-
import { existsSync as
|
|
5583
|
-
import { readFile as
|
|
5584
|
-
import { dirname as dirname2, join as
|
|
5929
|
+
import { existsSync as existsSync3, readdirSync } from "fs";
|
|
5930
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
5931
|
+
import { dirname as dirname2, join as join8, resolve as resolve2 } from "path";
|
|
5585
5932
|
function findSkillsDir(env = process.env) {
|
|
5586
5933
|
if (env.NANOPM_SKILLS_DIR) return env.NANOPM_SKILLS_DIR;
|
|
5587
5934
|
const candidates = [
|
|
5588
|
-
|
|
5935
|
+
join8(process.cwd(), "skills"),
|
|
5589
5936
|
...process.argv[1] ? [resolve2(dirname2(process.argv[1]), "..", "skills")] : []
|
|
5590
5937
|
];
|
|
5591
5938
|
for (const dir of candidates) {
|
|
5592
|
-
if (
|
|
5939
|
+
if (existsSync3(join8(dir, "onboard", "skill.json"))) return dir;
|
|
5593
5940
|
}
|
|
5594
5941
|
throw new Error(`Cannot find the skills directory (tried ${candidates.join(", ")}). Set NANOPM_SKILLS_DIR.`);
|
|
5595
5942
|
}
|
|
@@ -5599,9 +5946,9 @@ function skillDirs(skillsDir, env = process.env) {
|
|
|
5599
5946
|
}
|
|
5600
5947
|
async function loadSkill(name, skillsDir = findSkillsDir()) {
|
|
5601
5948
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new Error(`Invalid skill name "${name}"`);
|
|
5602
|
-
const dir = skillDirs(skillsDir).map((d) =>
|
|
5949
|
+
const dir = skillDirs(skillsDir).map((d) => join8(d, name)).find((d) => existsSync3(join8(d, "skill.json")));
|
|
5603
5950
|
if (!dir) throw new Error(`Unknown skill "${name}"`);
|
|
5604
|
-
const raw = JSON.parse(await
|
|
5951
|
+
const raw = JSON.parse(await readFile4(join8(dir, "skill.json"), "utf8"));
|
|
5605
5952
|
let manifest;
|
|
5606
5953
|
try {
|
|
5607
5954
|
manifest = parseSkillManifest(raw);
|
|
@@ -5610,7 +5957,7 @@ async function loadSkill(name, skillsDir = findSkillsDir()) {
|
|
|
5610
5957
|
}
|
|
5611
5958
|
const tainted = taintedByTheWeb(manifest);
|
|
5612
5959
|
if (tainted) throw new Error(`the "${name}" skill cannot be loaded: ${tainted}`);
|
|
5613
|
-
const body = await
|
|
5960
|
+
const body = await readFile4(join8(dir, "SKILL.md"), "utf8");
|
|
5614
5961
|
return { manifest, body, dir };
|
|
5615
5962
|
}
|
|
5616
5963
|
var NOT_WITH_READ_WEB = ["write_memory", "problems", "propose", "review", "write_repo", "run_commands"];
|
|
@@ -5645,7 +5992,7 @@ function renderPrompt(template, params) {
|
|
|
5645
5992
|
var NEEDS_THE_REQUESTERS_CLONE = ["write_repo", "run_commands"];
|
|
5646
5993
|
var NEEDS_THE_REQUESTERS_IDENTITY = /* @__PURE__ */ new Set(["want", "start"]);
|
|
5647
5994
|
async function clonelessSkills(skillsDir = findSkillsDir()) {
|
|
5648
|
-
const names = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() &&
|
|
5995
|
+
const names = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync3(join8(skillsDir, d.name, "skill.json"))).map((d) => d.name);
|
|
5649
5996
|
const out = [];
|
|
5650
5997
|
for (const name of names) {
|
|
5651
5998
|
try {
|
|
@@ -5699,30 +6046,6 @@ ${JSON.stringify(mappingOutputSchema(input.role))}`,
|
|
|
5699
6046
|
|
|
5700
6047
|
// ../daemon/src/problem-mapping.ts
|
|
5701
6048
|
import { createHash as createHash2 } from "crypto";
|
|
5702
|
-
|
|
5703
|
-
// ../daemon/src/usage.ts
|
|
5704
|
-
function sumUsage(a, b) {
|
|
5705
|
-
if (!a) return { ...b };
|
|
5706
|
-
return {
|
|
5707
|
-
...b,
|
|
5708
|
-
inputTokens: a.inputTokens + b.inputTokens,
|
|
5709
|
-
outputTokens: a.outputTokens + b.outputTokens,
|
|
5710
|
-
cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
|
|
5711
|
-
cacheWriteTokens: (a.cacheWriteTokens ?? 0) + (b.cacheWriteTokens ?? 0),
|
|
5712
|
-
costUsd: (a.costUsd ?? 0) + (b.costUsd ?? 0),
|
|
5713
|
-
turns: a.turns + b.turns,
|
|
5714
|
-
durationMs: a.durationMs + b.durationMs,
|
|
5715
|
-
...a.contextPeakTokens || b.contextPeakTokens ? { contextPeakTokens: Math.max(a.contextPeakTokens ?? 0, b.contextPeakTokens ?? 0) } : {},
|
|
5716
|
-
...a.reads || b.reads ? { reads: sumReads(a.reads, b.reads) } : {}
|
|
5717
|
-
};
|
|
5718
|
-
}
|
|
5719
|
-
function sumReads(a, b) {
|
|
5720
|
-
const calls = { ...a?.calls ?? {} };
|
|
5721
|
-
for (const [tool, n] of Object.entries(b?.calls ?? {})) calls[tool] = (calls[tool] ?? 0) + n;
|
|
5722
|
-
return { files: (a?.files ?? 0) + (b?.files ?? 0), calls };
|
|
5723
|
-
}
|
|
5724
|
-
|
|
5725
|
-
// ../daemon/src/problem-mapping.ts
|
|
5726
6049
|
var MAPPING_TURN_LIMIT = 40;
|
|
5727
6050
|
var MAPPING_TIMEOUT_MS = 20 * 6e4;
|
|
5728
6051
|
async function runProblemMapping(ports) {
|
|
@@ -5974,8 +6297,8 @@ async function askAgainIfUnanswered(attempt7, say, role7) {
|
|
|
5974
6297
|
var UNANSWERED = /structured[ _]output|returned no result/i;
|
|
5975
6298
|
|
|
5976
6299
|
// ../daemon/src/site.ts
|
|
5977
|
-
import { readFile as
|
|
5978
|
-
import { join as
|
|
6300
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
6301
|
+
import { join as join9 } from "path";
|
|
5979
6302
|
var SITE_PATHS = ["/", "/about", "/pricing"];
|
|
5980
6303
|
var PAGE_CHARS = 6e3;
|
|
5981
6304
|
var SITE_TIMEOUT_MS = 2e4;
|
|
@@ -6098,7 +6421,7 @@ function siteFromSearch(name, hits) {
|
|
|
6098
6421
|
}
|
|
6099
6422
|
async function read(repoRoot, name) {
|
|
6100
6423
|
try {
|
|
6101
|
-
const text2 = await
|
|
6424
|
+
const text2 = await readFile5(join9(repoRoot, name), "utf8");
|
|
6102
6425
|
return text2.length > 2e5 ? null : text2;
|
|
6103
6426
|
} catch {
|
|
6104
6427
|
return null;
|
|
@@ -6152,8 +6475,8 @@ async function fromReadme(repoRoot) {
|
|
|
6152
6475
|
}
|
|
6153
6476
|
|
|
6154
6477
|
// ../daemon/src/repo-docs.ts
|
|
6155
|
-
import { readFile as
|
|
6156
|
-
import { join as
|
|
6478
|
+
import { readFile as readFile6, readdir as readdir2, stat } from "fs/promises";
|
|
6479
|
+
import { join as join10, relative } from "path";
|
|
6157
6480
|
var WRITTEN = /(persona|audience|customer|user-research|research\/|icp|segment)/i;
|
|
6158
6481
|
var ABOUT_PEOPLE = /(persona|audience|customer|user-research|research\/|interview|feedback|survey|verbatim|voice|icp|segment)/i;
|
|
6159
6482
|
var DOC = /\.(md|mdx|txt)$/i;
|
|
@@ -6166,12 +6489,12 @@ async function readDocs(cwd, opts = {}) {
|
|
|
6166
6489
|
if (depth > 3 || found.length >= DOCS_MAX * 4) return;
|
|
6167
6490
|
let entries;
|
|
6168
6491
|
try {
|
|
6169
|
-
entries = await
|
|
6492
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
6170
6493
|
} catch {
|
|
6171
6494
|
return;
|
|
6172
6495
|
}
|
|
6173
6496
|
for (const entry of entries) {
|
|
6174
|
-
const full =
|
|
6497
|
+
const full = join10(dir, entry.name);
|
|
6175
6498
|
if (SKIP.test(`/${relative(cwd, full)}`)) continue;
|
|
6176
6499
|
if (entry.isDirectory()) await walk(full, depth + 1);
|
|
6177
6500
|
else if (DOC.test(entry.name) && (!opts.onlyAboutPeople || ABOUT_PEOPLE.test(relative(cwd, full)))) found.push(full);
|
|
@@ -6183,7 +6506,7 @@ async function readDocs(cwd, opts = {}) {
|
|
|
6183
6506
|
for (const path of found.slice(0, DOCS_MAX)) {
|
|
6184
6507
|
try {
|
|
6185
6508
|
if ((await stat(path)).size > 4e5) continue;
|
|
6186
|
-
out.push({ path: relative(cwd, path), text: (await
|
|
6509
|
+
out.push({ path: relative(cwd, path), text: (await readFile6(path, "utf8")).slice(0, DOC_CHARS) });
|
|
6187
6510
|
} catch {
|
|
6188
6511
|
}
|
|
6189
6512
|
}
|
|
@@ -7985,8 +8308,8 @@ ${body}
|
|
|
7985
8308
|
|
|
7986
8309
|
// ../daemon/src/crew.ts
|
|
7987
8310
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
7988
|
-
import { mkdir as
|
|
7989
|
-
import { join as
|
|
8311
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
8312
|
+
import { join as join11 } from "path";
|
|
7990
8313
|
var READ_ONLY = {
|
|
7991
8314
|
read_repo: /^(Read|Glob|Grep)$/,
|
|
7992
8315
|
read_data: /(^|__)(list_connections|describe_source|query)$/
|
|
@@ -8001,8 +8324,8 @@ async function attempt6({ adapter, spec: spec2, onEvent, skill: skillName, name,
|
|
|
8001
8324
|
if (skill.manifest.capabilities.some((c) => !holds.includes(c))) throw new Error(`The ${name} may not hold tools.`);
|
|
8002
8325
|
const refused = grant.find((c) => !READ_ONLY[c] || !skill.manifest.capabilities.includes(c));
|
|
8003
8326
|
if (refused) throw new Error(`The ${name} may not hold ${refused}.`);
|
|
8004
|
-
const runDir = dir ?
|
|
8005
|
-
if (dir) await
|
|
8327
|
+
const runDir = dir ? join11(spec2.runDir, dir) : spec2.runDir;
|
|
8328
|
+
if (dir) await mkdir3(runDir, { recursive: true, mode: 448 });
|
|
8006
8329
|
let result;
|
|
8007
8330
|
let usage = null;
|
|
8008
8331
|
await onEvent({ type: "text", text: say });
|
|
@@ -10059,9 +10382,9 @@ var told = (r) => ({
|
|
|
10059
10382
|
// ../daemon/src/objective-definitions.ts
|
|
10060
10383
|
import { execFile as execFile6 } from "child_process";
|
|
10061
10384
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
10062
|
-
import { existsSync as
|
|
10063
|
-
import { mkdir as
|
|
10064
|
-
import { join as
|
|
10385
|
+
import { existsSync as existsSync4 } from "fs";
|
|
10386
|
+
import { mkdir as mkdir4, readFile as readFile7 } from "fs/promises";
|
|
10387
|
+
import { join as join12 } from "path";
|
|
10065
10388
|
import { promisify as promisify6 } from "util";
|
|
10066
10389
|
var execFileAsync6 = promisify6(execFile6);
|
|
10067
10390
|
var DEFINITIONS_TIMEOUT_MS = 9e4;
|
|
@@ -10097,7 +10420,7 @@ function pickLines(raw) {
|
|
|
10097
10420
|
}
|
|
10098
10421
|
async function killRecorded(runDir) {
|
|
10099
10422
|
try {
|
|
10100
|
-
const pid = Number((await
|
|
10423
|
+
const pid = Number((await readFile7(join12(runDir, "harness.pid"), "utf8")).trim());
|
|
10101
10424
|
if (pid > 1) process.kill(pid, "SIGKILL");
|
|
10102
10425
|
} catch {
|
|
10103
10426
|
}
|
|
@@ -10117,8 +10440,8 @@ async function readDefinitions({ adapter, spec: spec2, publish: publish5, journa
|
|
|
10117
10440
|
if (!hasRepo) return give({}, "No repository to read: the card offers the common definitions.", "ok");
|
|
10118
10441
|
try {
|
|
10119
10442
|
const skill = await loadSkill("define-objective");
|
|
10120
|
-
const runDir =
|
|
10121
|
-
await
|
|
10443
|
+
const runDir = join12(spec2.runDir, "definitions");
|
|
10444
|
+
await mkdir4(runDir, { recursive: true, mode: 448 });
|
|
10122
10445
|
const schema = definitionsOutputSchema();
|
|
10123
10446
|
const [lines, listed] = await Promise.all([countingLines(spec2.cwd), listTrackedFiles(spec2.cwd)]);
|
|
10124
10447
|
const signal = AbortSignal.any([spec2.signal, AbortSignal.timeout(timeoutMs)]);
|
|
@@ -10179,7 +10502,7 @@ ${renderTrackedFiles(listed.files, void 0, listed.wiki)}
|
|
|
10179
10502
|
if (!result) throw new Error("no answer");
|
|
10180
10503
|
const parsed = DefinitionsAnswer.safeParse(JSON.parse(result));
|
|
10181
10504
|
if (!parsed.success) throw new Error("an answer in the wrong shape");
|
|
10182
|
-
const checked = checkProposals(parsed.data, (file) =>
|
|
10505
|
+
const checked = checkProposals(parsed.data, (file) => existsSync4(join12(spec2.cwd, file)));
|
|
10183
10506
|
const definitions2 = lever ? checked[lever] ? { [lever]: checked[lever] } : {} : checked;
|
|
10184
10507
|
const kept = Object.values(definitions2).reduce((n, list) => n + (list?.length ?? 0), 0);
|
|
10185
10508
|
const read6 = Object.values(definitions2).flat().filter((d) => d?.found).length;
|
|
@@ -10452,10 +10775,10 @@ function localDate(timezone, at) {
|
|
|
10452
10775
|
|
|
10453
10776
|
// ../daemon/src/run-job.ts
|
|
10454
10777
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
10455
|
-
import { existsSync as
|
|
10778
|
+
import { existsSync as existsSync5 } from "fs";
|
|
10456
10779
|
import { mkdir as mkdir5, readFile as readFile8, rm as rm4 } from "fs/promises";
|
|
10457
10780
|
import { homedir as homedir4, tmpdir } from "os";
|
|
10458
|
-
import { join as
|
|
10781
|
+
import { join as join13, resolve as resolve4 } from "path";
|
|
10459
10782
|
|
|
10460
10783
|
// ../daemon/src/events.ts
|
|
10461
10784
|
var JobEventWriter = class {
|
|
@@ -10535,168 +10858,6 @@ function lighten(value) {
|
|
|
10535
10858
|
return value ?? null;
|
|
10536
10859
|
}
|
|
10537
10860
|
|
|
10538
|
-
// ../daemon/src/build-job.ts
|
|
10539
|
-
import { mkdir as mkdir4, readdir as readdir2, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
|
|
10540
|
-
import { homedir as homedir3 } from "os";
|
|
10541
|
-
import { join as join11 } from "path";
|
|
10542
|
-
var PLAN_TOOL = "mcp__nanopm__build_plan";
|
|
10543
|
-
var STEP_TOOL = "mcp__nanopm__build_step";
|
|
10544
|
-
var CLAUDE_CODE_MIN = "2.1.283";
|
|
10545
|
-
async function prepareBuild(client, job, clone, adapter) {
|
|
10546
|
-
const params = job.params ?? {};
|
|
10547
|
-
const dir = buildDir(homedir3(), job.id);
|
|
10548
|
-
let made = false;
|
|
10549
|
-
try {
|
|
10550
|
-
const [solution, machine] = await Promise.all([loadSolution(client, job.project_id, String(params.solution_id ?? "")), machineRefusal(adapter)]);
|
|
10551
|
-
const refused = buildRefusal(solution, { anyway: params.anyway === true }) ?? machine;
|
|
10552
|
-
if (refused) return { ok: false, outcome: failed(refused) };
|
|
10553
|
-
const branch = buildBranch(solution.ref, solution.title);
|
|
10554
|
-
await mkdir4(join11(dir, ".."), { recursive: true, mode: 448 });
|
|
10555
|
-
const { base, fork, worktree } = await addWorktree(clone, dir, branch);
|
|
10556
|
-
made = true;
|
|
10557
|
-
await handOverTheSpec(worktree, solution.agent_spec);
|
|
10558
|
-
await startSolution(client, job.project_id, solution.ref, job.id);
|
|
10559
|
-
return { ok: true, build: { solution, clone, dir, worktree, branch, base, fork, network: buildNetwork(await readdir2(dir)) } };
|
|
10560
|
-
} catch (err) {
|
|
10561
|
-
if (made) await removeWorktree(clone, dir);
|
|
10562
|
-
return { ok: false, outcome: failed(errorMessage(err)) };
|
|
10563
|
-
}
|
|
10564
|
-
}
|
|
10565
|
-
async function runBuildJob(input) {
|
|
10566
|
-
const { client, job, adapter, spec: spec2, events, log, verifyCommand: verifyCommand2 } = input;
|
|
10567
|
-
const { solution, clone, dir, worktree, branch, base, fork } = input.build;
|
|
10568
|
-
await log("build.start", `building ${solution.ref} on ${branch}, in a copy of your code`);
|
|
10569
|
-
await events.write("text", { text: `Working on ${branch}, in a copy of your code. Your own checkout is not touched.` });
|
|
10570
|
-
let plan = null;
|
|
10571
|
-
const done = /* @__PURE__ */ new Set();
|
|
10572
|
-
let usage;
|
|
10573
|
-
let said = "";
|
|
10574
|
-
const stop2 = async (message, opts = { keep: true }) => {
|
|
10575
|
-
await unstartSolution(client, job.project_id, solution.ref, solution.status).catch(() => void 0);
|
|
10576
|
-
if (opts.keep) await commitAll(worktree, `${solution.ref}: unfinished \u2014 ${solution.title}`).catch(() => null);
|
|
10577
|
-
else await removeWorktree(clone, dir);
|
|
10578
|
-
return { ...failed(opts.keep ? `${message}. What was done is on ${branch}, in ${dir}.` : message), usage };
|
|
10579
|
-
};
|
|
10580
|
-
const session2 = async (prompt, resume2) => {
|
|
10581
|
-
let end;
|
|
10582
|
-
for await (const event of adapter.run({ ...spec2, prompt, resumeSessionId: resume2 ? spec2.sessionId : void 0 })) {
|
|
10583
|
-
await input.onEvent(event);
|
|
10584
|
-
if (event.type === "tool_use" && event.tool === PLAN_TOOL && !plan) {
|
|
10585
|
-
plan = stepsOf(event.input);
|
|
10586
|
-
if (plan) await events.write("plan", { steps: plan });
|
|
10587
|
-
} else if (event.type === "tool_result" && event.tool === STEP_TOOL && !event.isError) {
|
|
10588
|
-
const { step, summary } = event.input ?? {};
|
|
10589
|
-
const index = Number(step) - 1;
|
|
10590
|
-
if (Number.isInteger(index) && index >= 0 && !done.has(index)) {
|
|
10591
|
-
const says = plan?.[index] ?? `step ${index + 1}`;
|
|
10592
|
-
const commit = await commitAll(worktree, `${solution.ref}: ${says}`, summary);
|
|
10593
|
-
done.add(index);
|
|
10594
|
-
await events.write("step", { step: index + 1, says, commit: commit?.sha ?? null, files: commit?.files ?? 0 });
|
|
10595
|
-
}
|
|
10596
|
-
} else if (event.type === "finished") {
|
|
10597
|
-
usage = sumUsage(usage, event.usage);
|
|
10598
|
-
said = event.result;
|
|
10599
|
-
end = { status: "succeeded" };
|
|
10600
|
-
} else if (event.type === "failed") {
|
|
10601
|
-
end = { status: "failed", error: { code: event.code, message: event.message } };
|
|
10602
|
-
}
|
|
10603
|
-
}
|
|
10604
|
-
return end?.status === "succeeded" ? null : end ?? failed("the agent produced no result");
|
|
10605
|
-
};
|
|
10606
|
-
const check = verifyCommand2 ? `
|
|
10607
|
-
|
|
10608
|
-
The project's own check is \`${verifyCommand2}\`. Run it before your last build_step, fix what it reports, and give its result under *How I checked it*.` : "";
|
|
10609
|
-
let ended = await session2(spec2.prompt + check, false);
|
|
10610
|
-
for (let resumes = 0; !ended && !(plan && !stepsLeft(plan, done).length); resumes += 1) {
|
|
10611
|
-
if (resumes >= BUILD_RESUMES_MAX) {
|
|
10612
|
-
const left = stepsLeft(plan, done).length;
|
|
10613
|
-
return stop2(plan ? `Nano stopped with ${left} step${left === 1 ? "" : "s"} of its plan left` : "Nano stopped without saying its plan");
|
|
10614
|
-
}
|
|
10615
|
-
ended = await session2(carryOn(plan, done), true);
|
|
10616
|
-
}
|
|
10617
|
-
if (ended) return stop2(ended.error?.message ?? "the build stopped");
|
|
10618
|
-
await commitAll(worktree, `${solution.ref}: ${solution.title}`);
|
|
10619
|
-
if (!await commitsSinceFork(worktree, fork)) return stop2("the build finished without changing anything", { keep: false });
|
|
10620
|
-
const rebased = await rebaseOnto(worktree, base);
|
|
10621
|
-
if (!rebased.ok) return stop2(`${base} moved while Nano was building, and the same files changed on both sides: ${rebased.files.join(", ") || "see git status"}`);
|
|
10622
|
-
const [pushed, noPr, page] = await Promise.all([pushBranch(worktree, branch), ghReady(worktree), solutionPage(client, job.project_id, solution.ref)]);
|
|
10623
|
-
if (!pushed.pushed) return stop2(`the push failed: ${pushed.reason}`);
|
|
10624
|
-
const pr = noPr ? null : await openPullRequest(worktree, { branch, base, title: `${solution.ref}: ${solution.title}`, body: prBody(said, page) });
|
|
10625
|
-
if (pr && !pr.ok) return { ...failed(`the branch ${branch} is pushed, but GitHub refused the pull request: ${pr.reason}${pushed.reviewUrl ? `. Open it yourself: ${pushed.reviewUrl}` : ""}`), usage };
|
|
10626
|
-
if (pr) {
|
|
10627
|
-
await events.write("pr", { number: pr.number, url: pr.url });
|
|
10628
|
-
await log("build.pr", `draft pull request #${pr.number} for ${solution.ref}`);
|
|
10629
|
-
}
|
|
10630
|
-
await removeWorktree(clone, dir);
|
|
10631
|
-
return {
|
|
10632
|
-
status: "succeeded",
|
|
10633
|
-
result: [
|
|
10634
|
-
said.trim(),
|
|
10635
|
-
"",
|
|
10636
|
-
pr ? `Draft pull request #${pr.number} \xB7 ${pr.url}` : `Pushed ${branch}. No pull request: ${noPr}.${pushed.reviewUrl ? ` ${pushed.reviewUrl}` : ""}`
|
|
10637
|
-
].filter(Boolean).join("\n"),
|
|
10638
|
-
data: { branch, session: spec2.sessionId, ...pr ? { pr: { number: pr.number, url: pr.url } } : { review_url: pushed.reviewUrl ?? null } },
|
|
10639
|
-
usage
|
|
10640
|
-
};
|
|
10641
|
-
}
|
|
10642
|
-
var failed = (message) => ({ status: "failed", error: { code: "harness_error", message } });
|
|
10643
|
-
async function loadSolution(client, projectId, id) {
|
|
10644
|
-
const { data, error } = await client.from("solutions").select(BUILD_COLUMNS).eq("id", id).eq("project_id", projectId).is("superseded_by", null).maybeSingle();
|
|
10645
|
-
if (error) throw new Error(`could not read the solution: ${error.message}`);
|
|
10646
|
-
if (!data) throw new Error("that solution is not on this project any more");
|
|
10647
|
-
return data;
|
|
10648
|
-
}
|
|
10649
|
-
async function machineRefusal(adapter) {
|
|
10650
|
-
const found = await adapter.detect();
|
|
10651
|
-
const version = /\d+\.\d+\.\d+/.exec(found.version ?? "")?.[0];
|
|
10652
|
-
if (version && !newer(CLAUDE_CODE_MIN, version)) return null;
|
|
10653
|
-
return `a build needs Claude Code ${CLAUDE_CODE_MIN} or later on this machine${found.version ? ` (it has ${found.version})` : ""}: run claude update`;
|
|
10654
|
-
}
|
|
10655
|
-
async function handOverTheSpec(worktree, agentSpec) {
|
|
10656
|
-
await mkdir4(join11(worktree.dir, ".nanopm"), { recursive: true });
|
|
10657
|
-
await writeFile5(join11(worktree.dir, ".nanopm", "SPEC.md"), agentSpec);
|
|
10658
|
-
const exclude = join11(await commonGitDir(worktree), "info", "exclude");
|
|
10659
|
-
const current = await readFile7(exclude, "utf8").catch(() => "");
|
|
10660
|
-
if (!current.split("\n").includes(".nanopm/")) {
|
|
10661
|
-
await mkdir4(join11(exclude, ".."), { recursive: true });
|
|
10662
|
-
await writeFile5(exclude, `${current}${current && !current.endsWith("\n") ? "\n" : ""}.nanopm/
|
|
10663
|
-
`);
|
|
10664
|
-
}
|
|
10665
|
-
}
|
|
10666
|
-
function stepsOf(input) {
|
|
10667
|
-
const steps = input?.steps;
|
|
10668
|
-
return Array.isArray(steps) && steps.length && steps.every((s) => typeof s === "string") ? steps : null;
|
|
10669
|
-
}
|
|
10670
|
-
function carryOn(plan, done) {
|
|
10671
|
-
if (!plan) return "You stopped before saying your plan. Call build_plan with it now, then build it step by step, calling build_step after each.";
|
|
10672
|
-
const left = stepsLeft(plan, done);
|
|
10673
|
-
return [
|
|
10674
|
-
`Steps ${left.map((i) => i + 1).join(", ")} of your plan are not done yet:`,
|
|
10675
|
-
...left.map((i) => `${i + 1}. ${plan[i]}`),
|
|
10676
|
-
"",
|
|
10677
|
-
"Check git log and git status to see where you are, then carry on: finish each step, run its checks, and call build_step after it. Do not run anything in the background."
|
|
10678
|
-
].join("\n");
|
|
10679
|
-
}
|
|
10680
|
-
async function solutionPage(client, projectId, ref) {
|
|
10681
|
-
const { data } = await client.from("projects").select("slug").eq("id", projectId).maybeSingle();
|
|
10682
|
-
return data?.slug ? `${webUrl()}/p/${data.slug}/roadmap/${ref}` : null;
|
|
10683
|
-
}
|
|
10684
|
-
function prBody(said, page) {
|
|
10685
|
-
return [
|
|
10686
|
-
said.trim(),
|
|
10687
|
-
page ? `
|
|
10688
|
-
The solution and its spec: ${page}` : "",
|
|
10689
|
-
"\n\u2014 Built by NanoPM with Claude Code. A draft: review before merging."
|
|
10690
|
-
].filter(Boolean).join("\n");
|
|
10691
|
-
}
|
|
10692
|
-
async function startSolution(client, projectId, ref, jobId) {
|
|
10693
|
-
const { error } = await client.rpc("start_solution", { p_project_id: projectId, p_ref: ref, p_job_id: jobId });
|
|
10694
|
-
if (error) throw new Error(pgMessage(error));
|
|
10695
|
-
}
|
|
10696
|
-
async function unstartSolution(client, projectId, ref, was) {
|
|
10697
|
-
await client.rpc("unstart_solution", { p_project_id: projectId, p_ref: ref, p_to: was === "proposed" ? "proposed" : "accepted" });
|
|
10698
|
-
}
|
|
10699
|
-
|
|
10700
10861
|
// ../daemon/src/run-job.ts
|
|
10701
10862
|
import { hostname } from "os";
|
|
10702
10863
|
|
|
@@ -10731,11 +10892,13 @@ async function runJob(job, ctx) {
|
|
|
10731
10892
|
};
|
|
10732
10893
|
}
|
|
10733
10894
|
const durationMs = Date.now() - startedAt;
|
|
10895
|
+
const over = outcome.status !== "waiting";
|
|
10734
10896
|
await events.status(outcome.status, outcome.error ? { error: outcome.error } : crewRun(job) && outcome.result ? { text: outcome.result } : {});
|
|
10735
10897
|
await withRetry(
|
|
10736
10898
|
() => ctx.client.from("jobs").update({
|
|
10737
10899
|
status: outcome.status,
|
|
10738
|
-
|
|
10900
|
+
// A parked job is not over: it waits, with no heartbeat, for the founder or the limit.
|
|
10901
|
+
finished_at: over ? (/* @__PURE__ */ new Date()).toISOString() : null,
|
|
10739
10902
|
// A mapping run's root row belongs to SQL: checkpoints, receipt and text land there
|
|
10740
10903
|
// with the publication. A resumed job's own row is free to carry the text.
|
|
10741
10904
|
...job.skill === "problems" && !job.resume_of ? {} : { result: outcome.result !== void 0 ? { text: outcome.result, ...outcome.data } : null },
|
|
@@ -10748,7 +10911,7 @@ async function runJob(job, ctx) {
|
|
|
10748
10911
|
ctx,
|
|
10749
10912
|
job,
|
|
10750
10913
|
"job.end",
|
|
10751
|
-
outcome.status === "succeeded" ? `finished ${job.skill}` : outcome.status === "cancelled" ? `${job.skill} cancelled by the user` : `${job.skill} failed: ${outcome.error?.message}`,
|
|
10914
|
+
outcome.status === "succeeded" ? `finished ${job.skill}` : outcome.status === "waiting" ? `${job.skill} waits, with nothing running` : outcome.status === "cancelled" ? `${job.skill} cancelled by the user` : `${job.skill} failed: ${outcome.error?.message}`,
|
|
10752
10915
|
{
|
|
10753
10916
|
usage: {
|
|
10754
10917
|
tokens: usageTokens(outcome.usage),
|
|
@@ -10757,15 +10920,15 @@ async function runJob(job, ctx) {
|
|
|
10757
10920
|
...outcome.usage?.contextPeakTokens ? { context_peak: outcome.usage.contextPeakTokens } : {},
|
|
10758
10921
|
...outcome.usage?.reads ? { files_read: outcome.usage.reads.files, calls: outcome.usage.reads.calls } : {}
|
|
10759
10922
|
},
|
|
10760
|
-
outcome: outcome.status === "succeeded" ? "ok" : "error"
|
|
10923
|
+
outcome: outcome.status === "succeeded" || !over ? "ok" : "error"
|
|
10761
10924
|
}
|
|
10762
10925
|
);
|
|
10763
10926
|
return outcome;
|
|
10764
10927
|
}
|
|
10765
10928
|
async function execute(job, skill, ctx, events) {
|
|
10766
|
-
const runDir =
|
|
10929
|
+
const runDir = join13(nanopmHome(), "run", job.id);
|
|
10767
10930
|
await mkdir5(runDir, { recursive: true, mode: 448 });
|
|
10768
|
-
const resumeSessionId = job.resume_of ? await sessionOf(ctx.client, job.resume_of) : void 0;
|
|
10931
|
+
const resumeSessionId = job.resume_of ? await sessionOf(ctx.client, job.resume_of) : job.harness_session_id ?? void 0;
|
|
10769
10932
|
const sessionId = resumeSessionId ?? randomUUID12();
|
|
10770
10933
|
await ctx.client.from("jobs").update({ harness_session_id: sessionId }).eq("id", job.id);
|
|
10771
10934
|
const adapter = ctx.adapter ?? new ClaudeCodeAdapter();
|
|
@@ -10894,6 +11057,7 @@ async function execute(job, skill, ctx, events) {
|
|
|
10894
11057
|
capabilities,
|
|
10895
11058
|
harnessWeb,
|
|
10896
11059
|
network: build2?.network,
|
|
11060
|
+
answerFile: build2 ? buildAnswerFile(job.id) : void 0,
|
|
10897
11061
|
withoutTools: skill.manifest.withoutTools,
|
|
10898
11062
|
// The workspace reaches the memory server only for a job that changes files; that is
|
|
10899
11063
|
// what lets the build's tools refuse everywhere else.
|
|
@@ -11075,7 +11239,7 @@ function harnessWebFor(searchProvider, machineCanSearch) {
|
|
|
11075
11239
|
}
|
|
11076
11240
|
async function machineWebSearch() {
|
|
11077
11241
|
try {
|
|
11078
|
-
const parsed = JSON.parse(await readFile8(
|
|
11242
|
+
const parsed = JSON.parse(await readFile8(join13(nanopmHome(), "daemon.json"), "utf8"));
|
|
11079
11243
|
return parsed.webSearch === true;
|
|
11080
11244
|
} catch {
|
|
11081
11245
|
return false;
|
|
@@ -11117,7 +11281,7 @@ function buildHeader(job, project, cwd, harnessWeb = false, memory = false) {
|
|
|
11117
11281
|
return lines.join("\n");
|
|
11118
11282
|
}
|
|
11119
11283
|
function scratchDir(jobId) {
|
|
11120
|
-
return
|
|
11284
|
+
return join13(tmpdir(), `nanopm-run-${jobId}`);
|
|
11121
11285
|
}
|
|
11122
11286
|
async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
|
|
11123
11287
|
const refuse2 = (why) => {
|
|
@@ -11142,7 +11306,7 @@ async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
|
|
|
11142
11306
|
await mkdir5(fallback, { recursive: true, mode: 448 });
|
|
11143
11307
|
return fallback;
|
|
11144
11308
|
}
|
|
11145
|
-
if (
|
|
11309
|
+
if (existsSync5(allowed)) return resolve4(allowed);
|
|
11146
11310
|
if (mustBeTheClone) refuse2(`${allowed} is not on this machine any more`);
|
|
11147
11311
|
await mkdir5(fallback, { recursive: true, mode: 448 });
|
|
11148
11312
|
return fallback;
|
|
@@ -11156,7 +11320,7 @@ function harnessEnv(timeoutMs, source = process.env) {
|
|
|
11156
11320
|
const home = source.HOME ?? homedir4();
|
|
11157
11321
|
const env = {
|
|
11158
11322
|
HOME: home,
|
|
11159
|
-
PATH: [source.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
11323
|
+
PATH: [source.PATH ?? "/usr/local/bin:/usr/bin:/bin", join13(home, ".local", "bin")].join(":"),
|
|
11160
11324
|
MCP_TOOL_TIMEOUT: String(timeoutMs)
|
|
11161
11325
|
};
|
|
11162
11326
|
for (const key of ["USER", "LANG", "TMPDIR", "SHELL", "ANTHROPIC_API_KEY", "CLAUDE_CONFIG_DIR"]) {
|
|
@@ -11236,6 +11400,8 @@ async function runDaemon(options = {}) {
|
|
|
11236
11400
|
const startedAt = Date.now();
|
|
11237
11401
|
let lastDeployCheck = 0;
|
|
11238
11402
|
const DEPLOY_CHECK_MS = 30 * 6e4;
|
|
11403
|
+
const PAUSE_CHECK_MS = 6e4;
|
|
11404
|
+
let lastPauseCheck = 0;
|
|
11239
11405
|
const UPDATE_CHECK_MS = 60 * 6e4;
|
|
11240
11406
|
const updatable = !options.once && installKind() === "installer";
|
|
11241
11407
|
let lastUpdateCheck = Date.now() - UPDATE_CHECK_MS + 5 * 6e4;
|
|
@@ -11293,6 +11459,10 @@ async function runDaemon(options = {}) {
|
|
|
11293
11459
|
continue;
|
|
11294
11460
|
}
|
|
11295
11461
|
if (options.once && Date.now() - startedAt > onceTimeoutMs) break;
|
|
11462
|
+
if (!options.once && Date.now() - lastPauseCheck > PAUSE_CHECK_MS) {
|
|
11463
|
+
lastPauseCheck = Date.now();
|
|
11464
|
+
for (const id of await resumePausedBuilds(auth.client, daemonId).catch(() => [])) log(`build ${id}: the Claude limit reset, carrying on`);
|
|
11465
|
+
}
|
|
11296
11466
|
if (!options.once && Date.now() - lastDeployCheck > DEPLOY_CHECK_MS) {
|
|
11297
11467
|
lastDeployCheck = Date.now();
|
|
11298
11468
|
for (const check of await reviewDeploys(auth.client, daemonId).catch(() => [])) {
|
|
@@ -11421,7 +11591,7 @@ var heldInThisProcess = false;
|
|
|
11421
11591
|
function acquireLock() {
|
|
11422
11592
|
if (heldInThisProcess) throw new Error("a nanopm daemon is already running in this process");
|
|
11423
11593
|
mkdirSync(nanopmHome(), { recursive: true, mode: 448 });
|
|
11424
|
-
const path =
|
|
11594
|
+
const path = join14(nanopmHome(), "daemon.pid");
|
|
11425
11595
|
for (; ; ) {
|
|
11426
11596
|
try {
|
|
11427
11597
|
const fd = openSync(path, "wx");
|
|
@@ -11493,10 +11663,11 @@ async function claimNext(client, daemonId, skills) {
|
|
|
11493
11663
|
return rows2[0] ?? null;
|
|
11494
11664
|
}
|
|
11495
11665
|
async function reclaimOwnJobs(client, daemonId) {
|
|
11496
|
-
const { data: stale } = await client.from("jobs").select("id, project_id, skill").eq("assigned_daemon_id", daemonId).in("status", ["running", "waiting"]);
|
|
11666
|
+
const { data: stale } = await client.from("jobs").select("id, project_id, skill, status, attempt, params").eq("assigned_daemon_id", daemonId).in("status", ["running", "waiting"]);
|
|
11497
11667
|
const ids = [];
|
|
11498
11668
|
for (const job of stale ?? []) {
|
|
11499
11669
|
await killOrphanedHarness(job.id);
|
|
11670
|
+
if (job.skill === BUILD_SKILL && (job.status === "waiting" || await requeueLostBuild(client, job))) continue;
|
|
11500
11671
|
const error = { code: "interrupted", message: "the daemon stopped while this job was running" };
|
|
11501
11672
|
await client.from("jobs").update({ status: "failed", finished_at: (/* @__PURE__ */ new Date()).toISOString(), error }).eq("id", job.id).in("status", ["running", "waiting"]);
|
|
11502
11673
|
await client.from("job_events").insert({ job_id: job.id, seq: 0, kind: "status", payload: { status: "failed", error }, author_kind: "nanopm" });
|
|
@@ -11508,13 +11679,13 @@ async function reclaimOwnJobs(client, daemonId) {
|
|
|
11508
11679
|
async function harnessPids(runDir) {
|
|
11509
11680
|
const dirs = [runDir];
|
|
11510
11681
|
try {
|
|
11511
|
-
for (const d of await readdir3(runDir, { withFileTypes: true })) if (d.isDirectory()) dirs.push(
|
|
11682
|
+
for (const d of await readdir3(runDir, { withFileTypes: true })) if (d.isDirectory()) dirs.push(join14(runDir, d.name));
|
|
11512
11683
|
} catch {
|
|
11513
11684
|
}
|
|
11514
11685
|
const pids = [];
|
|
11515
11686
|
for (const dir of dirs) {
|
|
11516
11687
|
try {
|
|
11517
|
-
const pid = Number((await readFile9(
|
|
11688
|
+
const pid = Number((await readFile9(join14(dir, "harness.pid"), "utf8")).trim());
|
|
11518
11689
|
if (pid > 1) pids.push(pid);
|
|
11519
11690
|
} catch {
|
|
11520
11691
|
}
|
|
@@ -11522,7 +11693,7 @@ async function harnessPids(runDir) {
|
|
|
11522
11693
|
return pids;
|
|
11523
11694
|
}
|
|
11524
11695
|
async function killOrphanedHarness(jobId) {
|
|
11525
|
-
const runDir =
|
|
11696
|
+
const runDir = join14(nanopmHome(), "run", jobId);
|
|
11526
11697
|
for (const pid of await harnessPids(runDir)) {
|
|
11527
11698
|
try {
|
|
11528
11699
|
process.kill(pid, "SIGTERM");
|
|
@@ -11532,7 +11703,7 @@ async function killOrphanedHarness(jobId) {
|
|
|
11532
11703
|
await rm5(runDir, { recursive: true, force: true });
|
|
11533
11704
|
}
|
|
11534
11705
|
async function registerDaemon(auth, version, opts = {}) {
|
|
11535
|
-
const path =
|
|
11706
|
+
const path = join14(nanopmHome(), "daemon.json");
|
|
11536
11707
|
let identity = null;
|
|
11537
11708
|
try {
|
|
11538
11709
|
identity = JSON.parse(await readFile9(path, "utf8"));
|
|
@@ -11574,7 +11745,7 @@ async function registerDaemon(auth, version, opts = {}) {
|
|
|
11574
11745
|
}
|
|
11575
11746
|
async function rememberDaemon(id, shared = false) {
|
|
11576
11747
|
await mkdir6(nanopmHome(), { recursive: true, mode: 448 });
|
|
11577
|
-
await writeFile6(
|
|
11748
|
+
await writeFile6(join14(nanopmHome(), "daemon.json"), JSON.stringify({ id, shared }, null, 2) + "\n", { mode: 384 });
|
|
11578
11749
|
}
|
|
11579
11750
|
async function heartbeat(client, daemonId) {
|
|
11580
11751
|
await client.from("daemons").update({ status: "online", last_seen_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", daemonId);
|
|
@@ -11628,13 +11799,15 @@ function contextFromEnv(env = process.env) {
|
|
|
11628
11799
|
userId: env.NANOPM_USER_ID,
|
|
11629
11800
|
actor: env.NANOPM_ACTOR,
|
|
11630
11801
|
askTimeoutMs: Number(env.NANOPM_ASK_TIMEOUT_MS ?? 3 * 60 * 60 * 1e3),
|
|
11631
|
-
workspace: env.NANOPM_WORKSPACE || void 0
|
|
11802
|
+
workspace: env.NANOPM_WORKSPACE || void 0,
|
|
11803
|
+
answerFile: env.NANOPM_ANSWER_FILE || void 0
|
|
11632
11804
|
};
|
|
11633
11805
|
}
|
|
11634
11806
|
|
|
11635
11807
|
// ../memory-mcp/src/server.ts
|
|
11636
11808
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11637
11809
|
import { randomUUID as randomUUID13 } from "crypto";
|
|
11810
|
+
import { readFile as readFile10, unlink } from "fs/promises";
|
|
11638
11811
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
11639
11812
|
import { z as z21 } from "zod";
|
|
11640
11813
|
|
|
@@ -12160,6 +12333,8 @@ ${renderMap()}`,
|
|
|
12160
12333
|
}
|
|
12161
12334
|
},
|
|
12162
12335
|
async ({ question, options, why }, extra) => {
|
|
12336
|
+
const handed = ctx.answerFile ? await takeAnswer(ctx.answerFile) : null;
|
|
12337
|
+
if (handed !== null) return json({ answer: handed, skipped: false });
|
|
12163
12338
|
const answer = await askUser(ctx, { question, options, why }, extra.signal);
|
|
12164
12339
|
if (answer === null) return failure("No answer arrived in time, or the job was cancelled. Do not ask again; continue with what you have.");
|
|
12165
12340
|
return json({ answer, skipped: /^\s*(skip|passe?r?)\s*$/i.test(answer) || answer.trim() === "" });
|
|
@@ -12724,6 +12899,12 @@ async function writeContext(ctx, items, refused = []) {
|
|
|
12724
12899
|
await logActivity2(ctx, "memory.write", `wrote ${written.length} items under ${topics}: ${written.map((w) => w.key).join(", ")}${refusedNote}`);
|
|
12725
12900
|
return { written };
|
|
12726
12901
|
}
|
|
12902
|
+
async function takeAnswer(answerFile) {
|
|
12903
|
+
const taken = `${answerFile}.taken`;
|
|
12904
|
+
const answer = await readFile10(taken, "utf8").catch(() => null);
|
|
12905
|
+
if (answer !== null) await unlink(taken).catch(() => void 0);
|
|
12906
|
+
return answer;
|
|
12907
|
+
}
|
|
12727
12908
|
async function askUser(ctx, q, signal) {
|
|
12728
12909
|
const seq = await nextSeq(ctx);
|
|
12729
12910
|
const { error } = await ctx.client.from("job_events").insert({ job_id: ctx.jobId, seq, kind: "question", payload: { question: q.question, options: q.options ?? null, why: q.why ?? null }, author_kind: "nanopm" });
|
|
@@ -13509,19 +13690,19 @@ async function startMemoryServer(env = process.env) {
|
|
|
13509
13690
|
}
|
|
13510
13691
|
|
|
13511
13692
|
// src/commands/add.ts
|
|
13512
|
-
import { readFile as
|
|
13693
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
13513
13694
|
|
|
13514
13695
|
// src/project.ts
|
|
13515
13696
|
import { execFile as execFile7 } from "child_process";
|
|
13516
|
-
import { basename } from "path";
|
|
13697
|
+
import { basename as basename2 } from "path";
|
|
13517
13698
|
import { promisify as promisify7 } from "util";
|
|
13518
13699
|
|
|
13519
13700
|
// src/local.ts
|
|
13520
|
-
import { readFile as
|
|
13521
|
-
import { join as
|
|
13701
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
13702
|
+
import { join as join15 } from "path";
|
|
13522
13703
|
async function localDaemonId() {
|
|
13523
13704
|
try {
|
|
13524
|
-
const parsed = JSON.parse(await
|
|
13705
|
+
const parsed = JSON.parse(await readFile11(join15(nanopmHome(), "daemon.json"), "utf8"));
|
|
13525
13706
|
return parsed.id ?? null;
|
|
13526
13707
|
} catch {
|
|
13527
13708
|
return null;
|
|
@@ -13546,7 +13727,7 @@ async function detectRepo(cwd) {
|
|
|
13546
13727
|
} catch {
|
|
13547
13728
|
remote = null;
|
|
13548
13729
|
}
|
|
13549
|
-
const name = remote ? remote.split("/").pop() :
|
|
13730
|
+
const name = remote ? remote.split("/").pop() : basename2(root2);
|
|
13550
13731
|
return { root: root2, remote, name, empty: await repoIsEmpty(root2) };
|
|
13551
13732
|
}
|
|
13552
13733
|
async function repoIsEmpty(root2) {
|
|
@@ -13557,7 +13738,7 @@ async function repoIsEmpty(root2) {
|
|
|
13557
13738
|
}
|
|
13558
13739
|
const files = (await execFileAsync7("git", ["-C", root2, "ls-files"])).stdout.split("\n").filter(Boolean);
|
|
13559
13740
|
return files.every((f) => {
|
|
13560
|
-
const base =
|
|
13741
|
+
const base = basename2(f);
|
|
13561
13742
|
return base.startsWith(".") || /^(readme|licen[cs]e)(\.|$)/i.test(base);
|
|
13562
13743
|
});
|
|
13563
13744
|
}
|
|
@@ -13656,7 +13837,7 @@ async function add(file, opts) {
|
|
|
13656
13837
|
text2 = await readStdin();
|
|
13657
13838
|
} else {
|
|
13658
13839
|
try {
|
|
13659
|
-
text2 = await
|
|
13840
|
+
text2 = await readFile12(file, "utf8");
|
|
13660
13841
|
} catch {
|
|
13661
13842
|
throw new Error(`could not read ${file}`);
|
|
13662
13843
|
}
|
|
@@ -13671,7 +13852,7 @@ async function add(file, opts) {
|
|
|
13671
13852
|
}
|
|
13672
13853
|
|
|
13673
13854
|
// src/commands/run.ts
|
|
13674
|
-
import { readFile as
|
|
13855
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
13675
13856
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
13676
13857
|
import { setTimeout as sleep3 } from "timers/promises";
|
|
13677
13858
|
|
|
@@ -14003,7 +14184,7 @@ function questionBlock(p) {
|
|
|
14003
14184
|
|
|
14004
14185
|
// src/commands/run.ts
|
|
14005
14186
|
async function answersFromFile(path) {
|
|
14006
|
-
const raw = JSON.parse(await
|
|
14187
|
+
const raw = JSON.parse(await readFile13(path, "utf8"));
|
|
14007
14188
|
const entries = raw.map((a) => typeof a === "string" ? { answer: a } : a);
|
|
14008
14189
|
return scriptedAnswerer(entries);
|
|
14009
14190
|
}
|
|
@@ -14087,6 +14268,7 @@ async function streamJob(client, jobId, opts) {
|
|
|
14087
14268
|
const shownAnswers = /* @__PURE__ */ new Set();
|
|
14088
14269
|
for (; ; ) {
|
|
14089
14270
|
const { data: events } = await client.from("job_events").select("id, seq, kind, payload, author_kind").eq("job_id", jobId).gt("id", lastId).order("id", { ascending: true });
|
|
14271
|
+
for (const ev of events ?? []) if (ev.kind === "answer") answered.add(ev.seq);
|
|
14090
14272
|
for (const ev of events ?? []) {
|
|
14091
14273
|
lastId = Math.max(lastId, Number(ev.id));
|
|
14092
14274
|
const payload = ev.payload ?? {};
|
|
@@ -14225,16 +14407,16 @@ async function askOne(auth, projectId, question, daemonId, opts, out) {
|
|
|
14225
14407
|
|
|
14226
14408
|
// src/commands/daemon-service.ts
|
|
14227
14409
|
import { execFile as execFile8 } from "child_process";
|
|
14228
|
-
import { existsSync as
|
|
14229
|
-
import { mkdir as mkdir7, readFile as
|
|
14410
|
+
import { existsSync as existsSync6 } from "fs";
|
|
14411
|
+
import { mkdir as mkdir7, readFile as readFile14, rm as rm6, writeFile as writeFile7 } from "fs/promises";
|
|
14230
14412
|
import { homedir as homedir5 } from "os";
|
|
14231
|
-
import { join as
|
|
14413
|
+
import { join as join16 } from "path";
|
|
14232
14414
|
import { promisify as promisify8 } from "util";
|
|
14233
14415
|
|
|
14234
14416
|
// package.json
|
|
14235
14417
|
var package_default = {
|
|
14236
14418
|
name: "@nanopm/cli",
|
|
14237
|
-
version: "0.1.
|
|
14419
|
+
version: "0.1.12",
|
|
14238
14420
|
description: "An autonomous product manager for builders who run several small products. Runs on your machine, drives your own coding harness.",
|
|
14239
14421
|
type: "module",
|
|
14240
14422
|
license: "MIT",
|
|
@@ -14286,10 +14468,10 @@ var VERSION = package_default.version;
|
|
|
14286
14468
|
var execFileAsync8 = promisify8(execFile8);
|
|
14287
14469
|
var LAUNCHD_LABEL = "pm.nano.daemon";
|
|
14288
14470
|
function plistPath() {
|
|
14289
|
-
return
|
|
14471
|
+
return join16(homedir5(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
14290
14472
|
}
|
|
14291
14473
|
function daemonLogPath() {
|
|
14292
|
-
return
|
|
14474
|
+
return join16(nanopmHome(), "daemon.log");
|
|
14293
14475
|
}
|
|
14294
14476
|
function renderPlist(opts) {
|
|
14295
14477
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -14349,8 +14531,8 @@ async function installDaemon(out = (l) => console.log(l), opts = {}) {
|
|
|
14349
14531
|
const bin = process.argv[1];
|
|
14350
14532
|
if (!bin) throw new Error("cannot locate the nanopm binary");
|
|
14351
14533
|
const home = homedir5();
|
|
14352
|
-
const path = [process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
14353
|
-
await mkdir7(
|
|
14534
|
+
const path = [process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", join16(home, ".local", "bin")].join(":");
|
|
14535
|
+
await mkdir7(join16(home, "Library", "LaunchAgents"), { recursive: true });
|
|
14354
14536
|
await mkdir7(nanopmHome(), { recursive: true, mode: 448 });
|
|
14355
14537
|
if (opts.shared !== void 0) await rememberShared(opts.shared);
|
|
14356
14538
|
await found({ ...detection, authenticated: true });
|
|
@@ -14367,7 +14549,7 @@ function guiTarget() {
|
|
|
14367
14549
|
return `gui/${process.getuid?.() ?? 501}`;
|
|
14368
14550
|
}
|
|
14369
14551
|
async function restartDaemon() {
|
|
14370
|
-
if (!
|
|
14552
|
+
if (!existsSync6(plistPath())) return false;
|
|
14371
14553
|
await execFileAsync8("launchctl", ["kickstart", "-k", `${guiTarget()}/${LAUNCHD_LABEL}`]);
|
|
14372
14554
|
return true;
|
|
14373
14555
|
}
|
|
@@ -14380,7 +14562,7 @@ async function uninstallDaemon(out = (l) => console.log(l)) {
|
|
|
14380
14562
|
}
|
|
14381
14563
|
async function daemonStatus(out = (l) => console.log(l)) {
|
|
14382
14564
|
const id = await localDaemonId();
|
|
14383
|
-
const installed = process.platform === "darwin" &&
|
|
14565
|
+
const installed = process.platform === "darwin" && existsSync6(plistPath());
|
|
14384
14566
|
out(`launchd: ${installed ? "installed" : "not installed"}`);
|
|
14385
14567
|
if (!id) {
|
|
14386
14568
|
out("daemon: never registered on this machine");
|
|
@@ -14402,7 +14584,7 @@ async function daemonStatus(out = (l) => console.log(l)) {
|
|
|
14402
14584
|
}
|
|
14403
14585
|
async function daemonLogs(lines = 50) {
|
|
14404
14586
|
try {
|
|
14405
|
-
const text2 = await
|
|
14587
|
+
const text2 = await readFile14(daemonLogPath(), "utf8");
|
|
14406
14588
|
return text2.trimEnd().split("\n").slice(-lines).join("\n");
|
|
14407
14589
|
} catch {
|
|
14408
14590
|
return `no log at ${daemonLogPath()} yet`;
|
|
@@ -14410,17 +14592,17 @@ async function daemonLogs(lines = 50) {
|
|
|
14410
14592
|
}
|
|
14411
14593
|
async function localWebSearch() {
|
|
14412
14594
|
try {
|
|
14413
|
-
const parsed = JSON.parse(await
|
|
14595
|
+
const parsed = JSON.parse(await readFile14(join16(nanopmHome(), "daemon.json"), "utf8"));
|
|
14414
14596
|
return parsed.webSearch === true;
|
|
14415
14597
|
} catch {
|
|
14416
14598
|
return false;
|
|
14417
14599
|
}
|
|
14418
14600
|
}
|
|
14419
14601
|
async function rememberWebSearch(webSearch2) {
|
|
14420
|
-
const path =
|
|
14602
|
+
const path = join16(nanopmHome(), "daemon.json");
|
|
14421
14603
|
let parsed = {};
|
|
14422
14604
|
try {
|
|
14423
|
-
parsed = JSON.parse(await
|
|
14605
|
+
parsed = JSON.parse(await readFile14(path, "utf8"));
|
|
14424
14606
|
} catch {
|
|
14425
14607
|
parsed = {};
|
|
14426
14608
|
}
|
|
@@ -14429,17 +14611,17 @@ async function rememberWebSearch(webSearch2) {
|
|
|
14429
14611
|
}
|
|
14430
14612
|
async function localShared() {
|
|
14431
14613
|
try {
|
|
14432
|
-
const parsed = JSON.parse(await
|
|
14614
|
+
const parsed = JSON.parse(await readFile14(join16(nanopmHome(), "daemon.json"), "utf8"));
|
|
14433
14615
|
return Boolean(parsed.shared);
|
|
14434
14616
|
} catch {
|
|
14435
14617
|
return false;
|
|
14436
14618
|
}
|
|
14437
14619
|
}
|
|
14438
14620
|
async function rememberShared(shared) {
|
|
14439
|
-
const path =
|
|
14621
|
+
const path = join16(nanopmHome(), "daemon.json");
|
|
14440
14622
|
let parsed = {};
|
|
14441
14623
|
try {
|
|
14442
|
-
parsed = JSON.parse(await
|
|
14624
|
+
parsed = JSON.parse(await readFile14(path, "utf8"));
|
|
14443
14625
|
} catch {
|
|
14444
14626
|
parsed = {};
|
|
14445
14627
|
}
|
|
@@ -14751,6 +14933,12 @@ async function verifyCommand(command, opts) {
|
|
|
14751
14933
|
return [`${project.name} will verify with:`, "", ` ${next}`, "", "A build runs it in its sandboxed copy of the repository before its last step."].join("\n");
|
|
14752
14934
|
}
|
|
14753
14935
|
|
|
14936
|
+
// src/commands/build.ts
|
|
14937
|
+
import { execFile as execFile10 } from "child_process";
|
|
14938
|
+
import { existsSync as existsSync7 } from "fs";
|
|
14939
|
+
import { homedir as homedir6 } from "os";
|
|
14940
|
+
import { promisify as promisify9 } from "util";
|
|
14941
|
+
|
|
14754
14942
|
// src/format/opportunities.ts
|
|
14755
14943
|
function renderOpportunities(rows2, lens, opts) {
|
|
14756
14944
|
const paint = painter(opts.color);
|
|
@@ -15625,35 +15813,64 @@ function normalizeRef(input, items) {
|
|
|
15625
15813
|
}
|
|
15626
15814
|
|
|
15627
15815
|
// src/commands/build.ts
|
|
15816
|
+
var execFileAsync9 = promisify9(execFile10);
|
|
15628
15817
|
async function build(ref, opts) {
|
|
15629
15818
|
const { renderer, out } = rendererFor(opts.out);
|
|
15630
15819
|
const auth = await authenticate();
|
|
15631
15820
|
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15632
15821
|
const repo = await detectRepo(opts.cwd);
|
|
15633
15822
|
if (!repo) throw new Error("run this inside the repository, so NanoPM builds from the clone you are looking at");
|
|
15634
|
-
const
|
|
15635
|
-
|
|
15636
|
-
|
|
15637
|
-
|
|
15638
|
-
|
|
15823
|
+
const { data: solutions } = await auth.client.from("solutions").select("id, ref, title, status").eq("project_id", project.id).is("superseded_by", null);
|
|
15824
|
+
const wanted = normalizeRef(ref, solutions ?? []);
|
|
15825
|
+
const solution = (solutions ?? []).find((s) => s.ref === wanted);
|
|
15826
|
+
if (!solution) throw new Error(`No solution ${wanted} on the Roadmap.`);
|
|
15827
|
+
const stream = async (jobId2) => {
|
|
15828
|
+
out(`${solution.ref} ${solution.title}`);
|
|
15829
|
+
const answerer = opts.answersFile ? await answersFromFile(opts.answersFile) : answersFromTerminal(out);
|
|
15830
|
+
return streamJob(auth.client, jobId2, { pollMs: 700, out, renderer, cancelSignal: opts.cancelSignal, answerer, verbose: verboseWanted() });
|
|
15831
|
+
};
|
|
15832
|
+
const live = await buildOf(auth.client, project.id, [...ACTIVE_STATUSES]);
|
|
15833
|
+
if (live) {
|
|
15834
|
+
if (live.solutionId !== solution.id) throw new Error(`${project.name} already has a build on its way: one at a time. \`nanopm build\` on that solution comes back to it.`);
|
|
15835
|
+
if (opts.throwAway) throw new Error(`${solution.ref}'s build is still on its way: stop it first (\`nanopm stop\`).`);
|
|
15836
|
+
return stream(live.id);
|
|
15837
|
+
}
|
|
15838
|
+
const stopped = opts.continue || opts.throwAway ? await buildOf(auth.client, project.id, ["failed", "cancelled"], solution.id) : null;
|
|
15839
|
+
if ((opts.continue || opts.throwAway) && !stopped) throw new Error(`${solution.ref} has no stopped build to ${opts.continue ? "continue" : "throw away"}.`);
|
|
15840
|
+
if (opts.throwAway) return throwAway(auth.client, stopped.origin, repo.root, out);
|
|
15841
|
+
const [daemon, local] = await Promise.all([awakeDaemon(auth.client, auth.userId), localDaemonId()]);
|
|
15639
15842
|
if (!local || daemon?.id !== local) throw new Error("This machine\u2019s daemon is asleep, so nothing was queued. `nanopm daemon install` starts it; then build again.");
|
|
15640
15843
|
await rememberClone(auth.client, project.id, repo.root);
|
|
15641
|
-
const { jobId
|
|
15844
|
+
const { jobId } = await queueBuild(auth.client, {
|
|
15642
15845
|
projectId: project.id,
|
|
15643
15846
|
userId: auth.userId,
|
|
15644
15847
|
daemonId: local,
|
|
15645
|
-
ref:
|
|
15848
|
+
ref: solution.ref,
|
|
15646
15849
|
repoPath: repo.root,
|
|
15647
|
-
anyway: opts.anyway
|
|
15850
|
+
anyway: opts.anyway,
|
|
15851
|
+
...stopped ? { resumeOf: { id: stopped.id, was: stopped.was, origin: stopped.origin } } : {}
|
|
15648
15852
|
});
|
|
15649
|
-
|
|
15650
|
-
|
|
15651
|
-
|
|
15853
|
+
return stream(jobId);
|
|
15854
|
+
}
|
|
15855
|
+
async function buildOf(client, projectId, statuses, solutionId) {
|
|
15856
|
+
let query = client.from("jobs").select("id, params").eq("project_id", projectId).eq("skill", BUILD_SKILL).in("status", statuses);
|
|
15857
|
+
if (solutionId) query = query.eq("params->>solution_id", solutionId);
|
|
15858
|
+
const { data } = await query.order("created_at", { ascending: false }).limit(1).maybeSingle();
|
|
15859
|
+
const params = data?.params ?? {};
|
|
15860
|
+
return data ? { id: data.id, solutionId: params.solution_id ?? "", was: wasOf(data.params), origin: params.origin ?? data.id } : null;
|
|
15861
|
+
}
|
|
15862
|
+
async function throwAway(client, jobId, clone, out) {
|
|
15863
|
+
const { place } = await loadBuildHistory(client, [jobId], { home: homedir6(), jobId });
|
|
15864
|
+
if (!place) throw new Error("that build left nothing on this machine.");
|
|
15865
|
+
if (existsSync7(place.dir)) await execFileAsync9("git", ["-C", clone, "worktree", "remove", "--force", place.dir]);
|
|
15866
|
+
await execFileAsync9("git", ["-C", clone, "branch", "-D", place.branch]).catch(() => void 0);
|
|
15867
|
+
out(`Thrown away: the copy of the code and the local branch ${place.branch}.`);
|
|
15868
|
+
return 0;
|
|
15652
15869
|
}
|
|
15653
15870
|
|
|
15654
15871
|
// src/detect-sources.ts
|
|
15655
|
-
import { readFile as
|
|
15656
|
-
import { join as
|
|
15872
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
15873
|
+
import { join as join17 } from "path";
|
|
15657
15874
|
var ENV_FILES = [".env", ".env.local", ".env.development.local", ".env.development", ".env.example"];
|
|
15658
15875
|
var POSTGRES_KEYS = [
|
|
15659
15876
|
"DATABASE_URL",
|
|
@@ -15703,7 +15920,7 @@ async function envFiles(repoRoot) {
|
|
|
15703
15920
|
const out = [];
|
|
15704
15921
|
for (const file of ENV_FILES) {
|
|
15705
15922
|
try {
|
|
15706
|
-
out.push({ file, env: parseEnv(await
|
|
15923
|
+
out.push({ file, env: parseEnv(await readFile15(join17(repoRoot, file), "utf8")) });
|
|
15707
15924
|
} catch {
|
|
15708
15925
|
continue;
|
|
15709
15926
|
}
|
|
@@ -16472,7 +16689,7 @@ async function seedWatches(watches2, read6) {
|
|
|
16472
16689
|
}
|
|
16473
16690
|
|
|
16474
16691
|
// src/commands/watch.ts
|
|
16475
|
-
import { readFile as
|
|
16692
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
16476
16693
|
function cursorFor(source, watchId) {
|
|
16477
16694
|
const all = source.cursor ?? {};
|
|
16478
16695
|
return all[watchId] ?? {};
|
|
@@ -16533,7 +16750,7 @@ async function addWatch(labelOrId, id, opts) {
|
|
|
16533
16750
|
const paint = painter(color);
|
|
16534
16751
|
if (!labelOrId || !id) throw new Error('say which feed and what to call it: nanopm watch add <source> <id> --query "\u2026"');
|
|
16535
16752
|
if (opts.query && opts.queryFile) throw new Error("--query or --query-file, not both");
|
|
16536
|
-
const query = opts.queryFile ? await
|
|
16753
|
+
const query = opts.queryFile ? await readFile16(opts.queryFile, "utf8") : opts.query ?? "";
|
|
16537
16754
|
const watch = watchInput(id, query, opts);
|
|
16538
16755
|
const auth = await authenticate();
|
|
16539
16756
|
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
@@ -17466,7 +17683,7 @@ async function invite(email, opts) {
|
|
|
17466
17683
|
const [, ...rest] = inviteLines({ name: project.name, code, email: email.trim().toLowerCase(), web: webUrl() });
|
|
17467
17684
|
return [`Invite to ${paint(ANSI.bold, project.name)}: ${paint(ANSI.bold, code)}`, ...rest.map((l) => l ? paint(ANSI.dim, l) : l)].join("\n");
|
|
17468
17685
|
}
|
|
17469
|
-
async function
|
|
17686
|
+
async function join18(code, opts) {
|
|
17470
17687
|
const normalized2 = code.trim().toUpperCase();
|
|
17471
17688
|
if (!INVITE_CODE.test(normalized2)) throw new Error(`that does not look like an invite code: ten letters and digits, like ${"K7PQ2XM9RT"}`);
|
|
17472
17689
|
const repo = await detectRepo(opts.cwd);
|
|
@@ -17751,7 +17968,7 @@ async function showSchedule(opts) {
|
|
|
17751
17968
|
}
|
|
17752
17969
|
|
|
17753
17970
|
// src/commands/status.ts
|
|
17754
|
-
import { existsSync as
|
|
17971
|
+
import { existsSync as existsSync8 } from "fs";
|
|
17755
17972
|
async function status(opts) {
|
|
17756
17973
|
const auth = await authenticate();
|
|
17757
17974
|
const color = opts.color ?? useColor();
|
|
@@ -17804,7 +18021,7 @@ async function projectStatus(client, project) {
|
|
|
17804
18021
|
openProblems: (problems2.data ?? []).length,
|
|
17805
18022
|
unprovenProblems: (problems2.data ?? []).filter((p) => p.importance_basis === "judgment").length,
|
|
17806
18023
|
search: describeSearch(search, await localWebSearch()),
|
|
17807
|
-
onThisMachine: Boolean(project.repoPath &&
|
|
18024
|
+
onThisMachine: Boolean(project.repoPath && existsSync8(project.repoPath)),
|
|
17808
18025
|
measures: measures({
|
|
17809
18026
|
solutionStatuses: (roadmap2.data ?? []).map((r) => r.status),
|
|
17810
18027
|
decisions: decisions2.data ?? [],
|
|
@@ -18167,7 +18384,7 @@ function buildProgram() {
|
|
|
18167
18384
|
console.log(await invite(email, { cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
|
|
18168
18385
|
});
|
|
18169
18386
|
program.command("join <code>").description("Join a project you were invited to, from inside your clone of its repository").option("--no-color", "plain output").action(async (code, opts) => {
|
|
18170
|
-
console.log(await
|
|
18387
|
+
console.log(await join18(code, { cwd: process.cwd(), color: colorOf(opts) }));
|
|
18171
18388
|
});
|
|
18172
18389
|
program.command("members").description("Who is on this project, and whether their daemon is awake").option("--project <id>", "project id (default: the project of the current repo)").option("--no-color", "plain output").action(async (opts) => {
|
|
18173
18390
|
console.log(await members({ cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
|
|
@@ -18189,10 +18406,11 @@ function buildProgram() {
|
|
|
18189
18406
|
program.command("status").description("What is happening with this product: next review, what is waiting on you, whether the daemon is awake").option("--project <id>", "project id (default: the project of the current repo)").option("--no-color", "plain output").action(async (opts) => {
|
|
18190
18407
|
console.log(await status({ cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
|
|
18191
18408
|
});
|
|
18192
|
-
program.command("build <ref>").description("Build a solution from its agent spec, in a copy of this repository, to a draft pull request").option("--project <id>", "project id (default: the project of the current repo)").option("--anyway", "build although its questions for you are not answered").option("--answers <file>", "answer its questions from a file instead of the terminal").action(async (ref, opts) => {
|
|
18409
|
+
program.command("build <ref>").description("Build a solution from its agent spec, in a copy of this repository, to a draft pull request").option("--project <id>", "project id (default: the project of the current repo)").option("--anyway", "build although its questions for you are not answered").option("--continue", "take up a stopped build where it was").option("--throw-away", "delete a stopped build's copy of the code and its local branch").option("--answers <file>", "answer its questions from a file instead of the terminal").action(async (ref, opts) => {
|
|
18193
18410
|
const cancel = new AbortController();
|
|
18194
18411
|
process.once("SIGINT", () => cancel.abort());
|
|
18195
|
-
|
|
18412
|
+
if (opts.continue && opts.throwAway) throw new Error("--continue or --throw-away, not both.");
|
|
18413
|
+
process.exitCode = await build(ref, { cwd: process.cwd(), projectId: opts.project, anyway: opts.anyway, continue: opts.continue, throwAway: opts.throwAway, answersFile: opts.answers, cancelSignal: cancel.signal });
|
|
18196
18414
|
});
|
|
18197
18415
|
program.command("autonomy [level]").description("How far NanoPM may go on its own: read it, or set it").option("--project <id>", "project id (default: the project of the current repo)").action(async (level, opts) => {
|
|
18198
18416
|
console.log(await autonomy(level, { cwd: process.cwd(), projectId: opts.project }));
|