@nanopm/cli 0.1.12 → 0.1.15

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.
Files changed (2) hide show
  1. package/dist/index.js +568 -380
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -67,7 +67,7 @@ var Constants = {
67
67
  opportunity_next_action: ["investigate", "explore_solutions"],
68
68
  opportunity_priority: ["low", "medium", "high"],
69
69
  opportunity_status: ["proposed", "confirmed", "set_aside", "replaced"],
70
- notification_kind: ["review", "stalled"],
70
+ notification_kind: ["review", "stalled", "build_question", "build_pr"],
71
71
  problem_status: ["candidate", "confirmed", "addressed", "dropped"],
72
72
  provenance: ["observed", "stated", "inferred"],
73
73
  source_kind: ["postgres_watch", "posthog_watch"],
@@ -2189,12 +2189,14 @@ function boardOf(all, solutions) {
2189
2189
  walk(tree);
2190
2190
  const cards2 = [];
2191
2191
  for (const [leafId, list] of byLeaf) {
2192
- const standing = list.filter((s) => s.status === "proposed");
2193
- const pick3 = standing.find((s) => s.horizon) ?? null;
2192
+ const standing = list.filter((s) => s.status === "proposed" || s.status === "building");
2193
+ const placed = list.find((s) => s.horizon && s.status !== "rejected") ?? null;
2194
+ const shipped = list.filter((s) => s.status === "done").at(-1);
2195
+ const pick3 = standing.find((s) => s.status === "building") ?? shipped ?? placed;
2194
2196
  const leaf = nodes.get(leafId);
2195
2197
  const branch = leaf ? branchOf(tree, leaf.id) : null;
2196
- if (!pick3 || !leaf || !branch) continue;
2197
- cards2.push({ leaf, branch, pick: pick3, others: standing.filter((s) => s.id !== pick3.id), horizon: pick3.horizon, place: pick3.place ?? 1 });
2198
+ if (!pick3 || !placed || !leaf || !branch) continue;
2199
+ cards2.push({ leaf, branch, pick: pick3, others: standing.filter((s) => s.id !== pick3.id), horizon: placed.horizon, place: placed.place ?? 1 });
2198
2200
  }
2199
2201
  const col = (h) => HORIZONS.indexOf(h);
2200
2202
  cards2.sort((a, b) => col(a.horizon) - col(b.horizon) || a.place - b.place || a.pick.created_at.localeCompare(b.pick.created_at));
@@ -4143,11 +4145,14 @@ async function latestVersion(fetchImpl = fetch) {
4143
4145
  return null;
4144
4146
  }
4145
4147
  }
4148
+ function installArgs(version, home = homedir2()) {
4149
+ return ["install", "-g", "--prefix", appDir(home), `${PACKAGE}@${version}`, "--prefer-online", "--no-fund", "--no-audit", "--loglevel=error"];
4150
+ }
4146
4151
  async function installVersion(version, home = homedir2()) {
4147
4152
  if (!RELEASE.test(version)) return { ok: false, why: `not a version: ${version}` };
4148
4153
  const runtime = join4(root(home), "runtime", "bin");
4149
4154
  try {
4150
- await execFileAsync2(join4(runtime, "npm"), ["install", "-g", "--prefix", appDir(home), `${PACKAGE}@${version}`, "--no-fund", "--no-audit", "--loglevel=error"], {
4155
+ await execFileAsync2(join4(runtime, "npm"), installArgs(version, home), {
4151
4156
  timeout: 5 * 6e4,
4152
4157
  env: { ...process.env, PATH: `${runtime}:${process.env.PATH ?? ""}`, NPM_CONFIG_UPDATE_NOTIFIER: "false" }
4153
4158
  });
@@ -5333,11 +5338,26 @@ var BUILD_STEP_MAX = 120;
5333
5338
  var BUILD_RESUMES_MAX = 3;
5334
5339
  var BUILD_TRAILER = "Co-Authored-By: NanoPM <noreply@nanopm.app>";
5335
5340
  var BUILD_COLUMNS = "id, ref, title, status, actor, agent_spec, spec";
5341
+ var answered = (q) => typeof q?.answer === "string" && Boolean(q.answer.trim()) && typeof q.ask === "string";
5342
+ function openQuestions(spec2) {
5343
+ return (spec2?.questions ?? []).filter((q) => !answered(q)).length;
5344
+ }
5345
+ function specForAgent(agentSpec, spec2) {
5346
+ const said = (spec2?.questions ?? []).filter(answered);
5347
+ if (!said.length) return agentSpec;
5348
+ return `${agentSpec.trimEnd()}
5349
+
5350
+ ## The founder's answers
5351
+
5352
+ ${said.map((q) => `- ${q.ask.trim()}
5353
+ ${q.answer.trim()}`).join("\n")}
5354
+ `;
5355
+ }
5336
5356
  function buildRefusal(solution, opts = {}) {
5337
5357
  if (solution.actor === "founder") return `${solution.ref} is yours to do, not Nano's to build.`;
5338
5358
  if (!["proposed", "accepted", "building"].includes(solution.status)) return `${solution.ref} is ${solution.status}: only a solution on the roadmap can be built.`;
5339
5359
  if (!solution.agent_spec) return `${solution.ref} has no agent spec yet. Write its specs first: nanopm spec ${solution.ref} --write`;
5340
- const open = solution.spec?.questions?.length ?? 0;
5360
+ const open = openQuestions(solution.spec);
5341
5361
  if (open && !opts.anyway) {
5342
5362
  return `${solution.ref} still has ${open} question${open === 1 ? "" : "s"} for you. Answer ${open === 1 ? "it" : "them"} in its spec, or build anyway: nanopm build ${solution.ref} --anyway`;
5343
5363
  }
@@ -5396,12 +5416,20 @@ async function queueBuild(client, ask7) {
5396
5416
  return { jobId: job.id, solution };
5397
5417
  }
5398
5418
  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*.";
5419
+ var isDecideForMe = (answer) => /^\s*decide(\s+for\s+me)?\s*\.?\s*$/i.test(answer);
5399
5420
  function answerForAgent(answer) {
5400
- return /^\s*decide(\s+for\s+me)?\s*\.?\s*$/i.test(answer) ? DECIDE_FOR_ME : answer;
5421
+ return isDecideForMe(answer) ? DECIDE_FOR_ME : answer;
5401
5422
  }
5423
+ var BUILD_HISTORY_KINDS = ["worktree", "plan", "step", "question", "answer", "paused", "pr", "handed"];
5424
+ var BUILD_CONFLICT = { resolve: "Let Nano resolve it", mine: "I\u2019ll do it in my terminal" };
5425
+ var chose = (answer, option) => {
5426
+ const plain = (s) => s.trim().toLowerCase().replace(/[’‘]/g, "'");
5427
+ return plain(answer) === plain(option);
5428
+ };
5429
+ var BUILD_REFUSED = { again: "Try again", open: "Open it on GitHub" };
5402
5430
  function trustedPlace(p, at) {
5403
5431
  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;
5432
+ if (!/^nano\/[a-z0-9][a-z0-9-]*$/.test(branch) || !/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(base) || /^refs\/|HEAD$/.test(base) || !/^[0-9a-f]{7,40}$/.test(fork)) return null;
5405
5433
  return { dir: buildDir(at.home, at.jobId), branch, base, fork };
5406
5434
  }
5407
5435
  function buildHistory(events, at) {
@@ -5409,26 +5437,36 @@ function buildHistory(events, at) {
5409
5437
  let plan = null;
5410
5438
  const done = /* @__PURE__ */ new Set();
5411
5439
  let last = null;
5440
+ let report = "";
5441
+ let handed = false;
5412
5442
  const answers = /* @__PURE__ */ new Map();
5413
5443
  for (const e of events) {
5414
- if (e.kind === "worktree") place = trustedPlace(e.payload, at);
5444
+ if (e.kind === "worktree" && !place) place = trustedPlace(e.payload, at);
5415
5445
  else if (e.kind === "plan" && Array.isArray(e.payload.steps)) plan = e.payload.steps;
5416
5446
  else if (e.kind === "step" && typeof e.payload.step === "number") done.add(e.payload.step - 1);
5417
5447
  else if (e.kind === "answer") answers.set(e.seq, String(e.payload.answer ?? ""));
5448
+ else if (e.kind === "handed") handed = true;
5449
+ if (e.kind === "question" && (e.payload.about === "conflict" || e.payload.about === "refused")) {
5450
+ if (typeof e.payload.report === "string") report = e.payload.report;
5451
+ if (e.payload.about === "conflict") handed = false;
5452
+ }
5418
5453
  if (e.kind === "question" || e.kind === "paused") last = e;
5419
- else if (e.kind === "plan" || e.kind === "step") last = null;
5454
+ else if (e.kind === "plan" || e.kind === "step" || e.kind === "handed") last = null;
5420
5455
  }
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" } };
5456
+ const found = (resume2) => ({ place, plan, done, resume: resume2, report, handed });
5457
+ if (!place) return found(null);
5458
+ const answered2 = last?.kind === "question" && answers.has(last.seq) ? answers.get(last.seq) : null;
5459
+ if (answered2 !== null && last.payload.about === "usage") return found({ why: "limit" });
5460
+ if (answered2 !== null && (last.payload.about === "conflict" || last.payload.about === "refused")) return found({ why: last.payload.about, answer: answered2 });
5461
+ if (answered2 !== null) return found({ why: "answer", answer: answered2 });
5462
+ if (last?.kind === "paused") return found({ why: "limit" });
5463
+ return found({ why: "lost" });
5426
5464
  }
5427
5465
  function wasOf(params) {
5428
5466
  return params?.was === "proposed" ? "proposed" : "accepted";
5429
5467
  }
5430
5468
  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");
5469
+ const { data, error } = await client.from("job_events").select("kind, seq, payload").in("job_id", [...jobIds]).in("kind", [...BUILD_HISTORY_KINDS]).order("id");
5432
5470
  if (error) throw new Error(`could not read the build's history: ${error.message}`);
5433
5471
  return buildHistory((data ?? []).map((e) => ({ kind: e.kind, seq: e.seq, payload: e.payload ?? {} })), at);
5434
5472
  }
@@ -5439,11 +5477,18 @@ var execFileAsync3 = promisify3(execFile3);
5439
5477
  var MAX_BUFFER = 4 * 1024 * 1024;
5440
5478
  var TIMEOUT_MS = 6e4;
5441
5479
  var LOCKED = {
5442
- GIT_CONFIG_COUNT: "2",
5480
+ GIT_CONFIG_COUNT: "5",
5443
5481
  GIT_CONFIG_KEY_0: "core.hooksPath",
5444
5482
  GIT_CONFIG_VALUE_0: "/dev/null",
5445
5483
  GIT_CONFIG_KEY_1: "core.fsmonitor",
5446
- GIT_CONFIG_VALUE_1: "false"
5484
+ GIT_CONFIG_VALUE_1: "false",
5485
+ // A fetch never opens a nested repository the agent could have planted where a submodule goes.
5486
+ GIT_CONFIG_KEY_2: "fetch.recurseSubmodules",
5487
+ GIT_CONFIG_VALUE_2: "false",
5488
+ GIT_CONFIG_KEY_3: "submodule.recurse",
5489
+ GIT_CONFIG_VALUE_3: "false",
5490
+ GIT_CONFIG_KEY_4: "protocol.ext.allow",
5491
+ GIT_CONFIG_VALUE_4: "never"
5447
5492
  };
5448
5493
  async function exec(cmd, repo, args, timeoutMs = TIMEOUT_MS) {
5449
5494
  if (typeof repo === "string") return execFileAsync3(cmd, cmd === "git" ? ["-C", repo, ...args] : args, { cwd: repo, maxBuffer: MAX_BUFFER, timeout: timeoutMs });
@@ -5461,7 +5506,7 @@ var RepoError = class extends Error {
5461
5506
  async function commitAll(cwd, message, note) {
5462
5507
  await git(cwd, ["add", "-A"]);
5463
5508
  const staged = await git(cwd, ["diff", "--cached", "--name-only"]).catch(() => "");
5464
- if (!staged) return null;
5509
+ if (!staged && !await merging(cwd)) return null;
5465
5510
  await git(cwd, ["commit", "--no-verify", "-m", message, ...note ? ["-m", note] : [], "-m", `Written by NanoPM. Review before merging.
5466
5511
 
5467
5512
  ${BUILD_TRAILER}`]);
@@ -5542,6 +5587,42 @@ async function rebaseOnto(cwd, base) {
5542
5587
  return { ok: false, files };
5543
5588
  }
5544
5589
  }
5590
+ async function containsBase(cwd, base) {
5591
+ const onto = await baseRef(cwd, base);
5592
+ return git(cwd, ["merge-base", "--is-ancestor", onto, "HEAD"]).then(
5593
+ () => true,
5594
+ () => false
5595
+ );
5596
+ }
5597
+ async function merging(cwd) {
5598
+ return Boolean(await git(cwd, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]).catch(() => ""));
5599
+ }
5600
+ async function mergedSince(cwd, fork) {
5601
+ return Boolean(await git(cwd, ["rev-list", "--merges", "-n", "1", `${fork}..HEAD`]).catch(() => ""));
5602
+ }
5603
+ async function mergeBase(cwd, base) {
5604
+ if (await merging(cwd)) return { files: await conflictsLeft(cwd, base) };
5605
+ const onto = await baseRef(cwd, base);
5606
+ try {
5607
+ await git(cwd, ["merge", "--no-ff", "--no-commit", "--no-verify", onto], 12e4);
5608
+ return { files: [] };
5609
+ } catch (err) {
5610
+ const files = (await git(cwd, ["diff", "--name-only", "--diff-filter=U"]).catch(() => "")).split("\n").filter(Boolean);
5611
+ if (!files.length) throw new RepoError(`merging ${base} failed: ${errorMessage(err).split("\n").filter(Boolean).slice(-2).join(" ")}`);
5612
+ return { files };
5613
+ }
5614
+ }
5615
+ async function conflictsLeft(cwd, base) {
5616
+ const onto = await baseRef(cwd, base);
5617
+ const changed = (await git(cwd, ["diff", "--name-only", onto]).catch(() => "")).split("\n").filter(Boolean);
5618
+ if (!changed.length) return [];
5619
+ const marked = await git(cwd, ["grep", "-l", "-E", "^(<<<<<<<|>>>>>>>)( |$)", "--", ...changed]).catch(() => "");
5620
+ return marked.split("\n").filter(Boolean).sort();
5621
+ }
5622
+ async function newPullRequestUrl(cwd, branch) {
5623
+ const repo = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await originUrl(cwd))?.[1];
5624
+ return repo ? `https://github.com/${repo}/pull/new/${branch}` : null;
5625
+ }
5545
5626
  async function gh(cwd, args) {
5546
5627
  return (await exec("gh", cwd, args, 12e4)).stdout.trim();
5547
5628
  }
@@ -5554,6 +5635,13 @@ async function ghReady(cwd) {
5554
5635
  return /ENOENT/.test(errorMessage(err)) ? "the GitHub CLI is not installed on this machine: brew install gh, then gh auth login" : "the GitHub CLI is not signed in on this machine: run gh auth login in your terminal";
5555
5636
  }
5556
5637
  }
5638
+ async function pullRequestMerged(cwd, number) {
5639
+ try {
5640
+ return await gh(cwd, ["pr", "view", String(number), "--json", "state", "--jq", ".state"]) === "MERGED";
5641
+ } catch {
5642
+ return false;
5643
+ }
5644
+ }
5557
5645
  async function openPullRequest(cwd, pr) {
5558
5646
  try {
5559
5647
  const out = await gh(cwd, ["pr", "create", "--draft", "--head", pr.branch, "--base", pr.base, "--title", pr.title, "--body", pr.body]);
@@ -5629,314 +5717,26 @@ async function reviewDeploys(client, daemonId, now = /* @__PURE__ */ new Date())
5629
5717
  }
5630
5718
 
5631
5719
  // ../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
- }
5720
+ import { existsSync as existsSync5 } from "fs";
5721
+ import { mkdir as mkdir5, readdir as readdir2, readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
5722
+ import { homedir as homedir4 } from "os";
5723
+ import { basename, join as join13 } from "path";
5899
5724
 
5900
- // ../daemon/src/review-preflight.ts
5901
- async function reviewWithoutData(client, job) {
5902
- if (job.skill !== "review" || !job.project_id) return null;
5903
- if (job.trigger === "user") return null;
5904
- const { data: connections } = await client.from("connections").select("id, status").eq("project_id", job.project_id).is("removed_at", null);
5905
- const usable = (connections ?? []).filter((c) => c.status === "ok");
5906
- if (usable.length) return null;
5907
- const attached = (connections ?? []).length;
5908
- const summary = attached ? `No review this time: ${attached} data source${attached === 1 ? " is" : "s are"} attached but none is working. Run \`nanopm connect --list\` to see what is wrong.` : "No review this time: nothing is connected, so there was nothing to read. Run `nanopm connect` to attach your database.";
5909
- const { error } = await client.from("reviews").insert({
5910
- project_id: job.project_id,
5911
- summary,
5912
- confirms: [],
5913
- contradicts: [],
5914
- job_id: job.id
5915
- });
5916
- if (error) return { status: "failed", error: { code: "harness_error", message: `could not record that there was nothing to read: ${error.message}` } };
5917
- await client.from("activity_log").insert({
5918
- project_id: job.project_id,
5919
- job_id: job.id,
5920
- actor: "daemon",
5921
- step: "review.skipped",
5922
- summary: attached ? "no working data source" : "nothing connected",
5923
- outcome: "ok"
5924
- });
5925
- return { status: "succeeded", result: summary };
5926
- }
5725
+ // ../daemon/src/problem-mapping-harness.ts
5726
+ import { randomUUID as randomUUID2 } from "crypto";
5927
5727
 
5928
5728
  // ../daemon/src/skills.ts
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";
5729
+ import { existsSync as existsSync2, readdirSync } from "fs";
5730
+ import { readFile as readFile3 } from "fs/promises";
5731
+ import { dirname as dirname2, join as join7, resolve as resolve2 } from "path";
5932
5732
  function findSkillsDir(env = process.env) {
5933
5733
  if (env.NANOPM_SKILLS_DIR) return env.NANOPM_SKILLS_DIR;
5934
5734
  const candidates = [
5935
- join8(process.cwd(), "skills"),
5735
+ join7(process.cwd(), "skills"),
5936
5736
  ...process.argv[1] ? [resolve2(dirname2(process.argv[1]), "..", "skills")] : []
5937
5737
  ];
5938
5738
  for (const dir of candidates) {
5939
- if (existsSync3(join8(dir, "onboard", "skill.json"))) return dir;
5739
+ if (existsSync2(join7(dir, "onboard", "skill.json"))) return dir;
5940
5740
  }
5941
5741
  throw new Error(`Cannot find the skills directory (tried ${candidates.join(", ")}). Set NANOPM_SKILLS_DIR.`);
5942
5742
  }
@@ -5946,9 +5746,9 @@ function skillDirs(skillsDir, env = process.env) {
5946
5746
  }
5947
5747
  async function loadSkill(name, skillsDir = findSkillsDir()) {
5948
5748
  if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new Error(`Invalid skill name "${name}"`);
5949
- const dir = skillDirs(skillsDir).map((d) => join8(d, name)).find((d) => existsSync3(join8(d, "skill.json")));
5749
+ const dir = skillDirs(skillsDir).map((d) => join7(d, name)).find((d) => existsSync2(join7(d, "skill.json")));
5950
5750
  if (!dir) throw new Error(`Unknown skill "${name}"`);
5951
- const raw = JSON.parse(await readFile4(join8(dir, "skill.json"), "utf8"));
5751
+ const raw = JSON.parse(await readFile3(join7(dir, "skill.json"), "utf8"));
5952
5752
  let manifest;
5953
5753
  try {
5954
5754
  manifest = parseSkillManifest(raw);
@@ -5957,7 +5757,7 @@ async function loadSkill(name, skillsDir = findSkillsDir()) {
5957
5757
  }
5958
5758
  const tainted = taintedByTheWeb(manifest);
5959
5759
  if (tainted) throw new Error(`the "${name}" skill cannot be loaded: ${tainted}`);
5960
- const body = await readFile4(join8(dir, "SKILL.md"), "utf8");
5760
+ const body = await readFile3(join7(dir, "SKILL.md"), "utf8");
5961
5761
  return { manifest, body, dir };
5962
5762
  }
5963
5763
  var NOT_WITH_READ_WEB = ["write_memory", "problems", "propose", "review", "write_repo", "run_commands"];
@@ -5992,7 +5792,7 @@ function renderPrompt(template, params) {
5992
5792
  var NEEDS_THE_REQUESTERS_CLONE = ["write_repo", "run_commands"];
5993
5793
  var NEEDS_THE_REQUESTERS_IDENTITY = /* @__PURE__ */ new Set(["want", "start"]);
5994
5794
  async function clonelessSkills(skillsDir = findSkillsDir()) {
5995
- const names = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync3(join8(skillsDir, d.name, "skill.json"))).map((d) => d.name);
5795
+ const names = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync2(join7(skillsDir, d.name, "skill.json"))).map((d) => d.name);
5996
5796
  const out = [];
5997
5797
  for (const name of names) {
5998
5798
  try {
@@ -6006,7 +5806,6 @@ async function clonelessSkills(skillsDir = findSkillsDir()) {
6006
5806
  }
6007
5807
 
6008
5808
  // ../daemon/src/problem-mapping-harness.ts
6009
- import { randomUUID as randomUUID2 } from "crypto";
6010
5809
  function mappingHarnessRunner(options) {
6011
5810
  return async (input) => {
6012
5811
  const skill = await loadSkill(`problem-${input.role}`);
@@ -6045,7 +5844,31 @@ ${JSON.stringify(mappingOutputSchema(input.role))}`,
6045
5844
  }
6046
5845
 
6047
5846
  // ../daemon/src/problem-mapping.ts
6048
- import { createHash as createHash2 } from "crypto";
5847
+ import { createHash as createHash2 } from "crypto";
5848
+
5849
+ // ../daemon/src/usage.ts
5850
+ function sumUsage(a, b) {
5851
+ if (!a) return { ...b };
5852
+ return {
5853
+ ...b,
5854
+ inputTokens: a.inputTokens + b.inputTokens,
5855
+ outputTokens: a.outputTokens + b.outputTokens,
5856
+ cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
5857
+ cacheWriteTokens: (a.cacheWriteTokens ?? 0) + (b.cacheWriteTokens ?? 0),
5858
+ costUsd: (a.costUsd ?? 0) + (b.costUsd ?? 0),
5859
+ turns: a.turns + b.turns,
5860
+ durationMs: a.durationMs + b.durationMs,
5861
+ ...a.contextPeakTokens || b.contextPeakTokens ? { contextPeakTokens: Math.max(a.contextPeakTokens ?? 0, b.contextPeakTokens ?? 0) } : {},
5862
+ ...a.reads || b.reads ? { reads: sumReads(a.reads, b.reads) } : {}
5863
+ };
5864
+ }
5865
+ function sumReads(a, b) {
5866
+ const calls = { ...a?.calls ?? {} };
5867
+ for (const [tool, n] of Object.entries(b?.calls ?? {})) calls[tool] = (calls[tool] ?? 0) + n;
5868
+ return { files: (a?.files ?? 0) + (b?.files ?? 0), calls };
5869
+ }
5870
+
5871
+ // ../daemon/src/problem-mapping.ts
6049
5872
  var MAPPING_TURN_LIMIT = 40;
6050
5873
  var MAPPING_TIMEOUT_MS = 20 * 6e4;
6051
5874
  async function runProblemMapping(ports) {
@@ -6297,8 +6120,8 @@ async function askAgainIfUnanswered(attempt7, say, role7) {
6297
6120
  var UNANSWERED = /structured[ _]output|returned no result/i;
6298
6121
 
6299
6122
  // ../daemon/src/site.ts
6300
- import { readFile as readFile5 } from "fs/promises";
6301
- import { join as join9 } from "path";
6123
+ import { readFile as readFile4 } from "fs/promises";
6124
+ import { join as join8 } from "path";
6302
6125
  var SITE_PATHS = ["/", "/about", "/pricing"];
6303
6126
  var PAGE_CHARS = 6e3;
6304
6127
  var SITE_TIMEOUT_MS = 2e4;
@@ -6421,7 +6244,7 @@ function siteFromSearch(name, hits) {
6421
6244
  }
6422
6245
  async function read(repoRoot, name) {
6423
6246
  try {
6424
- const text2 = await readFile5(join9(repoRoot, name), "utf8");
6247
+ const text2 = await readFile4(join8(repoRoot, name), "utf8");
6425
6248
  return text2.length > 2e5 ? null : text2;
6426
6249
  } catch {
6427
6250
  return null;
@@ -6475,8 +6298,8 @@ async function fromReadme(repoRoot) {
6475
6298
  }
6476
6299
 
6477
6300
  // ../daemon/src/repo-docs.ts
6478
- import { readFile as readFile6, readdir as readdir2, stat } from "fs/promises";
6479
- import { join as join10, relative } from "path";
6301
+ import { readFile as readFile5, readdir, stat } from "fs/promises";
6302
+ import { join as join9, relative } from "path";
6480
6303
  var WRITTEN = /(persona|audience|customer|user-research|research\/|icp|segment)/i;
6481
6304
  var ABOUT_PEOPLE = /(persona|audience|customer|user-research|research\/|interview|feedback|survey|verbatim|voice|icp|segment)/i;
6482
6305
  var DOC = /\.(md|mdx|txt)$/i;
@@ -6489,12 +6312,12 @@ async function readDocs(cwd, opts = {}) {
6489
6312
  if (depth > 3 || found.length >= DOCS_MAX * 4) return;
6490
6313
  let entries;
6491
6314
  try {
6492
- entries = await readdir2(dir, { withFileTypes: true });
6315
+ entries = await readdir(dir, { withFileTypes: true });
6493
6316
  } catch {
6494
6317
  return;
6495
6318
  }
6496
6319
  for (const entry of entries) {
6497
- const full = join10(dir, entry.name);
6320
+ const full = join9(dir, entry.name);
6498
6321
  if (SKIP.test(`/${relative(cwd, full)}`)) continue;
6499
6322
  if (entry.isDirectory()) await walk(full, depth + 1);
6500
6323
  else if (DOC.test(entry.name) && (!opts.onlyAboutPeople || ABOUT_PEOPLE.test(relative(cwd, full)))) found.push(full);
@@ -6506,7 +6329,7 @@ async function readDocs(cwd, opts = {}) {
6506
6329
  for (const path of found.slice(0, DOCS_MAX)) {
6507
6330
  try {
6508
6331
  if ((await stat(path)).size > 4e5) continue;
6509
- out.push({ path: relative(cwd, path), text: (await readFile6(path, "utf8")).slice(0, DOC_CHARS) });
6332
+ out.push({ path: relative(cwd, path), text: (await readFile5(path, "utf8")).slice(0, DOC_CHARS) });
6510
6333
  } catch {
6511
6334
  }
6512
6335
  }
@@ -6981,8 +6804,8 @@ function rank(candidates, readings, verdict, faults) {
6981
6804
  const failed2 = v?.failed ?? [];
6982
6805
  const fatal = failed2.filter((t) => MUST_PASS_TESTS.includes(t.test));
6983
6806
  const gates = faults.get(candidate.lands) ?? [];
6984
- const answered = /* @__PURE__ */ new Set([...v?.passed ?? [], ...failed2.map((t) => t.test)]);
6985
- const unanswered = MUST_PASS_TESTS.filter((t) => !answered.has(t));
6807
+ const answered2 = /* @__PURE__ */ new Set([...v?.passed ?? [], ...failed2.map((t) => t.test)]);
6808
+ const unanswered = MUST_PASS_TESTS.filter((t) => !answered2.has(t));
6986
6809
  const reservations = [
6987
6810
  ...fatal.map((t) => `${t.test} \u2014 ${t.because}`),
6988
6811
  ...gates,
@@ -7922,7 +7745,7 @@ function gateProposal(input) {
7922
7745
  for (let hops = 0; n && n.status === "replaced" && n.replaced_by && hops < 50; hops += 1) n = byId.get(n.replaced_by);
7923
7746
  return n && n.status !== "replaced" ? n : void 0;
7924
7747
  };
7925
- const answered = (r) => r.status === "confirmed" || r.author_kind === "user";
7748
+ const answered2 = (r) => r.status === "confirmed" || r.author_kind === "user";
7926
7749
  const under = scopeRow ? new Set(descendants(rows2, scopeRow).map((r) => r.id)) : null;
7927
7750
  const needOf = (r) => {
7928
7751
  let n = r;
@@ -8041,13 +7864,13 @@ function gateProposal(input) {
8041
7864
  keptIds.add(target.id);
8042
7865
  continue;
8043
7866
  }
8044
- if (input.hook && !answered(target) && target.provenance === "observed" && draft.provenance === "inferred") {
7867
+ if (input.hook && !answered2(target) && target.provenance === "observed" && draft.provenance === "inferred") {
8045
7868
  placed.set(node.key, { kind: "existing", row: target, branch: home });
8046
7869
  keptIds.add(target.id);
8047
7870
  kept.push(`"${target.label}"`);
8048
7871
  continue;
8049
7872
  }
8050
- if (answered(target)) {
7873
+ if (answered2(target)) {
8051
7874
  home.nodes.push(outgoing(node.key, draft, outside, parent, nextRank(parent), { next_of: target.id }));
8052
7875
  proposals += 1;
8053
7876
  placed.set(node.key, { kind: "existing", row: target, branch: home });
@@ -8074,7 +7897,7 @@ function gateProposal(input) {
8074
7897
  if (input.resumeOf && r.job_id === input.resumeOf) return false;
8075
7898
  if (within && needOf(r).id !== within) return false;
8076
7899
  if (input.hook && r.provenance === "observed") return false;
8077
- return !descendants(rows2, r).some((d) => d.id !== r.id && d.status !== "dropped" && (answered(d) || keptIds.has(d.id) || input.hook && d.provenance === "observed"));
7900
+ return !descendants(rows2, r).some((d) => d.id !== r.id && d.status !== "dropped" && (answered2(d) || keptIds.has(d.id) || input.hook && d.provenance === "observed"));
8078
7901
  });
8079
7902
  }
8080
7903
  return { branches: [...branches.values()].filter((b) => b.nodes.length), cut, kept, proposals, sweepable };
@@ -8308,8 +8131,8 @@ ${body}
8308
8131
 
8309
8132
  // ../daemon/src/crew.ts
8310
8133
  import { randomUUID as randomUUID8 } from "crypto";
8311
- import { mkdir as mkdir3 } from "fs/promises";
8312
- import { join as join11 } from "path";
8134
+ import { mkdir as mkdir2 } from "fs/promises";
8135
+ import { join as join10 } from "path";
8313
8136
  var READ_ONLY = {
8314
8137
  read_repo: /^(Read|Glob|Grep)$/,
8315
8138
  read_data: /(^|__)(list_connections|describe_source|query)$/
@@ -8324,8 +8147,8 @@ async function attempt6({ adapter, spec: spec2, onEvent, skill: skillName, name,
8324
8147
  if (skill.manifest.capabilities.some((c) => !holds.includes(c))) throw new Error(`The ${name} may not hold tools.`);
8325
8148
  const refused = grant.find((c) => !READ_ONLY[c] || !skill.manifest.capabilities.includes(c));
8326
8149
  if (refused) throw new Error(`The ${name} may not hold ${refused}.`);
8327
- const runDir = dir ? join11(spec2.runDir, dir) : spec2.runDir;
8328
- if (dir) await mkdir3(runDir, { recursive: true, mode: 448 });
8150
+ const runDir = dir ? join10(spec2.runDir, dir) : spec2.runDir;
8151
+ if (dir) await mkdir2(runDir, { recursive: true, mode: 448 });
8329
8152
  let result;
8330
8153
  let usage = null;
8331
8154
  await onEvent({ type: "text", text: say });
@@ -9604,8 +9427,8 @@ async function flashBranch(ctx, w, n, places) {
9604
9427
  }
9605
9428
  return out;
9606
9429
  }
9607
- function answersFor(answered, leaf) {
9608
- return answered.find((a) => a.ref.trim().toUpperCase() === leaf.ref.toUpperCase())?.solutions ?? [];
9430
+ function answersFor(answered2, leaf) {
9431
+ return answered2.find((a) => a.ref.trim().toUpperCase() === leaf.ref.toUpperCase())?.solutions ?? [];
9609
9432
  }
9610
9433
  async function deepDive(ctx, leaf, place, reads) {
9611
9434
  const whole = deepMaterial(ctx.known, leaf);
@@ -10382,9 +10205,9 @@ var told = (r) => ({
10382
10205
  // ../daemon/src/objective-definitions.ts
10383
10206
  import { execFile as execFile6 } from "child_process";
10384
10207
  import { randomUUID as randomUUID11 } from "crypto";
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";
10208
+ import { existsSync as existsSync3 } from "fs";
10209
+ import { mkdir as mkdir3, readFile as readFile6 } from "fs/promises";
10210
+ import { join as join11 } from "path";
10388
10211
  import { promisify as promisify6 } from "util";
10389
10212
  var execFileAsync6 = promisify6(execFile6);
10390
10213
  var DEFINITIONS_TIMEOUT_MS = 9e4;
@@ -10420,7 +10243,7 @@ function pickLines(raw) {
10420
10243
  }
10421
10244
  async function killRecorded(runDir) {
10422
10245
  try {
10423
- const pid = Number((await readFile7(join12(runDir, "harness.pid"), "utf8")).trim());
10246
+ const pid = Number((await readFile6(join11(runDir, "harness.pid"), "utf8")).trim());
10424
10247
  if (pid > 1) process.kill(pid, "SIGKILL");
10425
10248
  } catch {
10426
10249
  }
@@ -10440,8 +10263,8 @@ async function readDefinitions({ adapter, spec: spec2, publish: publish5, journa
10440
10263
  if (!hasRepo) return give({}, "No repository to read: the card offers the common definitions.", "ok");
10441
10264
  try {
10442
10265
  const skill = await loadSkill("define-objective");
10443
- const runDir = join12(spec2.runDir, "definitions");
10444
- await mkdir4(runDir, { recursive: true, mode: 448 });
10266
+ const runDir = join11(spec2.runDir, "definitions");
10267
+ await mkdir3(runDir, { recursive: true, mode: 448 });
10445
10268
  const schema = definitionsOutputSchema();
10446
10269
  const [lines, listed] = await Promise.all([countingLines(spec2.cwd), listTrackedFiles(spec2.cwd)]);
10447
10270
  const signal = AbortSignal.any([spec2.signal, AbortSignal.timeout(timeoutMs)]);
@@ -10502,7 +10325,7 @@ ${renderTrackedFiles(listed.files, void 0, listed.wiki)}
10502
10325
  if (!result) throw new Error("no answer");
10503
10326
  const parsed = DefinitionsAnswer.safeParse(JSON.parse(result));
10504
10327
  if (!parsed.success) throw new Error("an answer in the wrong shape");
10505
- const checked = checkProposals(parsed.data, (file) => existsSync4(join12(spec2.cwd, file)));
10328
+ const checked = checkProposals(parsed.data, (file) => existsSync3(join11(spec2.cwd, file)));
10506
10329
  const definitions2 = lever ? checked[lever] ? { [lever]: checked[lever] } : {} : checked;
10507
10330
  const kept = Object.values(definitions2).reduce((n, list) => n + (list?.length ?? 0), 0);
10508
10331
  const read6 = Object.values(definitions2).flat().filter((d) => d?.found).length;
@@ -10775,10 +10598,10 @@ function localDate(timezone, at) {
10775
10598
 
10776
10599
  // ../daemon/src/run-job.ts
10777
10600
  import { randomUUID as randomUUID12 } from "crypto";
10778
- import { existsSync as existsSync5 } from "fs";
10779
- import { mkdir as mkdir5, readFile as readFile8, rm as rm4 } from "fs/promises";
10780
- import { homedir as homedir4, tmpdir } from "os";
10781
- import { join as join13, resolve as resolve4 } from "path";
10601
+ import { existsSync as existsSync4 } from "fs";
10602
+ import { mkdir as mkdir4, readFile as readFile7, rm as rm4 } from "fs/promises";
10603
+ import { homedir as homedir3, tmpdir } from "os";
10604
+ import { join as join12, resolve as resolve4 } from "path";
10782
10605
 
10783
10606
  // ../daemon/src/events.ts
10784
10607
  var JobEventWriter = class {
@@ -10926,8 +10749,8 @@ async function runJob(job, ctx) {
10926
10749
  return outcome;
10927
10750
  }
10928
10751
  async function execute(job, skill, ctx, events) {
10929
- const runDir = join13(nanopmHome(), "run", job.id);
10930
- await mkdir5(runDir, { recursive: true, mode: 448 });
10752
+ const runDir = join12(nanopmHome(), "run", job.id);
10753
+ await mkdir4(runDir, { recursive: true, mode: 448 });
10931
10754
  const resumeSessionId = job.resume_of ? await sessionOf(ctx.client, job.resume_of) : job.harness_session_id ?? void 0;
10932
10755
  const sessionId = resumeSessionId ?? randomUUID12();
10933
10756
  await ctx.client.from("jobs").update({ harness_session_id: sessionId }).eq("id", job.id);
@@ -10978,7 +10801,7 @@ async function execute(job, skill, ctx, events) {
10978
10801
  if (quickRun2) {
10979
10802
  const cwd2 = scratchDir(job.id);
10980
10803
  try {
10981
- await mkdir5(cwd2, { recursive: true, mode: 448 });
10804
+ await mkdir4(cwd2, { recursive: true, mode: 448 });
10982
10805
  const caps = skill.manifest.capabilities;
10983
10806
  return await quickRun2({
10984
10807
  client: ctx.client,
@@ -11239,7 +11062,7 @@ function harnessWebFor(searchProvider, machineCanSearch) {
11239
11062
  }
11240
11063
  async function machineWebSearch() {
11241
11064
  try {
11242
- const parsed = JSON.parse(await readFile8(join13(nanopmHome(), "daemon.json"), "utf8"));
11065
+ const parsed = JSON.parse(await readFile7(join12(nanopmHome(), "daemon.json"), "utf8"));
11243
11066
  return parsed.webSearch === true;
11244
11067
  } catch {
11245
11068
  return false;
@@ -11281,7 +11104,7 @@ function buildHeader(job, project, cwd, harnessWeb = false, memory = false) {
11281
11104
  return lines.join("\n");
11282
11105
  }
11283
11106
  function scratchDir(jobId) {
11284
- return join13(tmpdir(), `nanopm-run-${jobId}`);
11107
+ return join12(tmpdir(), `nanopm-run-${jobId}`);
11285
11108
  }
11286
11109
  async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
11287
11110
  const refuse2 = (why) => {
@@ -11290,12 +11113,12 @@ async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
11290
11113
  const requested = params.repo_path;
11291
11114
  if (typeof requested !== "string" || !requested) {
11292
11115
  if (mustBeTheClone) refuse2("the job named no repository");
11293
- await mkdir5(fallback, { recursive: true, mode: 448 });
11116
+ await mkdir4(fallback, { recursive: true, mode: 448 });
11294
11117
  return fallback;
11295
11118
  }
11296
11119
  if (!job.project_id) {
11297
11120
  if (mustBeTheClone) refuse2("the job belongs to no project");
11298
- await mkdir5(fallback, { recursive: true, mode: 448 });
11121
+ await mkdir4(fallback, { recursive: true, mode: 448 });
11299
11122
  return fallback;
11300
11123
  }
11301
11124
  const { data: clone } = await ctx.client.from("clones").select("repo_path").eq("project_id", job.project_id).eq("daemon_id", ctx.daemonId).maybeSingle();
@@ -11303,12 +11126,12 @@ async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
11303
11126
  if (!allowed || resolve4(allowed) !== resolve4(requested)) {
11304
11127
  if (mustBeTheClone) refuse2(`${requested} is not the clone recorded for this project on this machine. Run any \`nanopm\` command inside your clone here to record it`);
11305
11128
  console.error(`job ${job.id}: repo_path ${requested} is not this machine's clone; running in a scratch directory instead`);
11306
- await mkdir5(fallback, { recursive: true, mode: 448 });
11129
+ await mkdir4(fallback, { recursive: true, mode: 448 });
11307
11130
  return fallback;
11308
11131
  }
11309
- if (existsSync5(allowed)) return resolve4(allowed);
11132
+ if (existsSync4(allowed)) return resolve4(allowed);
11310
11133
  if (mustBeTheClone) refuse2(`${allowed} is not on this machine any more`);
11311
- await mkdir5(fallback, { recursive: true, mode: 448 });
11134
+ await mkdir4(fallback, { recursive: true, mode: 448 });
11312
11135
  return fallback;
11313
11136
  }
11314
11137
  function asData8(label, body) {
@@ -11317,10 +11140,10 @@ ${body}
11317
11140
  </${label}>`;
11318
11141
  }
11319
11142
  function harnessEnv(timeoutMs, source = process.env) {
11320
- const home = source.HOME ?? homedir4();
11143
+ const home = source.HOME ?? homedir3();
11321
11144
  const env = {
11322
11145
  HOME: home,
11323
- PATH: [source.PATH ?? "/usr/local/bin:/usr/bin:/bin", join13(home, ".local", "bin")].join(":"),
11146
+ PATH: [source.PATH ?? "/usr/local/bin:/usr/bin:/bin", join12(home, ".local", "bin")].join(":"),
11324
11147
  MCP_TOOL_TIMEOUT: String(timeoutMs)
11325
11148
  };
11326
11149
  for (const key of ["USER", "LANG", "TMPDIR", "SHELL", "ANTHROPIC_API_KEY", "CLAUDE_CONFIG_DIR"]) {
@@ -11351,6 +11174,362 @@ function usageTokens(usage) {
11351
11174
  return usage.inputTokens + (usage.cacheWriteTokens ?? 0) + usage.outputTokens;
11352
11175
  }
11353
11176
 
11177
+ // ../daemon/src/build-job.ts
11178
+ var PLAN_TOOL = "mcp__nanopm__build_plan";
11179
+ var ASK_TOOL = "mcp__nanopm__ask_user";
11180
+ var STEP_TOOL = "mcp__nanopm__build_step";
11181
+ var CLAUDE_CODE_MIN = "2.1.283";
11182
+ async function prepareBuild(client, job, clone, adapter) {
11183
+ const params = job.params ?? {};
11184
+ const dir = buildDir(homedir4(), job.id);
11185
+ let made = false;
11186
+ try {
11187
+ const [solution, machine, history] = await Promise.all([
11188
+ loadSolution(client, job.project_id, String(params.solution_id ?? "")),
11189
+ machineRefusal(adapter),
11190
+ loadHistory(client, job)
11191
+ ]);
11192
+ const refused = buildRefusal(solution, { anyway: params.anyway === true || Boolean(history.place) }) ?? machine;
11193
+ if (refused) return { ok: false, outcome: failed(refused) };
11194
+ const network = async (at) => buildNetwork(await readdir2(at));
11195
+ if (history.place) {
11196
+ const { place } = history;
11197
+ if (!existsSync5(place.dir)) return { ok: false, outcome: failed(`this build's copy of the code is gone (${place.dir}): build it again`) };
11198
+ const worktree2 = { dir: place.dir, admin: join13(await commonGitDir(clone), "worktrees", basename(place.dir)), commondir: "../..\n" };
11199
+ await startSolution(client, job.project_id, solution.ref, job.id);
11200
+ return { ok: true, build: { solution, clone, ...place, worktree: worktree2, network: await network(place.dir), history } };
11201
+ }
11202
+ const branch = buildBranch(solution.ref, solution.title);
11203
+ await mkdir5(join13(dir, ".."), { recursive: true, mode: 448 });
11204
+ const { base, fork, worktree } = await addWorktree(clone, dir, branch);
11205
+ made = true;
11206
+ await appendJobEvent(client, job.id, "worktree", { dir, branch, base, fork }, "nanopm");
11207
+ await handOverTheSpec(worktree, specForAgent(solution.agent_spec, solution.spec));
11208
+ await startSolution(client, job.project_id, solution.ref, job.id);
11209
+ return { ok: true, build: { solution, clone, dir, worktree, branch, base, fork, network: await network(dir), history } };
11210
+ } catch (err) {
11211
+ if (made) await removeWorktree(clone, dir);
11212
+ return { ok: false, outcome: failed(errorMessage(err)) };
11213
+ }
11214
+ }
11215
+ async function runBuildJob(input) {
11216
+ const { client, job, adapter, spec: spec2, events, log, verifyCommand: verifyCommand2 } = input;
11217
+ const { solution, clone, dir, worktree, branch, base, fork, history } = input.build;
11218
+ const was = wasOf(job.params);
11219
+ let plan = history.plan;
11220
+ const done = new Set(history.done);
11221
+ let usage;
11222
+ let said = history.report;
11223
+ const stop2 = async (message, opts = { keep: true }) => {
11224
+ await unstartSolution(client, job.project_id, solution.ref, was).catch(() => void 0);
11225
+ if (opts.keep && !await merging(worktree)) await commitAll(worktree, `${solution.ref}: unfinished \u2014 ${solution.title}`).catch(() => null);
11226
+ else await removeWorktree(clone, dir);
11227
+ return { ...failed(opts.keep ? `${message}. What was done is on ${branch}, in ${dir}.` : message), usage };
11228
+ };
11229
+ const park = async (why) => {
11230
+ if ("ask" in why) {
11231
+ await events.write("question", { ...why.ask, ...why.ask.about ? { report: said } : plan ? {} : { about: "plan" } });
11232
+ await log("build.ask", `${solution.ref} waits on the founder`);
11233
+ } else if (why.resetsAt) {
11234
+ await events.write("paused", { why: "limit", resets_at: new Date(why.resetsAt * 1e3).toISOString() });
11235
+ await log("build.pause", `${solution.ref} paused until the Claude limit resets`);
11236
+ } else {
11237
+ 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" });
11238
+ }
11239
+ return { status: "waiting", usage };
11240
+ };
11241
+ const session2 = async (prompt, resume3) => {
11242
+ let end = { stop: failed("the agent produced no result") };
11243
+ for await (const event of adapter.run({ ...spec2, prompt, resumeSessionId: resume3 ? spec2.sessionId : void 0 })) {
11244
+ await input.onEvent(event);
11245
+ if (event.type === "tool_use" && event.tool === PLAN_TOOL && !plan) {
11246
+ plan = stepsOf(event.input);
11247
+ if (plan) await events.write("plan", { steps: plan });
11248
+ } else if (event.type === "tool_result" && event.tool === STEP_TOOL && !event.isError) {
11249
+ const { step, summary } = event.input ?? {};
11250
+ const index = Number(step) - 1;
11251
+ if (Number.isInteger(index) && index >= 0 && !done.has(index)) {
11252
+ const says = plan?.[index] ?? `step ${index + 1}`;
11253
+ const commit = await commitAll(worktree, `${solution.ref}: ${says}`, summary);
11254
+ done.add(index);
11255
+ await events.write("step", { step: index + 1, says, commit: commit?.sha ?? null, files: commit?.files ?? 0 });
11256
+ }
11257
+ } else if (event.type === "finished") {
11258
+ usage = sumUsage(usage, event.usage);
11259
+ said = event.result;
11260
+ end = null;
11261
+ } else if (event.type === "deferred" && event.tool === ASK_TOOL) {
11262
+ usage = sumUsage(usage, event.usage);
11263
+ end = { park: { ask: askOf(event.input) } };
11264
+ } else if (event.type === "failed") {
11265
+ end = event.code === "rate_limit" ? { park: { resetsAt: event.resetsAt } } : { stop: { status: "failed", error: { code: event.code, message: event.message } } };
11266
+ }
11267
+ }
11268
+ return end;
11269
+ };
11270
+ const resolving = (files) => `\`${base}\` moved while you were building, and it is merged into your branch: ${files.map((f) => `\`${f}\``).join(", ")} still have conflict markers. Resolve each so both sides' intent holds, remove every marker, run the project's check again, then give your report again, whole and in the same form, with what you kept under *Decisions I made*. Do not call build_step, and do not commit: NanoPM commits the merge.`;
11271
+ const left = async () => {
11272
+ if (!finished()) return carryOn(plan, done);
11273
+ if (history.handed) return null;
11274
+ const files = await conflictsLeft(worktree, base);
11275
+ return files.length ? resolving(files) : null;
11276
+ };
11277
+ const openPr = async (reviewUrl) => {
11278
+ const [noPr, page] = await Promise.all([ghReady(worktree), solutionPage(client, job.project_id, solution.ref)]);
11279
+ if (noPr) return pushed(null, reviewUrl, noPr);
11280
+ const pr = await openPullRequest(worktree, { branch, base, title: `${solution.ref}: ${solution.title}`, body: prBody(said, page) });
11281
+ if (!pr.ok) return park({ ask: { question: "The branch is pushed, but GitHub refused the pull request.", why: pr.reason, options: [BUILD_REFUSED.again, BUILD_REFUSED.open], about: "refused" } });
11282
+ return pushed(pr, null);
11283
+ };
11284
+ const pushed = async (pr, reviewUrl, why = "not yet") => {
11285
+ if (pr) {
11286
+ await events.write("pr", { number: pr.number, url: pr.url });
11287
+ await log("build.pr", `draft pull request #${pr.number} for ${solution.ref}`);
11288
+ }
11289
+ await removeWorktree(clone, dir);
11290
+ return {
11291
+ status: "succeeded",
11292
+ result: [said.trim(), "", pr ? `Draft pull request #${pr.number} \xB7 ${pr.url}` : `Pushed ${branch}. No pull request: ${why}.${reviewUrl ? ` ${reviewUrl}` : ""}`].filter(Boolean).join("\n"),
11293
+ data: { branch, session: spec2.sessionId, ...pr ? { pr: { number: pr.number, url: pr.url } } : { review_url: reviewUrl ?? null } },
11294
+ usage
11295
+ };
11296
+ };
11297
+ const resume2 = history.resume;
11298
+ const check = verifyCommand2 ? `
11299
+
11300
+ 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*.` : "";
11301
+ const finished = () => Boolean(plan && !stepsLeft(plan, done).length);
11302
+ if (resume2?.why === "conflict" && chose(resume2.answer, BUILD_CONFLICT.mine)) {
11303
+ const opened = await mergeBase(worktree, base).catch((err) => ({ error: errorMessage(err) }));
11304
+ if ("error" in opened) return stop2(opened.error);
11305
+ await events.write("handed", {});
11306
+ return stop2(`It is yours to resolve: ${opened.files.join(", ")} carry conflict markers in that copy. Edit them, then Continue (nanopm build ${solution.ref} --continue): NanoPM commits the merge`);
11307
+ }
11308
+ if (resume2?.why === "refused" && chose(resume2.answer, BUILD_REFUSED.open)) return pushed(null, await newPullRequestUrl(worktree, branch), "you open it on GitHub");
11309
+ if (!resume2) {
11310
+ await log("build.start", `building ${solution.ref} on ${branch}, in a copy of your code`);
11311
+ await events.write("text", { text: `Working on ${branch}, in a copy of your code. Your own checkout is not touched.` });
11312
+ } else {
11313
+ await events.write("text", { text: AGAIN[resume2.why] });
11314
+ if (resume2.why === "answer" && spec2.answerFile) await writeFile5(spec2.answerFile, answerForAgent(resume2.answer), { mode: 384 });
11315
+ }
11316
+ if (resume2?.why === "refused") return openPr();
11317
+ let turn = null;
11318
+ if (!resume2) turn = await session2(spec2.prompt + check, false);
11319
+ else if (resume2.why === "answer") turn = await session2("", true);
11320
+ else if (resume2.why === "conflict") {
11321
+ const opened = await mergeBase(worktree, base).catch((err) => ({ error: errorMessage(err) }));
11322
+ if ("error" in opened) return stop2(opened.error);
11323
+ if (opened.files.length) turn = await session2(resolving(opened.files), true);
11324
+ } else {
11325
+ const next = await left();
11326
+ if (next) turn = await session2(`${resume2.why === "limit" ? "Your Claude limit has reset." : "You were interrupted: the machine stopped."} ${next}`, true);
11327
+ }
11328
+ for (let resumes = 0; !turn; resumes += 1) {
11329
+ const next = await left();
11330
+ if (!next) break;
11331
+ if (resumes >= BUILD_RESUMES_MAX) {
11332
+ if (finished()) break;
11333
+ const steps = stepsLeft(plan, done).length;
11334
+ return stop2(plan ? `Nano stopped with ${steps} step${steps === 1 ? "" : "s"} of its plan left` : "Nano stopped without saying its plan");
11335
+ }
11336
+ turn = await session2(next, true);
11337
+ }
11338
+ if (turn && "park" in turn) return park(turn.park);
11339
+ if (turn) return stop2(turn.stop.error?.message ?? "the build stopped");
11340
+ const marked = await conflictsLeft(worktree, base);
11341
+ if (marked.length) return park({ ask: conflictAsk(base, marked) });
11342
+ await commitAll(worktree, `${solution.ref}: ${solution.title}`);
11343
+ if (!await commitsSinceFork(worktree, fork)) return stop2("the build finished without changing anything", { keep: false });
11344
+ if (!await containsBase(worktree, base)) {
11345
+ if (await mergedSince(worktree, fork)) {
11346
+ const opened = await mergeBase(worktree, base).catch((err) => ({ error: errorMessage(err) }));
11347
+ if ("error" in opened) return stop2(opened.error);
11348
+ if (opened.files.length) return park({ ask: conflictAsk(base, opened.files) });
11349
+ await commitAll(worktree, `${solution.ref}: ${base} merged in`);
11350
+ } else {
11351
+ const rebased = await rebaseOnto(worktree, base);
11352
+ if (!rebased.ok && !rebased.files.length) return stop2(`the rebase onto ${base} failed: see git status`);
11353
+ if (!rebased.ok) return park({ ask: conflictAsk(base, rebased.files) });
11354
+ }
11355
+ }
11356
+ const push = await pushBranch(worktree, branch);
11357
+ if (!push.pushed) return stop2(`the push failed: ${push.reason}`);
11358
+ return openPr(push.reviewUrl);
11359
+ }
11360
+ var conflictAsk = (base, files) => ({
11361
+ question: `Someone changed the same files on ${base} while Nano was building.`,
11362
+ why: "Nano can try to merge them and run your checks again, or you resolve it yourself in your terminal. Nothing is pushed until it is resolved.",
11363
+ files,
11364
+ options: [BUILD_CONFLICT.resolve, BUILD_CONFLICT.mine],
11365
+ about: "conflict"
11366
+ });
11367
+ var failed = (message) => ({ status: "failed", error: { code: "harness_error", message } });
11368
+ var AGAIN = {
11369
+ answer: "Your answer is in: carrying on.",
11370
+ limit: "Your Claude limit has reset: carrying on.",
11371
+ lost: "Picked up again where it stopped.",
11372
+ conflict: "Merging the main branch in, and resolving it.",
11373
+ refused: "Asking GitHub for the pull request again."
11374
+ };
11375
+ function askOf(input) {
11376
+ const { question, options, why } = input ?? {};
11377
+ return {
11378
+ question: String(question ?? ""),
11379
+ ...Array.isArray(options) ? { options: options.map(String) } : {},
11380
+ ...typeof why === "string" ? { why } : {}
11381
+ };
11382
+ }
11383
+ async function loadHistory(client, job) {
11384
+ const named = (job.params ?? {}).origin;
11385
+ 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;
11386
+ 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}`);
11387
+ return loadBuildHistory(client, [.../* @__PURE__ */ new Set([origin, job.id, ...(runs ?? []).map((r) => r.id)])], { home: homedir4(), jobId: origin });
11388
+ }
11389
+ async function loadSolution(client, projectId, id) {
11390
+ const { data, error } = await client.from("solutions").select(BUILD_COLUMNS).eq("id", id).eq("project_id", projectId).is("superseded_by", null).maybeSingle();
11391
+ if (error) throw new Error(`could not read the solution: ${error.message}`);
11392
+ if (!data) throw new Error("that solution is not on this project any more");
11393
+ return data;
11394
+ }
11395
+ async function machineRefusal(adapter) {
11396
+ const found = await adapter.detect();
11397
+ const version = /\d+\.\d+\.\d+/.exec(found.version ?? "")?.[0];
11398
+ if (version && !newer(CLAUDE_CODE_MIN, version)) return null;
11399
+ return `a build needs Claude Code ${CLAUDE_CODE_MIN} or later on this machine${found.version ? ` (it has ${found.version})` : ""}: run claude update`;
11400
+ }
11401
+ async function handOverTheSpec(worktree, agentSpec) {
11402
+ await mkdir5(join13(worktree.dir, ".nanopm"), { recursive: true });
11403
+ await writeFile5(join13(worktree.dir, ".nanopm", "SPEC.md"), agentSpec);
11404
+ const exclude = join13(await commonGitDir(worktree), "info", "exclude");
11405
+ const current = await readFile8(exclude, "utf8").catch(() => "");
11406
+ if (!current.split("\n").includes(".nanopm/")) {
11407
+ await mkdir5(join13(exclude, ".."), { recursive: true });
11408
+ await writeFile5(exclude, `${current}${current && !current.endsWith("\n") ? "\n" : ""}.nanopm/
11409
+ `);
11410
+ }
11411
+ }
11412
+ function stepsOf(input) {
11413
+ const steps = input?.steps;
11414
+ return Array.isArray(steps) && steps.length && steps.every((s) => typeof s === "string") ? steps : null;
11415
+ }
11416
+ function carryOn(plan, done) {
11417
+ 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.";
11418
+ const left = stepsLeft(plan, done);
11419
+ return [
11420
+ `Steps ${left.map((i) => i + 1).join(", ")} of your plan are not done yet:`,
11421
+ ...left.map((i) => `${i + 1}. ${plan[i]}`),
11422
+ "",
11423
+ "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."
11424
+ ].join("\n");
11425
+ }
11426
+ async function solutionPage(client, projectId, ref) {
11427
+ const { data } = await client.from("projects").select("slug").eq("id", projectId).maybeSingle();
11428
+ return data?.slug ? `${webUrl()}/p/${data.slug}/roadmap/${ref}` : null;
11429
+ }
11430
+ function prBody(said, page) {
11431
+ return [
11432
+ said.trim(),
11433
+ page ? `
11434
+ The solution and its spec: ${page}` : "",
11435
+ "\n\u2014 Built by NanoPM with Claude Code. A draft: review before merging."
11436
+ ].filter(Boolean).join("\n");
11437
+ }
11438
+ async function startSolution(client, projectId, ref, jobId) {
11439
+ const { error } = await client.rpc("start_solution", { p_project_id: projectId, p_ref: ref, p_job_id: jobId });
11440
+ if (error) throw new Error(pgMessage(error));
11441
+ }
11442
+ async function unstartSolution(client, projectId, ref, was) {
11443
+ await client.rpc("unstart_solution", { p_project_id: projectId, p_ref: ref, p_to: was });
11444
+ }
11445
+ async function requeueLostBuild(client, job) {
11446
+ const params = job.params ?? {};
11447
+ if ((job.attempt ?? 1) < 2) {
11448
+ 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");
11449
+ if (data?.length) return true;
11450
+ }
11451
+ const { data: solution } = await client.from("solutions").select("ref").eq("id", params.solution_id ?? "").maybeSingle();
11452
+ if (job.project_id && solution) await unstartSolution(client, job.project_id, solution.ref, wasOf(job.params)).catch(() => void 0);
11453
+ return false;
11454
+ }
11455
+ async function resumePausedBuilds(client, daemonId, now = Date.now()) {
11456
+ const { data: parked } = await client.from("jobs").select("id").eq("skill", BUILD_SKILL).eq("status", "waiting").eq("assigned_daemon_id", daemonId);
11457
+ const ids = (parked ?? []).map((j) => j.id);
11458
+ if (!ids.length) return [];
11459
+ 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 });
11460
+ const last = /* @__PURE__ */ new Map();
11461
+ for (const e of stops ?? []) if (!last.has(e.job_id)) last.set(e.job_id, e);
11462
+ const due = ids.filter((id) => {
11463
+ const stop2 = last.get(id);
11464
+ return stop2?.kind === "paused" && Date.parse(String(stop2.payload?.resets_at ?? "")) <= now;
11465
+ });
11466
+ if (!due.length) return [];
11467
+ const { data } = await client.from("jobs").update({ status: "queued", assigned_daemon_id: null }).in("id", due).eq("status", "waiting").select("id");
11468
+ return (data ?? []).map((j) => j.id);
11469
+ }
11470
+ async function shippedBuilds(client, daemonId, userId) {
11471
+ const { data: building } = await client.from("solutions").select("id, ref, project_id").eq("status", "building");
11472
+ if (!building?.length) return [];
11473
+ const [{ data: built }, { data: clones }] = await Promise.all([
11474
+ client.from("jobs").select("id, params, pr:result->pr").eq("skill", BUILD_SKILL).eq("status", "succeeded").eq("assigned_daemon_id", daemonId).eq("requested_by", userId).in("params->>solution_id", building.map((s) => s.id)),
11475
+ client.from("clones").select("project_id, repo_path").eq("daemon_id", daemonId)
11476
+ ]);
11477
+ const cloneOf = new Map((clones ?? []).map((c) => [c.project_id, c.repo_path]));
11478
+ const shipped = [];
11479
+ for (const j of built ?? []) {
11480
+ const solution = building.find((s) => s.id === j.params?.solution_id);
11481
+ const number = j.pr?.number;
11482
+ const clone = solution ? cloneOf.get(solution.project_id) : void 0;
11483
+ if (!solution || typeof number !== "number" || number <= 0 || !clone || !existsSync5(clone) || !await pullRequestMerged(clone, number)) continue;
11484
+ const { error } = await client.rpc("finish_solution", { p_project_id: solution.project_id, p_ref: solution.ref, p_job_id: j.id });
11485
+ if (error) continue;
11486
+ await logActivity({ client }, { id: j.id, project_id: solution.project_id }, "shipped", `Pull request #${number} merged: ${solution.ref} shipped`);
11487
+ shipped.push(j.id);
11488
+ }
11489
+ return shipped;
11490
+ }
11491
+ async function tellTheApp(accessToken, jobId, fetchImpl = fetch) {
11492
+ try {
11493
+ const response = await fetchImpl(`${webUrl()}/api/build/news`, {
11494
+ method: "POST",
11495
+ headers: { "content-type": "application/json", authorization: `Bearer ${accessToken}` },
11496
+ body: JSON.stringify({ job_id: jobId })
11497
+ });
11498
+ const body = await response.json().catch(() => ({}));
11499
+ return response.ok ? body.said ?? "nothing to say" : `not said \u2014 ${body.error ?? `the app answered ${response.status}`}`;
11500
+ } catch (err) {
11501
+ return `not said \u2014 ${errorMessage(err)}`;
11502
+ }
11503
+ }
11504
+
11505
+ // ../daemon/src/review-preflight.ts
11506
+ async function reviewWithoutData(client, job) {
11507
+ if (job.skill !== "review" || !job.project_id) return null;
11508
+ if (job.trigger === "user") return null;
11509
+ const { data: connections } = await client.from("connections").select("id, status").eq("project_id", job.project_id).is("removed_at", null);
11510
+ const usable = (connections ?? []).filter((c) => c.status === "ok");
11511
+ if (usable.length) return null;
11512
+ const attached = (connections ?? []).length;
11513
+ const summary = attached ? `No review this time: ${attached} data source${attached === 1 ? " is" : "s are"} attached but none is working. Run \`nanopm connect --list\` to see what is wrong.` : "No review this time: nothing is connected, so there was nothing to read. Run `nanopm connect` to attach your database.";
11514
+ const { error } = await client.from("reviews").insert({
11515
+ project_id: job.project_id,
11516
+ summary,
11517
+ confirms: [],
11518
+ contradicts: [],
11519
+ job_id: job.id
11520
+ });
11521
+ if (error) return { status: "failed", error: { code: "harness_error", message: `could not record that there was nothing to read: ${error.message}` } };
11522
+ await client.from("activity_log").insert({
11523
+ project_id: job.project_id,
11524
+ job_id: job.id,
11525
+ actor: "daemon",
11526
+ step: "review.skipped",
11527
+ summary: attached ? "no working data source" : "nothing connected",
11528
+ outcome: "ok"
11529
+ });
11530
+ return { status: "succeeded", result: summary };
11531
+ }
11532
+
11354
11533
  // ../daemon/src/daemon.ts
