@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 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-75-build-it.md, settled 2).
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
- yield { type: "failed", sessionId: parser.currentSessionId, ...failure2, message: appendExit(failure2.message, code) };
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 join13 } from "path";
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
- ...workspace ? { NANOPM_WORKSPACE: workspace } : {},
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 join5 } from "path";
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
- params: { solution_id: solution.id, repo_path: ask7.repoPath, ...ask7.anyway ? { anyway: true } : {} }
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(join5(repo.dir, ".git"), `gitdir: ${repo.admin}
5450
+ await writeFile4(join6(repo.dir, ".git"), `gitdir: ${repo.admin}
5373
5451
  `);
5374
- await writeFile4(join5(repo.admin, "commondir"), repo.commondir);
5375
- await rm3(join5(repo.admin, "config.worktree"), { force: true });
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(join5(admin, "commondir"), "utf8") };
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 existsSync2, readdirSync } from "fs";
5583
- import { readFile as readFile3 } from "fs/promises";
5584
- import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
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
- join6(process.cwd(), "skills"),
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 (existsSync2(join6(dir, "onboard", "skill.json"))) return dir;
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) => join6(d, name)).find((d) => existsSync2(join6(d, "skill.json")));
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 readFile3(join6(dir, "skill.json"), "utf8"));
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 readFile3(join6(dir, "SKILL.md"), "utf8");
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() && existsSync2(join6(skillsDir, d.name, "skill.json"))).map((d) => d.name);
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 readFile4 } from "fs/promises";
5978
- import { join as join7 } from "path";
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 readFile4(join7(repoRoot, name), "utf8");
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 readFile5, readdir, stat } from "fs/promises";
6156
- import { join as join8, relative } from "path";
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 readdir(dir, { withFileTypes: true });
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 = join8(dir, entry.name);
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 readFile5(path, "utf8")).slice(0, DOC_CHARS) });
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 mkdir2 } from "fs/promises";
7989
- import { join as join9 } from "path";
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 ? join9(spec2.runDir, dir) : spec2.runDir;
8005
- if (dir) await mkdir2(runDir, { recursive: true, mode: 448 });
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 existsSync3 } from "fs";
10063
- import { mkdir as mkdir3, readFile as readFile6 } from "fs/promises";
10064
- import { join as join10 } from "path";
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 readFile6(join10(runDir, "harness.pid"), "utf8")).trim());
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 = join10(spec2.runDir, "definitions");
10121
- await mkdir3(runDir, { recursive: true, mode: 448 });
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) => existsSync3(join10(spec2.cwd, 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 existsSync4 } from "fs";
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 join12, resolve as resolve4 } from "path";
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
- finished_at: (/* @__PURE__ */ new Date()).toISOString(),
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 = join12(nanopmHome(), "run", job.id);
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(join12(nanopmHome(), "daemon.json"), "utf8"));
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 join12(tmpdir(), `nanopm-run-${jobId}`);
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 (existsSync4(allowed)) return resolve4(allowed);
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", join12(home, ".local", "bin")].join(":"),
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 = join13(nanopmHome(), "daemon.pid");
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(join13(runDir, d.name));
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(join13(dir, "harness.pid"), "utf8")).trim());
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 = join13(nanopmHome(), "run", jobId);
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 = join13(nanopmHome(), "daemon.json");
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(join13(nanopmHome(), "daemon.json"), JSON.stringify({ id, shared }, null, 2) + "\n", { mode: 384 });
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 readFile11 } from "fs/promises";
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 readFile10 } from "fs/promises";
13521
- import { join as join14 } from "path";
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 readFile10(join14(nanopmHome(), "daemon.json"), "utf8"));
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() : basename(root2);
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 = basename(f);
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 readFile11(file, "utf8");
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 readFile12 } from "fs/promises";
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 readFile12(path, "utf8"));
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 existsSync5 } from "fs";
14229
- import { mkdir as mkdir7, readFile as readFile13, rm as rm6, writeFile as writeFile7 } from "fs/promises";
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 join15 } from "path";
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.11",
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 join15(homedir5(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
14471
+ return join16(homedir5(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
14290
14472
  }
14291
14473
  function daemonLogPath() {
14292
- return join15(nanopmHome(), "daemon.log");
14474
+ return join16(nanopmHome(), "daemon.log");
14293
14475
  }
14294
14476
  function renderPlist(opts) {
14295
14477
  const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -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", join15(home, ".local", "bin")].join(":");
14353
- await mkdir7(join15(home, "Library", "LaunchAgents"), { recursive: true });
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 (!existsSync5(plistPath())) return false;
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" && existsSync5(plistPath());
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 readFile13(daemonLogPath(), "utf8");
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 readFile13(join15(nanopmHome(), "daemon.json"), "utf8"));
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 = join15(nanopmHome(), "daemon.json");
14602
+ const path = join16(nanopmHome(), "daemon.json");
14421
14603
  let parsed = {};
14422
14604
  try {
14423
- parsed = JSON.parse(await readFile13(path, "utf8"));
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 readFile13(join15(nanopmHome(), "daemon.json"), "utf8"));
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 = join15(nanopmHome(), "daemon.json");
14621
+ const path = join16(nanopmHome(), "daemon.json");
14440
14622
  let parsed = {};
14441
14623
  try {
14442
- parsed = JSON.parse(await readFile13(path, "utf8"));
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 [daemon, local, { data: refs }] = await Promise.all([
15635
- awakeDaemon(auth.client, auth.userId),
15636
- localDaemonId(),
15637
- auth.client.from("solutions").select("ref").eq("project_id", project.id).is("superseded_by", null)
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, solution } = await queueBuild(auth.client, {
15844
+ const { jobId } = await queueBuild(auth.client, {
15642
15845
  projectId: project.id,
15643
15846
  userId: auth.userId,
15644
15847
  daemonId: local,
15645
- ref: normalizeRef(ref, refs ?? []),
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
- out(`${solution.ref} ${solution.title}`);
15650
- const answerer = opts.answersFile ? await answersFromFile(opts.answersFile) : answersFromTerminal(out);
15651
- return streamJob(auth.client, jobId, { pollMs: 700, out, renderer, cancelSignal: opts.cancelSignal, answerer, verbose: verboseWanted() });
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 readFile14 } from "fs/promises";
15656
- import { join as join16 } from "path";
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 readFile14(join16(repoRoot, file), "utf8")) });
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 readFile15 } from "fs/promises";
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 readFile15(opts.queryFile, "utf8") : opts.query ?? "";
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 join17(code, opts) {
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 existsSync6 } from "fs";
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 && existsSync6(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 join17(code, { cwd: process.cwd(), color: colorOf(opts) }));
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
- process.exitCode = await build(ref, { cwd: process.cwd(), projectId: opts.project, anyway: opts.anyway, answersFile: opts.answers, cancelSignal: cancel.signal });
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 }));