11355
11534
  var SESSION_HEADROOM_MS = 10 * 60 * 1e3;
11356
11535
  var LONGEST_CAP_MS = 3 * 60 * 60 * 1e3;
@@ -11402,6 +11581,8 @@ async function runDaemon(options = {}) {
11402
11581
  const DEPLOY_CHECK_MS = 30 * 6e4;
11403
11582
  const PAUSE_CHECK_MS = 6e4;
11404
11583
  let lastPauseCheck = 0;
11584
+ const SHIP_CHECK_MS = 10 * 6e4;
11585
+ let lastShipCheck = 0;
11405
11586
  const UPDATE_CHECK_MS = 60 * 6e4;
11406
11587
  const updatable = !options.once && installKind() === "installer";
11407
11588
  let lastUpdateCheck = Date.now() - UPDATE_CHECK_MS + 5 * 6e4;
@@ -11453,6 +11634,9 @@ async function runDaemon(options = {}) {
11453
11634
  const digest = await sendDigestFor(auth.client, auth.accessToken, job.id);
11454
11635
  if (digest) log(`digest for job ${job.id}: ${digest.ok ? digest.detail : `not sent \u2014 ${digest.detail}`}`);
11455
11636
  }
11637
+ if (job.skill === BUILD_SKILL && (outcome.status === "waiting" || outcome.status === "succeeded")) {
11638
+ log(`build ${job.id}: ${await tellTheApp(auth.accessToken, job.id)}`);
11639
+ }
11456
11640
  await chainAfter(auth, job, outcome, log);
11457
11641
  processed += 1;
11458
11642
  if (options.once) break;
@@ -11463,6 +11647,10 @@ async function runDaemon(options = {}) {
11463
11647
  lastPauseCheck = Date.now();
11464
11648
  for (const id of await resumePausedBuilds(auth.client, daemonId).catch(() => [])) log(`build ${id}: the Claude limit reset, carrying on`);
11465
11649
  }
11650
+ if (!options.once && Date.now() - lastShipCheck > SHIP_CHECK_MS) {
11651
+ lastShipCheck = Date.now();
11652
+ for (const id of await shippedBuilds(auth.client, daemonId, auth.userId).catch(() => [])) log(`build ${id}: its pull request merged, shipped`);
11653
+ }
11466
11654
  if (!options.once && Date.now() - lastDeployCheck > DEPLOY_CHECK_MS) {
11467
11655
  lastDeployCheck = Date.now();
11468
11656
  for (const check of await reviewDeploys(auth.client, daemonId).catch(() => [])) {
@@ -14264,24 +14452,24 @@ async function streamJob(client, jobId, opts) {
14264
14452
  let cancelSent = false;
14265
14453
  let cancelDeadline = Infinity;
14266
14454
  let finalStatus = null;
14267
- const answered = /* @__PURE__ */ new Set();
14455
+ const answered2 = /* @__PURE__ */ new Set();
14268
14456
  const shownAnswers = /* @__PURE__ */ new Set();
14269
14457
  for (; ; ) {
14270
14458
  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);
14459
+ for (const ev of events ?? []) if (ev.kind === "answer") answered2.add(ev.seq);
14272
14460
  for (const ev of events ?? []) {
14273
14461
  lastId = Math.max(lastId, Number(ev.id));
14274
14462
  const payload = ev.payload ?? {};
14275
14463
  if (ev.kind === "answer" && shownAnswers.has(Number(ev.id))) continue;
14276
14464
  show(progress.feed({ kind: ev.kind, payload, author_kind: ev.author_kind }));
14277
- if (ev.kind === "question" && payload.about === "site" && !answered.has(ev.seq)) {
14278
- answered.add(ev.seq);
14465
+ if (ev.kind === "question" && payload.about === "site" && !answered2.has(ev.seq)) {
14466
+ answered2.add(ev.seq);
14279
14467
  renderer.settle(" (answer in the browser)");
14280
14468
  continue;
14281
14469
  }
14282
- if (ev.kind === "question" && opts.answerer && !cancelSent && !answered.has(ev.seq)) {
14283
- if (answered.size === 0 && opts.link) renderer.settle(` (answer here, or in the browser: ${opts.link})`);
14284
- answered.add(ev.seq);
14470
+ if (ev.kind === "question" && opts.answerer && !cancelSent && !answered2.has(ev.seq)) {
14471
+ if (answered2.size === 0 && opts.link) renderer.settle(` (answer here, or in the browser: ${opts.link})`);
14472
+ answered2.add(ev.seq);
14285
14473
  const q = payload;
14286
14474
  renderer.pause();
14287
14475
  const outcome = await answerOrWatch(client, jobId, ev.seq, q, opts.answerer, opts.cancelSignal, opts.pollMs);
@@ -14416,7 +14604,7 @@ import { promisify as promisify8 } from "util";
14416
14604
  // package.json
14417
14605
  var package_default = {
14418
14606
  name: "@nanopm/cli",
14419
- version: "0.1.12",
14607
+ version: "0.1.15",
14420
14608
  description: "An autonomous product manager for builders who run several small products. Runs on your machine, drives your own coding harness.",
14421
14609
  type: "module",
14422
14610
  license: "MIT",
@@ -16132,13 +16320,13 @@ async function spec(ref, opts) {
16132
16320
  const auth = await authenticate();
16133
16321
  const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
16134
16322
  const wanted = ref.trim().toUpperCase();
16135
- const { data: solution } = await auth.client.from("solutions").select("id, ref, title, agent_spec").eq("project_id", project.id).eq("ref", wanted).maybeSingle();
16323
+ const { data: solution } = await auth.client.from("solutions").select("id, ref, title, agent_spec, spec").eq("project_id", project.id).eq("ref", wanted).maybeSingle();
16136
16324
  if (!solution) throw new Error(`no solution ${wanted}. Run \`nanopm roadmap\` to see them.`);
16137
16325
  if (opts.write && opts.rework) throw new Error("--write or --rework, not both.");
16138
16326
  const mode = opts.rework ? "rework" : opts.write ? "write" : null;
16139
16327
  if (!mode) {
16140
16328
  if (!solution.agent_spec) return `${solution.ref} has no specs yet: \`nanopm spec ${solution.ref} --write\`, or *Write the specs* on its sheet.`;
16141
- return solution.agent_spec.trimEnd();
16329
+ return specForAgent(solution.agent_spec, solution.spec).trimEnd();
16142
16330
  }
16143
16331
  const daemon = await awakeDaemon(auth.client, auth.userId);
16144
16332
  if (!daemon) throw new Error("Your machine is asleep: none of your daemons has been seen in the last two minutes, so nothing was queued. `nanopm daemon install` starts it; then ask again.");
@@ -17024,7 +17212,7 @@ Review this suggestion on the Problems page.`);
17024
17212
  async function problemList(client, projectId, color) {
17025
17213
  const list = await problemRows(client, projectId);
17026
17214
  const { count: manualMerges } = await client.from("decisions").select("id", { count: "exact", head: true }).eq("project_id", projectId).eq("kind", "merge");
17027
- return renderProblems(list, await openQuestions(client, projectId, list), {
17215
+ return renderProblems(list, await openQuestions2(client, projectId, list), {
17028
17216
  color,
17029
17217
  manualMerges: manualMerges ?? 0
17030
17218
  });
@@ -17059,7 +17247,7 @@ async function keptSeparate(client, projectId) {
17059
17247
  return a && b ? [a, b].sort().join("|") : null;
17060
17248
  }).filter((k) => Boolean(k));
17061
17249
  }
17062
- async function openQuestions(client, projectId, list) {
17250
+ async function openQuestions2(client, projectId, list) {
17063
17251
  const { data } = await client.from("questions").select("refs, problems!questions_problem_id_fkey(ref)").eq("project_id", projectId).eq("kind", "same").is("answered_at", null).order("created_at", { ascending: false });
17064
17252
  const open = new Set(list.map((p) => p.ref));
17065
17253
  const pairs = [];