@nanopm/cli 0.1.8 → 0.1.11

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
@@ -156,7 +156,8 @@ var NEEDS_GO_AHEAD = /* @__PURE__ */ new Set(["execute_after_validation"]);
156
156
  function requiresGoAhead(autonomy2) {
157
157
  return NEEDS_GO_AHEAD.has(autonomy2);
158
158
  }
159
- function capCapabilities(requested, autonomy2) {
159
+ function capCapabilities(requested, autonomy2, opts = {}) {
160
+ if (opts.asked) return [...requested];
160
161
  const denied = CAPPED_BELOW[autonomy2];
161
162
  const granted = requested.filter((c) => !denied.has(c));
162
163
  if (granted.includes("write_repo") && requiresGoAhead(autonomy2) && !granted.includes("ask_user")) {
@@ -387,6 +388,12 @@ var SkillManifestSchema = z2.object({
387
388
  },
388
389
  { message: "timeout_deep must look like 3h, 5m, 90s" }
389
390
  ).optional(),
391
+ /**
392
+ * Run only when the founder asks for it on one item, by a click or a command, never by Nano on
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).
395
+ */
396
+ asked: z2.boolean().default(false),
390
397
  /** The first user message sent to the harness, unless the job params provide one. */
391
398
  prompt: z2.string().min(1),
392
399
  /**
@@ -628,14 +635,6 @@ var SECRET_PATTERNS = [
628
635
  "**/credentials.json",
629
636
  "**/service-account*.json"
630
637
  ];
631
- function looksLikeASecret(path) {
632
- const base = path.replace(/\\/g, "/").split("/").pop() ?? "";
633
- return SECRET_PATTERNS.some((pattern) => {
634
- const glob = pattern.replace(/^\*\*\//, "");
635
- const re = new RegExp(`^${glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`);
636
- return re.test(base);
637
- });
638
- }
639
638
 
640
639
  // ../shared/src/stream.ts
641
640
  function formatStreamEvent(event) {
@@ -944,7 +943,7 @@ var AFTER_DEEP = {
944
943
  function daemonQueues(skill, depth = 0, parent = null, deep = false) {
945
944
  if (deep) return depth >= CHAIN_DEPTH_MAX ? [] : AFTER_DEEP[skill] ?? [];
946
945
  const theirs = new Set(SQL_OWNED[skill] ?? []);
947
- return chainedAfter(skill, depth, parent).filter((next2) => !theirs.has(next2));
946
+ return chainedAfter(skill, depth, parent).filter((next) => !theirs.has(next));
948
947
  }
949
948
  var NOT_CHAINED_BY_DAEMON = ["ingest"];
950
949
  var CHAIN_DEPTH_MAX = 3;
@@ -954,7 +953,7 @@ function chainedAfter(skill, depth = 0, parent = null) {
954
953
  const gates = ONLY_AFTER[skill] ?? {};
955
954
  const bars2 = NEVER_AFTER[skill] ?? {};
956
955
  return (CHAINED[skill] ?? []).filter(
957
- (next2) => (!gates[next2] || parent !== null && gates[next2].includes(parent)) && !(parent !== null && bars2[next2]?.includes(parent))
956
+ (next) => (!gates[next] || parent !== null && gates[next].includes(parent)) && !(parent !== null && bars2[next]?.includes(parent))
958
957
  );
959
958
  }
960
959
  var CHAINED_SKILLS = [...new Set(Object.values(CHAINED).flatMap((s) => s ?? []))];
@@ -974,7 +973,7 @@ var SKILL_SENTENCE = {
974
973
  direction: "working out where this is going",
975
974
  solutions: "proposing options",
976
975
  review: "reviewing",
977
- next: "building",
976
+ build: "building",
978
977
  ask: "answering a question",
979
978
  want: "shaping what you want",
980
979
  ingest: "watching the numbers",
@@ -982,22 +981,22 @@ var SKILL_SENTENCE = {
982
981
  spec: "writing specs"
983
982
  };
984
983
  function skillSentence(skill, opts = {}) {
985
- if (skill === "next") return opts.ref ? `building ${opts.ref}` : "building an option";
984
+ if (skill === "build") return opts.ref ? `building ${opts.ref}` : "building an option";
986
985
  return SKILL_SENTENCE[skill] ?? `running ${skill}`;
987
986
  }
988
987
 
989
988
  // ../shared/src/time.ts
990
989
  var DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
991
990
  function describeNextRun(schedule, now = /* @__PURE__ */ new Date(), opts = {}) {
992
- const next2 = new Date(schedule.next_due_at);
991
+ const next = new Date(schedule.next_due_at);
993
992
  const local = new Intl.DateTimeFormat("en-GB", {
994
993
  timeZone: schedule.timezone,
995
994
  weekday: "long",
996
995
  hour: "2-digit",
997
996
  minute: "2-digit",
998
997
  hour12: false
999
- }).format(next2);
1000
- const hours = Math.round((next2.getTime() - now.getTime()) / 36e5);
998
+ }).format(next);
999
+ const hours = Math.round((next.getTime() - now.getTime()) / 36e5);
1001
1000
  const when = hours < 1 ? "any minute" : hours < 36 ? `in ${hours}h` : `in ${Math.round(hours / 24)} days`;
1002
1001
  return opts.timezone === false ? `${local}, ${when}` : `${local} ${schedule.timezone}, ${when}`;
1003
1002
  }
@@ -2018,10 +2017,10 @@ function placeRanks(crew, pinned) {
2018
2017
  while (slots[at] !== null && at > 0) at -= 1;
2019
2018
  slots[at] = id;
2020
2019
  }
2021
- let next2 = 0;
2020
+ let next = 0;
2022
2021
  for (let i = 0; i < total; i += 1) {
2023
2022
  if (slots[i] !== null) continue;
2024
- slots[i] = loose[next2++] ?? null;
2023
+ slots[i] = loose[next++] ?? null;
2025
2024
  }
2026
2025
  const out = /* @__PURE__ */ new Map();
2027
2026
  slots.forEach((id, i) => id && out.set(id, i + 1));
@@ -2672,7 +2671,7 @@ import { z as z9 } from "zod";
2672
2671
  var PITCH_LINE_KEYS = ["what", "who", "need"];
2673
2672
  var PITCH_TOPIC = "identity";
2674
2673
  var PITCH_KEY = /^pitch-/;
2675
- var pitchKey = (line, next2 = false) => `pitch-${line}${next2 ? "-next" : ""}`;
2674
+ var pitchKey = (line, next = false) => `pitch-${line}${next ? "-next" : ""}`;
2676
2675
  var PITCH_LINE_TITLE = { what: "What it is", who: "Who it is for", need: "What they come for" };
2677
2676
 
2678
2677
  // ../shared/src/pitch-crew.ts
@@ -2724,7 +2723,7 @@ var DIRECTION_ROWS = ["mission", "vision"];
2724
2723
  var MISSION_TOPIC = "mission";
2725
2724
  var VISION_TOPIC = "vision";
2726
2725
  var DIRECTION_KEY = /^(?:mission|vision)(?:-next)?$/;
2727
- var directionKey = (row, next2 = false) => `${row}${next2 ? "-next" : ""}`;
2726
+ var directionKey = (row, next = false) => `${row}${next ? "-next" : ""}`;
2728
2727
  var DIRECTION_ROW_TITLE = { mission: "Mission", vision: "Vision" };
2729
2728
 
2730
2729
  // ../shared/src/direction-crew.ts
@@ -3483,7 +3482,7 @@ function foldRound(r) {
3483
3482
  }
3484
3483
  }
3485
3484
  const answer = TalkAnswer.safeParse(r.result);
3486
- const failed = r.status === "failed" || r.status === "cancelled";
3485
+ const failed2 = r.status === "failed" || r.status === "cancelled";
3487
3486
  const error = r.error ?? {};
3488
3487
  return {
3489
3488
  id: r.id,
@@ -3494,8 +3493,8 @@ function foldRound(r) {
3494
3493
  depth: params.data.depth,
3495
3494
  actions: [...actions.values()],
3496
3495
  answer: answer.success ? answer.data : null,
3497
- state: r.status === "succeeded" ? "done" : failed ? "failed" : "working",
3498
- ...failed ? { error: error.code === "timeout" ? "That took too long. Send it again?" : r.status === "cancelled" ? "Stopped." : `I couldn\u2019t answer${error.message ? `: ${clip(error.message, 140)}` : "."}` } : {},
3496
+ state: r.status === "succeeded" ? "done" : failed2 ? "failed" : "working",
3497
+ ...failed2 ? { error: error.code === "timeout" ? "That took too long. Send it again?" : r.status === "cancelled" ? "Stopped." : `I couldn\u2019t answer${error.message ? `: ${clip(error.message, 140)}` : "."}` } : {},
3499
3498
  ...doing ? { doing } : {}
3500
3499
  };
3501
3500
  }
@@ -3522,7 +3521,7 @@ var PROBLEM_TOOLS = ["propose_problems", "add_evidence", "flag_duplicate", "file
3522
3521
  var READ_WEB_TOOLS = ["web_search", "web_fetch"];
3523
3522
  var STORE_TOOLS = ["app_store_search", "play_store_search"];
3524
3523
  var MARKET_TOOLS = ["write_market"];
3525
- var WRITE_REPO_TOOLS = ["remove_files"];
3524
+ var BUILD_TOOLS = ["build_plan", "build_step"];
3526
3525
  var ALL_MEMORY_TOOLS = [
3527
3526
  ...MEMORY_READ_TOOLS,
3528
3527
  ...MEMORY_WRITE_TOOLS,
@@ -3532,7 +3531,7 @@ var ALL_MEMORY_TOOLS = [
3532
3531
  ...PROBLEM_TOOLS,
3533
3532
  ...READ_WEB_TOOLS,
3534
3533
  ...MARKET_TOOLS,
3535
- ...WRITE_REPO_TOOLS,
3534
+ ...BUILD_TOOLS,
3536
3535
  ...TALK_TOOLS,
3537
3536
  "last_review",
3538
3537
  "list_problems",
@@ -3574,7 +3573,7 @@ function claudeCodeToolFlags(capabilities, opts = {}) {
3574
3573
  tools.add(t);
3575
3574
  allowed.add(`${t}(${pathRule(workspace)})`);
3576
3575
  }
3577
- for (const t of WRITE_REPO_TOOLS) allowed.add(mcpTool(t));
3576
+ for (const t of BUILD_TOOLS) allowed.add(mcpTool(t));
3578
3577
  } else {
3579
3578
  for (const t of WRITE_TOOLS) disallowed.add(t);
3580
3579
  }
@@ -3632,7 +3631,7 @@ function claudeCodeToolFlags(capabilities, opts = {}) {
3632
3631
  allowed.delete(t);
3633
3632
  disallowed.add(t);
3634
3633
  }
3635
- return { tools: [...tools], allowedTools: [...allowed], disallowedTools: [...disallowed] };
3634
+ return { tools: [...tools], allowedTools: [...allowed], disallowedTools: [...disallowed], permissionMode: tools.has("Bash") ? "auto" : "default" };
3636
3635
  }
3637
3636
 
3638
3637
  // ../daemon/src/adapters/claude-code/sandbox.ts
@@ -3674,10 +3673,18 @@ function buildSandboxSettings(home) {
3674
3673
  }
3675
3674
  return { permissions: { deny, defaultMode: "default" } };
3676
3675
  }
3677
- function sandboxSettingsFor(home, grantedTools) {
3676
+ function sandboxSettingsFor(home, grantedTools, network = []) {
3678
3677
  const settings = buildSandboxSettings(home);
3679
3678
  const granted = new Set(grantedTools);
3680
3679
  settings.permissions.deny = settings.permissions.deny.filter((rule) => !granted.has(rule));
3680
+ if (granted.has("Bash")) {
3681
+ settings.sandbox = {
3682
+ enabled: true,
3683
+ allowUnsandboxedCommands: false,
3684
+ failIfUnavailable: true,
3685
+ network: { allowedDomains: network.filter((h) => !/(^|\.)github(usercontent)?\.com$/.test(h)) }
3686
+ };
3687
+ }
3681
3688
  return settings;
3682
3689
  }
3683
3690
 
@@ -3818,8 +3825,8 @@ function classifyExit(stderr, aborted) {
3818
3825
  if (t.includes("not logged in") || t.includes("invalid api key") || t.includes("authentication") || t.includes("subscription") || t.includes("please run /login") || t.includes("oauth")) {
3819
3826
  return { code: "auth", message: "Claude Code is not logged in on this machine. Run `claude login` or set ANTHROPIC_API_KEY." };
3820
3827
  }
3821
- const tail2 = stderr.trim().split("\n").slice(-3).join(" ").trim();
3822
- return { code: "harness_error", message: tail2 || "claude exited without a result" };
3828
+ const tail = stderr.trim().split("\n").slice(-3).join(" ").trim();
3829
+ return { code: "harness_error", message: tail || "claude exited without a result" };
3823
3830
  }
3824
3831
 
3825
3832
  // ../daemon/src/adapters/claude-code/index.ts
@@ -3856,10 +3863,10 @@ var ClaudeCodeAdapter = class {
3856
3863
  const result = parsePrintResult(stdout);
3857
3864
  return result?.is_error ? { ok: false, message: result.result ?? "claude reported an error" } : { ok: true, message: `claude answered in ${result?.subtype ?? "success"}` };
3858
3865
  } catch (err) {
3859
- const failed = err;
3860
- const reported = parsePrintResult(failed.stdout)?.result;
3866
+ const failed2 = err;
3867
+ const reported = parsePrintResult(failed2.stdout)?.result;
3861
3868
  if (reported) return { ok: false, message: reported };
3862
- const text2 = failed.stderr?.trim() || failed.message || String(err);
3869
+ const text2 = failed2.stderr?.trim() || failed2.message || String(err);
3863
3870
  return { ok: false, message: classifyExit(text2, false).message };
3864
3871
  }
3865
3872
  }
@@ -3895,9 +3902,9 @@ var ClaudeCodeAdapter = class {
3895
3902
  child.stderr.on("data", (chunk) => {
3896
3903
  stderr += chunk.toString();
3897
3904
  });
3898
- const exit = new Promise((resolve6) => {
3899
- child.on("close", (code) => resolve6(code));
3900
- child.on("error", () => resolve6(127));
3905
+ const exit = new Promise((resolve5) => {
3906
+ child.on("close", (code) => resolve5(code));
3907
+ child.on("error", () => resolve5(127));
3901
3908
  });
3902
3909
  const parser = new StreamParser();
3903
3910
  let searched = false;
@@ -3937,7 +3944,7 @@ var ClaudeCodeAdapter = class {
3937
3944
  "--append-system-prompt",
3938
3945
  spec2.systemPrompt,
3939
3946
  "--permission-mode",
3940
- "default",
3947
+ flags.permissionMode,
3941
3948
  "--max-turns",
3942
3949
  String(spec2.maxTurns),
3943
3950
  "--setting-sources",
@@ -3986,7 +3993,7 @@ var ClaudeCodeAdapter = class {
3986
3993
  await writeFile2(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 384 });
3987
3994
  const settingsPath = join3(spec2.runDir, "settings.json");
3988
3995
  const flags = claudeCodeToolFlags(spec2.capabilities, { workspace: spec2.cwd, harnessWeb: spec2.harnessWeb, without: spec2.withoutTools });
3989
- await writeFile2(settingsPath, JSON.stringify(sandboxSettingsFor(spec2.env.HOME ?? "", flags.tools)), { mode: 384 });
3996
+ await writeFile2(settingsPath, JSON.stringify(sandboxSettingsFor(spec2.env.HOME ?? "", flags.tools, spec2.network)), { mode: 384 });
3990
3997
  const child = spawn(this.claudePath, this.buildArgs(spec2, mcpConfigPath, settingsPath), {
3991
3998
  cwd: spec2.cwd,
3992
3999
  env: spec2.env,
@@ -4005,9 +4012,9 @@ var ClaudeCodeAdapter = class {
4005
4012
  stderr += chunk.toString();
4006
4013
  if (stderr.length > 64e3) stderr = stderr.slice(-32e3);
4007
4014
  });
4008
- const exit = new Promise((resolve6) => {
4009
- child.on("close", (code, signal) => resolve6({ code, signal }));
4010
- child.on("error", () => resolve6({ code: 127, signal: null }));
4015
+ const exit = new Promise((resolve5) => {
4016
+ child.on("close", (code, signal) => resolve5({ code, signal }));
4017
+ child.on("error", () => resolve5({ code: 127, signal: null }));
4011
4018
  });
4012
4019
  const parser = new StreamParser(spec2.outputSchema !== void 0);
4013
4020
  try {
@@ -4042,10 +4049,10 @@ function parsePrintResult(stdout) {
4042
4049
  }
4043
4050
 
4044
4051
  // ../daemon/src/daemon.ts
4045
- import { mkdir as mkdir5, readdir as readdir2, readFile as readFile7, rm as rm4, writeFile as writeFile4 } from "fs/promises";
4052
+ import { mkdir as mkdir6, readdir as readdir3, readFile as readFile9, rm as rm5, writeFile as writeFile6 } from "fs/promises";
4046
4053
  import { openSync, closeSync, unlinkSync, writeFileSync, readFileSync as readFileSyncNode, mkdirSync } from "fs";
4047
4054
  import { hostname as hostname2 } from "os";
4048
- import { join as join11 } from "path";
4055
+ import { join as join13 } from "path";
4049
4056
  import { setTimeout as sleep } from "timers/promises";
4050
4057
 
4051
4058
  // ../daemon/src/awake.ts
@@ -4114,8 +4121,8 @@ async function installVersion(version, home = homedir2()) {
4114
4121
  });
4115
4122
  return { ok: true };
4116
4123
  } catch (err) {
4117
- const failed = err;
4118
- return { ok: false, why: (failed.stderr?.trim() || failed.message || String(err)).split("\n").slice(-3).join(" ") };
4124
+ const failed2 = err;
4125
+ return { ok: false, why: (failed2.stderr?.trim() || failed2.message || String(err)).split("\n").slice(-3).join(" ") };
4119
4126
  }
4120
4127
  }
4121
4128
  function autoUpdateOff(home = homedir2()) {
@@ -4177,15 +4184,15 @@ function stripLiterals(sql) {
4177
4184
  };
4178
4185
  while (i < n) {
4179
4186
  const c = sql[i];
4180
- const next2 = sql[i + 1];
4181
- if (c === "-" && next2 === "-") {
4187
+ const next = sql[i + 1];
4188
+ if (c === "-" && next === "-") {
4182
4189
  const end = sql.indexOf("\n", i);
4183
4190
  const stop2 = end === -1 ? n : end;
4184
4191
  blank(i, stop2);
4185
4192
  i = stop2;
4186
4193
  continue;
4187
4194
  }
4188
- if (c === "/" && next2 === "*") {
4195
+ if (c === "/" && next === "*") {
4189
4196
  let depth = 1;
4190
4197
  let k = i + 2;
4191
4198
  while (k < n && depth > 0) {
@@ -5273,14 +5280,142 @@ async function sendDigestFor(client, accessToken, jobId, fetchImpl = fetch) {
5273
5280
  }
5274
5281
 
5275
5282
  // ../daemon/src/deploys.ts
5283
+ import { execFile as execFile4 } from "child_process";
5284
+ import { promisify as promisify4 } from "util";
5285
+
5286
+ // ../daemon/src/repo.ts
5276
5287
  import { execFile as execFile3 } from "child_process";
5288
+ import { readFile as readFile2, rm as rm3, writeFile as writeFile4 } from "fs/promises";
5289
+ import { join as join5 } from "path";
5290
+
5291
+ // ../shared/src/build.ts
5292
+ var BUILD_SKILL = "build";
5293
+ var BUILD_STEPS_MIN = 1;
5294
+ var BUILD_STEPS_MAX = 8;
5295
+ var BUILD_STEP_MAX = 120;
5296
+ var BUILD_RESUMES_MAX = 3;
5297
+ var BUILD_TRAILER = "Co-Authored-By: NanoPM <noreply@nanopm.app>";
5298
+ var BUILD_COLUMNS = "id, ref, title, status, actor, agent_spec, spec";
5299
+ function buildRefusal(solution, opts = {}) {
5300
+ if (solution.actor === "founder") return `${solution.ref} is yours to do, not Nano's to build.`;
5301
+ if (!["proposed", "accepted", "building"].includes(solution.status)) return `${solution.ref} is ${solution.status}: only a solution on the roadmap can be built.`;
5302
+ if (!solution.agent_spec) return `${solution.ref} has no agent spec yet. Write its specs first: nanopm spec ${solution.ref} --write`;
5303
+ const open = solution.spec?.questions?.length ?? 0;
5304
+ if (open && !opts.anyway) {
5305
+ 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`;
5306
+ }
5307
+ return null;
5308
+ }
5309
+ function stepsLeft(plan, done) {
5310
+ return (plan ?? []).map((_, i) => i).filter((i) => !done.has(i));
5311
+ }
5312
+ function buildDir(home, jobId) {
5313
+ return `${home.replace(/\/+$/, "")}/.cache/nanopm/builds/${jobId}`;
5314
+ }
5315
+ function buildBranch(ref, title) {
5316
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
5317
+ return `nano/${ref.toLowerCase()}${slug ? `-${slug}` : ""}`;
5318
+ }
5319
+ var REGISTRIES = {
5320
+ "package-lock.json": ["registry.npmjs.org"],
5321
+ "npm-shrinkwrap.json": ["registry.npmjs.org"],
5322
+ "pnpm-lock.yaml": ["registry.npmjs.org"],
5323
+ "yarn.lock": ["registry.yarnpkg.com", "registry.npmjs.org"],
5324
+ "bun.lockb": ["registry.npmjs.org"],
5325
+ "bun.lock": ["registry.npmjs.org"],
5326
+ "poetry.lock": ["pypi.org", "files.pythonhosted.org"],
5327
+ "uv.lock": ["pypi.org", "files.pythonhosted.org"],
5328
+ "Pipfile.lock": ["pypi.org", "files.pythonhosted.org"],
5329
+ "requirements.txt": ["pypi.org", "files.pythonhosted.org"],
5330
+ "Cargo.lock": ["crates.io", "index.crates.io", "static.crates.io"],
5331
+ "Gemfile.lock": ["rubygems.org", "index.rubygems.org"],
5332
+ "go.sum": ["proxy.golang.org", "sum.golang.org"],
5333
+ "composer.lock": ["repo.packagist.org"]
5334
+ };
5335
+ function buildNetwork(rootFiles) {
5336
+ return [...new Set(rootFiles.flatMap((f) => REGISTRIES[f] ?? []))].sort();
5337
+ }
5338
+ async function queueBuild(client, ask7) {
5339
+ const { data, error: readError } = await client.from("solutions").select(BUILD_COLUMNS).eq("project_id", ask7.projectId).eq("ref", ask7.ref).is("superseded_by", null).maybeSingle();
5340
+ if (readError) throw new Error(`could not read the solution: ${pgMessage(readError)}`);
5341
+ if (!data) throw new Error(`No solution ${ask7.ref} on the Roadmap.`);
5342
+ const solution = data;
5343
+ const refused = buildRefusal(solution, { anyway: ask7.anyway });
5344
+ if (refused) throw new Error(refused);
5345
+ const { data: job, error } = await client.from("jobs").insert({
5346
+ project_id: ask7.projectId,
5347
+ skill: BUILD_SKILL,
5348
+ requested_by: ask7.userId,
5349
+ trigger: "user",
5350
+ target_daemon_id: ask7.daemonId,
5351
+ params: { solution_id: solution.id, repo_path: ask7.repoPath, ...ask7.anyway ? { anyway: true } : {} }
5352
+ }).select("id").single();
5353
+ 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
+ if (error || !job) throw new Error(`could not queue the build: ${error ? pgMessage(error) : "no row"}`);
5355
+ return { jobId: job.id, solution };
5356
+ }
5357
+
5358
+ // ../daemon/src/repo.ts
5277
5359
  import { promisify as promisify3 } from "util";
5278
5360
  var execFileAsync3 = promisify3(execFile3);
5279
- var SETTLE_HOURS = 3;
5280
- var MIN_COMMITS = 3;
5281
- async function git(cwd, args) {
5282
- const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], { timeout: 2e4, maxBuffer: 1024 * 1024 });
5283
- return stdout.trim();
5361
+ var MAX_BUFFER = 4 * 1024 * 1024;
5362
+ var TIMEOUT_MS = 6e4;
5363
+ var LOCKED = {
5364
+ GIT_CONFIG_COUNT: "2",
5365
+ GIT_CONFIG_KEY_0: "core.hooksPath",
5366
+ GIT_CONFIG_VALUE_0: "/dev/null",
5367
+ GIT_CONFIG_KEY_1: "core.fsmonitor",
5368
+ GIT_CONFIG_VALUE_1: "false"
5369
+ };
5370
+ async function exec(cmd, repo, args, timeoutMs = TIMEOUT_MS) {
5371
+ 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}
5373
+ `);
5374
+ await writeFile4(join5(repo.admin, "commondir"), repo.commondir);
5375
+ await rm3(join5(repo.admin, "config.worktree"), { force: true });
5376
+ 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
+ }
5378
+ async function git(repo, args, timeoutMs = TIMEOUT_MS) {
5379
+ return (await exec("git", repo, args, timeoutMs)).stdout.trim();
5380
+ }
5381
+ var RepoError = class extends Error {
5382
+ };
5383
+ async function commitAll(cwd, message, note) {
5384
+ await git(cwd, ["add", "-A"]);
5385
+ const staged = await git(cwd, ["diff", "--cached", "--name-only"]).catch(() => "");
5386
+ if (!staged) return null;
5387
+ await git(cwd, ["commit", "--no-verify", "-m", message, ...note ? ["-m", note] : [], "-m", `Written by NanoPM. Review before merging.
5388
+
5389
+ ${BUILD_TRAILER}`]);
5390
+ const sha = await git(cwd, ["rev-parse", "--short", "HEAD"]);
5391
+ return { sha, ...await diffStat(cwd) };
5392
+ }
5393
+ async function diffStat(cwd) {
5394
+ const numstat = await git(cwd, ["show", "--numstat", "--format=", "HEAD"]).catch(() => "");
5395
+ let files = 0;
5396
+ let insertions = 0;
5397
+ let deletions = 0;
5398
+ for (const line of numstat.split("\n").filter(Boolean)) {
5399
+ const [add2 = "", del = ""] = line.split(" ");
5400
+ files += 1;
5401
+ insertions += Number(add2) || 0;
5402
+ deletions += Number(del) || 0;
5403
+ }
5404
+ return { files, insertions, deletions };
5405
+ }
5406
+ async function pushBranch(cwd, name) {
5407
+ if (!await originUrl(cwd)) return { pushed: false, reason: "this repository has no origin remote" };
5408
+ try {
5409
+ const { stderr } = await exec("git", cwd, ["push", "--no-verify", "--set-upstream", "origin", name], 12e4);
5410
+ return { pushed: true, reviewUrl: reviewUrlFrom(stderr) };
5411
+ } catch (err) {
5412
+ const text2 = errorMessage(err);
5413
+ return { pushed: false, reason: text2.split("\n").slice(-3).join(" ").trim() || "git push failed" };
5414
+ }
5415
+ }
5416
+ function reviewUrlFrom(pushOutput) {
5417
+ const match = /https?:\/\/\S+/.exec(pushOutput);
5418
+ return match?.[0]?.replace(/[).,]+$/, "");
5284
5419
  }
5285
5420
  async function defaultBranch(cwd) {
5286
5421
  try {
@@ -5290,9 +5425,78 @@ async function defaultBranch(cwd) {
5290
5425
  return git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "HEAD");
5291
5426
  }
5292
5427
  }
5428
+ async function originUrl(cwd) {
5429
+ return git(cwd, ["remote", "get-url", "origin"]).catch(() => "");
5430
+ }
5431
+ async function baseRef(cwd, branch) {
5432
+ if (!await originUrl(cwd)) return branch;
5433
+ await git(cwd, ["fetch", "--quiet", "origin", branch], 12e4).catch(() => "");
5434
+ return await git(cwd, ["rev-parse", "--verify", "--quiet", `origin/${branch}`]).catch(() => "") ? `origin/${branch}` : branch;
5435
+ }
5436
+ async function addWorktree(clone, dir, branch) {
5437
+ if (!await git(clone, ["rev-parse", "HEAD"]).catch(() => "")) throw new RepoError("this repository has no commits yet; make the first commit, then build");
5438
+ const existing = await git(clone, ["branch", "--list", branch]).catch(() => "");
5439
+ if (existing.trim()) throw new RepoError(`branch ${branch} already exists. Delete it or finish what is on it first.`);
5440
+ const base = await defaultBranch(clone);
5441
+ const onto = await baseRef(clone, base);
5442
+ await git(clone, ["worktree", "add", "-b", branch, dir, onto]);
5443
+ const admin = await git(dir, ["rev-parse", "--absolute-git-dir"]);
5444
+ const worktree = { dir, admin, commondir: await readFile2(join5(admin, "commondir"), "utf8") };
5445
+ return { base, fork: await git(worktree, ["rev-parse", "HEAD"]), worktree };
5446
+ }
5447
+ async function removeWorktree(clone, dir) {
5448
+ await git(clone, ["worktree", "remove", "--force", dir]).catch(() => "");
5449
+ }
5450
+ async function commonGitDir(cwd) {
5451
+ return git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
5452
+ }
5453
+ async function commitsSinceFork(cwd, fork) {
5454
+ return Number(await git(cwd, ["rev-list", "--count", `${fork}..HEAD`]).catch(() => "0")) || 0;
5455
+ }
5456
+ async function rebaseOnto(cwd, base) {
5457
+ const onto = await baseRef(cwd, base);
5458
+ try {
5459
+ await git(cwd, ["rebase", "--no-verify", onto], 12e4);
5460
+ return { ok: true };
5461
+ } catch {
5462
+ const files = (await git(cwd, ["diff", "--name-only", "--diff-filter=U"]).catch(() => "")).split("\n").filter(Boolean);
5463
+ await git(cwd, ["rebase", "--abort"]).catch(() => "");
5464
+ return { ok: false, files };
5465
+ }
5466
+ }
5467
+ async function gh(cwd, args) {
5468
+ return (await exec("gh", cwd, args, 12e4)).stdout.trim();
5469
+ }
5470
+ async function ghReady(cwd) {
5471
+ if (!/github\.com[:/]/.test(await originUrl(cwd))) return "this repository has no GitHub origin, so there is nowhere to open a pull request";
5472
+ try {
5473
+ await gh(cwd, ["auth", "status"]);
5474
+ return null;
5475
+ } catch (err) {
5476
+ 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";
5477
+ }
5478
+ }
5479
+ async function openPullRequest(cwd, pr) {
5480
+ try {
5481
+ const out = await gh(cwd, ["pr", "create", "--draft", "--head", pr.branch, "--base", pr.base, "--title", pr.title, "--body", pr.body]);
5482
+ const url = reviewUrlFrom(out) ?? out.split("\n").at(-1) ?? "";
5483
+ return { ok: true, url, number: Number(/\/pull\/(\d+)/.exec(url)?.[1] ?? 0) };
5484
+ } catch (err) {
5485
+ return { ok: false, reason: errorMessage(err).split("\n").filter(Boolean).slice(-2).join(" ") || "gh pr create failed" };
5486
+ }
5487
+ }
5488
+
5489
+ // ../daemon/src/deploys.ts
5490
+ var execFileAsync4 = promisify4(execFile4);
5491
+ var SETTLE_HOURS = 3;
5492
+ var MIN_COMMITS = 3;
5493
+ async function git2(cwd, args) {
5494
+ const { stdout } = await execFileAsync4("git", ["-C", cwd, ...args], { timeout: 2e4, maxBuffer: 1024 * 1024 });
5495
+ return stdout.trim();
5496
+ }
5293
5497
  async function commitsSince(cwd, since) {
5294
5498
  const branch = await defaultBranch(cwd);
5295
- const out = await git(cwd, ["log", branch, `--since=${since.toISOString()}`, "--format=%cI"]);
5499
+ const out = await git2(cwd, ["log", branch, `--since=${since.toISOString()}`, "--format=%cI"]);
5296
5500
  const stamps = out.split("\n").filter(Boolean);
5297
5501
  return { count: stamps.length, newestAt: stamps[0] ? new Date(stamps[0]) : null };
5298
5502
  }
@@ -5376,16 +5580,16 @@ async function reviewWithoutData(client, job) {
5376
5580
 
5377
5581
  // ../daemon/src/skills.ts
5378
5582
  import { existsSync as existsSync2, readdirSync } from "fs";
5379
- import { readFile as readFile2 } from "fs/promises";
5380
- import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
5583
+ import { readFile as readFile3 } from "fs/promises";
5584
+ import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
5381
5585
  function findSkillsDir(env = process.env) {
5382
5586
  if (env.NANOPM_SKILLS_DIR) return env.NANOPM_SKILLS_DIR;
5383
5587
  const candidates = [
5384
- join5(process.cwd(), "skills"),
5588
+ join6(process.cwd(), "skills"),
5385
5589
  ...process.argv[1] ? [resolve2(dirname2(process.argv[1]), "..", "skills")] : []
5386
5590
  ];
5387
5591
  for (const dir of candidates) {
5388
- if (existsSync2(join5(dir, "onboard", "skill.json"))) return dir;
5592
+ if (existsSync2(join6(dir, "onboard", "skill.json"))) return dir;
5389
5593
  }
5390
5594
  throw new Error(`Cannot find the skills directory (tried ${candidates.join(", ")}). Set NANOPM_SKILLS_DIR.`);
5391
5595
  }
@@ -5395,9 +5599,9 @@ function skillDirs(skillsDir, env = process.env) {
5395
5599
  }
5396
5600
  async function loadSkill(name, skillsDir = findSkillsDir()) {
5397
5601
  if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new Error(`Invalid skill name "${name}"`);
5398
- const dir = skillDirs(skillsDir).map((d) => join5(d, name)).find((d) => existsSync2(join5(d, "skill.json")));
5602
+ const dir = skillDirs(skillsDir).map((d) => join6(d, name)).find((d) => existsSync2(join6(d, "skill.json")));
5399
5603
  if (!dir) throw new Error(`Unknown skill "${name}"`);
5400
- const raw = JSON.parse(await readFile2(join5(dir, "skill.json"), "utf8"));
5604
+ const raw = JSON.parse(await readFile3(join6(dir, "skill.json"), "utf8"));
5401
5605
  let manifest;
5402
5606
  try {
5403
5607
  manifest = parseSkillManifest(raw);
@@ -5406,7 +5610,7 @@ async function loadSkill(name, skillsDir = findSkillsDir()) {
5406
5610
  }
5407
5611
  const tainted = taintedByTheWeb(manifest);
5408
5612
  if (tainted) throw new Error(`the "${name}" skill cannot be loaded: ${tainted}`);
5409
- const body = await readFile2(join5(dir, "SKILL.md"), "utf8");
5613
+ const body = await readFile3(join6(dir, "SKILL.md"), "utf8");
5410
5614
  return { manifest, body, dir };
5411
5615
  }
5412
5616
  var NOT_WITH_READ_WEB = ["write_memory", "problems", "propose", "review", "write_repo", "run_commands"];
@@ -5441,7 +5645,7 @@ function renderPrompt(template, params) {
5441
5645
  var NEEDS_THE_REQUESTERS_CLONE = ["write_repo", "run_commands"];
5442
5646
  var NEEDS_THE_REQUESTERS_IDENTITY = /* @__PURE__ */ new Set(["want", "start"]);
5443
5647
  async function clonelessSkills(skillsDir = findSkillsDir()) {
5444
- const names = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync2(join5(skillsDir, d.name, "skill.json"))).map((d) => d.name);
5648
+ const names = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync2(join6(skillsDir, d.name, "skill.json"))).map((d) => d.name);
5445
5649
  const out = [];
5446
5650
  for (const name of names) {
5447
5651
  try {
@@ -5568,9 +5772,9 @@ async function runProblemMapping(ports) {
5568
5772
  if (cycle === 1 && ports.collect) {
5569
5773
  const maxTurns = Math.min(8, MAPPING_TURN_LIMIT - (usage?.turns ?? 0) - 2);
5570
5774
  await ports.checkpoint("collector.started", { maxTurns: Math.max(0, maxTurns), startedAt: Date.now() });
5571
- let next2 = null;
5775
+ let next = null;
5572
5776
  try {
5573
- next2 = await ports.collect(proposal.investigation, signal, maxTurns);
5777
+ next = await ports.collect(proposal.investigation, signal, maxTurns);
5574
5778
  } catch (err) {
5575
5779
  signal.throwIfAborted();
5576
5780
  failure2 = errorMessage(err);
@@ -5578,13 +5782,13 @@ async function runProblemMapping(ports) {
5578
5782
  usage = sumUsage(usage, spent);
5579
5783
  await ports.checkpoint("collector", { usage: spent, failed: failure2 });
5580
5784
  }
5581
- if (!next2 && !failure2) await ports.checkpoint("collector", { unavailable: true });
5785
+ if (!next && !failure2) await ports.checkpoint("collector", { unavailable: true });
5582
5786
  signal.throwIfAborted();
5583
- if (next2) {
5584
- if (next2.usage.turns > 0) usage = sumUsage(usage, next2.usage);
5585
- await ports.checkpoint("collector", { usage: next2.usage });
5787
+ if (next) {
5788
+ if (next.usage.turns > 0) usage = sumUsage(usage, next.usage);
5789
+ await ports.checkpoint("collector", { usage: next.usage });
5586
5790
  if ((usage?.turns ?? 0) >= MAPPING_TURN_LIMIT) throw new Error("Collection exhausted the shared turn budget.");
5587
- snapshot3 = copySnapshot(next2.snapshot);
5791
+ snapshot3 = copySnapshot(next.snapshot);
5588
5792
  feedback = { previous_proposal: proposal, instruction: proposal.investigation.collector === "sources" ? "The seen sources you named are now in the snapshot in full. Reconsider the proposal with them, then return the final version." : "Reconsider the proposal with the collected findings, then return the final version." };
5589
5793
  await ports.checkpoint("collection", { snapshot_hash: mappingHash(snapshot3), request: proposal.investigation });
5590
5794
  continue;
@@ -5770,8 +5974,8 @@ async function askAgainIfUnanswered(attempt7, say, role7) {
5770
5974
  var UNANSWERED = /structured[ _]output|returned no result/i;
5771
5975
 
5772
5976
  // ../daemon/src/site.ts
5773
- import { readFile as readFile3 } from "fs/promises";
5774
- import { join as join6 } from "path";
5977
+ import { readFile as readFile4 } from "fs/promises";
5978
+ import { join as join7 } from "path";
5775
5979
  var SITE_PATHS = ["/", "/about", "/pricing"];
5776
5980
  var PAGE_CHARS = 6e3;
5777
5981
  var SITE_TIMEOUT_MS = 2e4;
@@ -5894,7 +6098,7 @@ function siteFromSearch(name, hits) {
5894
6098
  }
5895
6099
  async function read(repoRoot, name) {
5896
6100
  try {
5897
- const text2 = await readFile3(join6(repoRoot, name), "utf8");
6101
+ const text2 = await readFile4(join7(repoRoot, name), "utf8");
5898
6102
  return text2.length > 2e5 ? null : text2;
5899
6103
  } catch {
5900
6104
  return null;
@@ -5948,8 +6152,8 @@ async function fromReadme(repoRoot) {
5948
6152
  }
5949
6153
 
5950
6154
  // ../daemon/src/repo-docs.ts
5951
- import { readFile as readFile4, readdir, stat } from "fs/promises";
5952
- import { join as join7, relative } from "path";
6155
+ import { readFile as readFile5, readdir, stat } from "fs/promises";
6156
+ import { join as join8, relative } from "path";
5953
6157
  var WRITTEN = /(persona|audience|customer|user-research|research\/|icp|segment)/i;
5954
6158
  var ABOUT_PEOPLE = /(persona|audience|customer|user-research|research\/|interview|feedback|survey|verbatim|voice|icp|segment)/i;
5955
6159
  var DOC = /\.(md|mdx|txt)$/i;
@@ -5967,7 +6171,7 @@ async function readDocs(cwd, opts = {}) {
5967
6171
  return;
5968
6172
  }
5969
6173
  for (const entry of entries) {
5970
- const full = join7(dir, entry.name);
6174
+ const full = join8(dir, entry.name);
5971
6175
  if (SKIP.test(`/${relative(cwd, full)}`)) continue;
5972
6176
  if (entry.isDirectory()) await walk(full, depth + 1);
5973
6177
  else if (DOC.test(entry.name) && (!opts.onlyAboutPeople || ABOUT_PEOPLE.test(relative(cwd, full)))) found.push(full);
@@ -5979,7 +6183,7 @@ async function readDocs(cwd, opts = {}) {
5979
6183
  for (const path of found.slice(0, DOCS_MAX)) {
5980
6184
  try {
5981
6185
  if ((await stat(path)).size > 4e5) continue;
5982
- out.push({ path: relative(cwd, path), text: (await readFile4(path, "utf8")).slice(0, DOC_CHARS) });
6186
+ out.push({ path: relative(cwd, path), text: (await readFile5(path, "utf8")).slice(0, DOC_CHARS) });
5983
6187
  } catch {
5984
6188
  }
5985
6189
  }
@@ -6451,10 +6655,10 @@ function rank(candidates, readings, verdict, faults) {
6451
6655
  const order = (lands) => DIRECTION_SHAPE_ORDER.indexOf(lands);
6452
6656
  return candidates.filter((c) => readings.some((r) => r.lands === c.lands)).map((candidate) => {
6453
6657
  const v = verdict.verdicts.find((x) => x.lands === candidate.lands);
6454
- const failed = v?.failed ?? [];
6455
- const fatal = failed.filter((t) => MUST_PASS_TESTS.includes(t.test));
6658
+ const failed2 = v?.failed ?? [];
6659
+ const fatal = failed2.filter((t) => MUST_PASS_TESTS.includes(t.test));
6456
6660
  const gates = faults.get(candidate.lands) ?? [];
6457
- const answered = /* @__PURE__ */ new Set([...v?.passed ?? [], ...failed.map((t) => t.test)]);
6661
+ const answered = /* @__PURE__ */ new Set([...v?.passed ?? [], ...failed2.map((t) => t.test)]);
6458
6662
  const unanswered = MUST_PASS_TESTS.filter((t) => !answered.has(t));
6459
6663
  const reservations = [
6460
6664
  ...fatal.map((t) => `${t.test} \u2014 ${t.because}`),
@@ -6527,17 +6731,17 @@ async function publish2(client, projectId, job, winner, current, because, confid
6527
6731
  for (const row of DIRECTION_ROWS) {
6528
6732
  const content = winner[row].trim();
6529
6733
  const base = current.find((r) => r.topic === row && r.key === directionKey(row));
6530
- const next2 = current.find((r) => r.topic === row && r.key === directionKey(row, true));
6734
+ const next = current.find((r) => r.topic === row && r.key === directionKey(row, true));
6531
6735
  const sources2 = [
6532
6736
  ...winner.restsOn.map((ref) => ({ kind: "memory", ref, note: because })),
6533
6737
  ...runnerUp && runnerUp[row]?.trim() && runnerUp[row].trim() !== content ? [{ kind: CONSIDERED, ref: runnerUp.lands, note: runnerUp[row].trim() }] : []
6534
6738
  ];
6535
6739
  if (base?.provenance === "stated") {
6536
6740
  if (base.content.trim() === content) {
6537
- if (next2) await retire(client, projectId, next2.id, "withdrawn: the crew now agrees with your line");
6741
+ if (next) await retire(client, projectId, next.id, "withdrawn: the crew now agrees with your line");
6538
6742
  continue;
6539
6743
  }
6540
- await write2(client, projectId, jobId, row, { key: directionKey(row, true), content, confidence, sources: sources2, supersedes: next2?.id ?? null });
6744
+ await write2(client, projectId, jobId, row, { key: directionKey(row, true), content, confidence, sources: sources2, supersedes: next?.id ?? null });
6541
6745
  proposed.push(row);
6542
6746
  } else {
6543
6747
  if (base && base.content.trim() === content) continue;
@@ -6758,17 +6962,17 @@ async function publish3(client, projectId, job, winner, current, because, runner
6758
6962
  for (const line of PITCH_LINE_KEYS) {
6759
6963
  const content = winner[line].trim();
6760
6964
  const base = current.find((r) => r.key === pitchKey(line));
6761
- const next2 = current.find((r) => r.key === pitchKey(line, true));
6965
+ const next = current.find((r) => r.key === pitchKey(line, true));
6762
6966
  const sources2 = [
6763
6967
  ...winner.restsOn.map((ref) => ({ kind: "memory", ref, note: because })),
6764
6968
  ...runnerUp && !sameLine(runnerUp[line], content) ? [{ kind: CONSIDERED, ref: runnerUp.form, note: runnerUp[line].trim() }] : []
6765
6969
  ];
6766
6970
  if (base?.provenance === "stated") {
6767
6971
  if (sameLine(base.content, content)) {
6768
- if (next2) await retire2(client, projectId, next2.id, "withdrawn: the crew now agrees with your line");
6972
+ if (next) await retire2(client, projectId, next.id, "withdrawn: the crew now agrees with your line");
6769
6973
  continue;
6770
6974
  }
6771
- await write3(client, projectId, jobId, { key: pitchKey(line, true), title: PITCH_LINE_TITLE[line], content, confidence: winner.confidence, sources: sources2, supersedes: next2?.id ?? null });
6975
+ await write3(client, projectId, jobId, { key: pitchKey(line, true), title: PITCH_LINE_TITLE[line], content, confidence: winner.confidence, sources: sources2, supersedes: next?.id ?? null });
6772
6976
  proposed.push(line);
6773
6977
  } else {
6774
6978
  if (base && sameLine(base.content, content)) continue;
@@ -7023,10 +7227,10 @@ async function runMarketJob({ client, job, adapter, spec: spec2, onEvent }) {
7023
7227
  }
7024
7228
  async function writeRow(client, projectId, job, rows2, item) {
7025
7229
  const base = rows2.find((r) => r.key === item.key);
7026
- const next2 = rows2.find((r) => r.key === `${item.key}-next`);
7230
+ const next = rows2.find((r) => r.key === `${item.key}-next`);
7027
7231
  if (base && base.content.trim() === item.content) return { kind: "written" };
7028
7232
  if (base?.provenance === "stated") {
7029
- await write4(client, projectId, job.id, { ...item, key: `${item.key}-next`, supersedes: next2?.id ?? null });
7233
+ await write4(client, projectId, job.id, { ...item, key: `${item.key}-next`, supersedes: next?.id ?? null });
7030
7234
  return { kind: "written" };
7031
7235
  }
7032
7236
  const held = keepsTheLine(job, base, item.confidence);
@@ -7782,7 +7986,7 @@ ${body}
7782
7986
  // ../daemon/src/crew.ts
7783
7987
  import { randomUUID as randomUUID8 } from "crypto";
7784
7988
  import { mkdir as mkdir2 } from "fs/promises";
7785
- import { join as join8 } from "path";
7989
+ import { join as join9 } from "path";
7786
7990
  var READ_ONLY = {
7787
7991
  read_repo: /^(Read|Glob|Grep)$/,
7788
7992
  read_data: /(^|__)(list_connections|describe_source|query)$/
@@ -7797,7 +8001,7 @@ async function attempt6({ adapter, spec: spec2, onEvent, skill: skillName, name,
7797
8001
  if (skill.manifest.capabilities.some((c) => !holds.includes(c))) throw new Error(`The ${name} may not hold tools.`);
7798
8002
  const refused = grant.find((c) => !READ_ONLY[c] || !skill.manifest.capabilities.includes(c));
7799
8003
  if (refused) throw new Error(`The ${name} may not hold ${refused}.`);
7800
- const runDir = dir ? join8(spec2.runDir, dir) : spec2.runDir;
8004
+ const runDir = dir ? join9(spec2.runDir, dir) : spec2.runDir;
7801
8005
  if (dir) await mkdir2(runDir, { recursive: true, mode: 448 });
7802
8006
  let result;
7803
8007
  let usage = null;
@@ -8595,7 +8799,7 @@ function critiqueWording(o, v, reworked) {
8595
8799
  return { problem: problem && problem.length <= PROBLEM_MAX ? problem : o.problem, description: v.description?.trim() || o.description };
8596
8800
  }
8597
8801
  function shape(o, input) {
8598
- const resolve6 = (ids, stance) => ids.map((id) => {
8802
+ const resolve5 = (ids, stance) => ids.map((id) => {
8599
8803
  const source = input.cites.get(id.trim().toUpperCase());
8600
8804
  if (!source) input.unresolved?.add(id.trim());
8601
8805
  return source;
@@ -8607,7 +8811,7 @@ function shape(o, input) {
8607
8811
  if (source) quoted.push({ ...source, note: clip3(stripContactDetails(e.quote), 140), stance: "supports" });
8608
8812
  }
8609
8813
  const quotedRefs = new Set(quoted.map((q) => q.ref));
8610
- const cited = [...resolve6(o.restsOn, "supports"), ...resolve6(o.against.filter((id) => !supporting.has(id.trim().toUpperCase())), "contradicts")].filter((c) => !(c.stance === "supports" && quotedRefs.has(c.ref)));
8814
+ const cited = [...resolve5(o.restsOn, "supports"), ...resolve5(o.against.filter((id) => !supporting.has(id.trim().toUpperCase())), "contradicts")].filter((c) => !(c.stance === "supports" && quotedRefs.has(c.ref)));
8611
8815
  const all = [...quoted, ...cited];
8612
8816
  let sources2 = all.filter((c, i) => all.findIndex((x) => x.kind === c.kind && x.ref === c.ref && x.stance === c.stance && x.note === c.note) === i);
8613
8817
  if (!sources2.some((s) => s.stance === "supports")) sources2 = [{ kind: "inference", ref: "nanopm", note: null, stance: "supports" }, ...sources2];
@@ -8897,11 +9101,11 @@ async function runSolutionsJob({ client, job, adapter, spec: spec2, onEvent }) {
8897
9101
  await journal("solutions.citations", `${ids.length} citation${ids.length === 1 ? "" : "s"} named nothing the crew was handed, and ${ids.length === 1 ? "was" : "were"} left out: ${ids.slice(0, 12).join(", ")}.`);
8898
9102
  }
8899
9103
  const recorded = results.filter((r) => "kept" in r);
8900
- const failed = results.filter((r) => "failed" in r);
9104
+ const failed2 = results.filter((r) => "failed" in r);
8901
9105
  const took = `Took ${duration(Date.now() - started)}${timings.length ? `: ${timings.map((t) => `${t.what} ${duration(t.ms)}`).join(", ")}` : ""}.`;
8902
9106
  const summary = [
8903
- deep ? leaves.length ? recorded.length ? `Deep dive on \u201C${clip3(leaves[0].problem, 120)}\u201D: kept ${recorded[0].kept.map((t) => `\u201C${t}\u201D`).join(", ")}.` : `Deep dive on \u201C${clip3(leaves[0].problem, 120)}\u201D: nothing recorded \u2014 ${failed[0]?.failed ?? "no solution came back"}.` : "Deep dive: nothing to think through." : `Flash: solutions for ${recorded.length} of ${leaves.length} problem${leaves.length === 1 ? "" : "s"} in ${branches.length} branch${branches.length === 1 ? "" : "es"}.`,
8904
- !deep && failed.length ? `Not recorded: ${failed.map((f) => `${f.leaf.ref} (${f.failed})`).join("; ")}.` : "",
9107
+ deep ? leaves.length ? recorded.length ? `Deep dive on \u201C${clip3(leaves[0].problem, 120)}\u201D: kept ${recorded[0].kept.map((t) => `\u201C${t}\u201D`).join(", ")}.` : `Deep dive on \u201C${clip3(leaves[0].problem, 120)}\u201D: nothing recorded \u2014 ${failed2[0]?.failed ?? "no solution came back"}.` : "Deep dive: nothing to think through." : `Flash: solutions for ${recorded.length} of ${leaves.length} problem${leaves.length === 1 ? "" : "s"} in ${branches.length} branch${branches.length === 1 ? "" : "es"}.`,
9108
+ !deep && failed2.length ? `Not recorded: ${failed2.map((f) => `${f.leaf.ref} (${f.failed})`).join("; ")}.` : "",
8905
9109
  skipped.length ? `Skipped: ${skipped.join("; ")}.` : "",
8906
9110
  took
8907
9111
  ].filter(Boolean).join(" ");
@@ -9071,9 +9275,9 @@ async function flashBranch(ctx, w, n, places) {
9071
9275
  const why = clip3(error instanceof Error ? error.message : String(error), 300);
9072
9276
  for (const leaf of w.leaves) if (!out.some((o) => o.leaf === leaf)) out.push({ leaf, failed: why });
9073
9277
  }
9074
- const failed = out.filter((o) => "failed" in o);
9075
- if (failed.length) {
9076
- await ctx.journal("solutions.failed", `\u201C${name}\u201D could not finish: ${failed.map((f) => `${f.leaf.ref} \u2014 ${f.failed}`).join("; ")}. The other branches were not held.`);
9278
+ const failed2 = out.filter((o) => "failed" in o);
9279
+ if (failed2.length) {
9280
+ await ctx.journal("solutions.failed", `\u201C${name}\u201D could not finish: ${failed2.map((f) => `${f.leaf.ref} \u2014 ${f.failed}`).join("; ")}. The other branches were not held.`);
9077
9281
  }
9078
9282
  return out;
9079
9283
  }
@@ -9234,6 +9438,9 @@ function gateOne(ctx, w, leaf) {
9234
9438
  return { draft, refused };
9235
9439
  }
9236
9440
  function draftOf(ctx, w) {
9441
+ return { ...draftFields(w), sources: resolve3(ctx, w.restsOn) };
9442
+ }
9443
+ function draftFields(w) {
9237
9444
  const headline = w.changelog.headline.trim().replace(/\s+/g, " ");
9238
9445
  const room = CHANGELOG_MAX - headline.length - 1;
9239
9446
  return {
@@ -9243,8 +9450,7 @@ function draftOf(ctx, w) {
9243
9450
  steps: w.steps.map((s) => ({ do: s.do.trim().replace(/\s+/g, " "), who: s.who, ...s.checks ? { checks: true } : {} })),
9244
9451
  size: w.size,
9245
9452
  assumption: clipOrNull(w.worksIf, WORKS_IF_MAX),
9246
- reasoning: wholeSentences(w.why, WHY_THIS_MAX),
9247
- sources: resolve3(ctx, w.restsOn)
9453
+ reasoning: wholeSentences(w.why, WHY_THIS_MAX)
9248
9454
  };
9249
9455
  }
9250
9456
  function resolve3(ctx, ids) {
@@ -9312,14 +9518,13 @@ async function readRoadmap(client, projectId) {
9312
9518
  var SPEC_SKILL = "spec";
9313
9519
  var SPEC_MINUTES = 5;
9314
9520
  var SPEC_STEPS = {
9315
- brief: { says: "Writing what your users will see, and how we\u2019ll know", label: "Writing the brief" },
9316
- agent: { says: "Turning the brief into a spec a coding agent can build from", label: "Writing the agent spec" },
9521
+ solution: { says: "Writing what your users will see, and how we\u2019ll know", label: "Writing what your users will see" },
9522
+ agent: { says: "Turning the solution into a spec a coding agent can build from", label: "Writing the agent spec" },
9317
9523
  review: { says: "Reading both as you would, then as a coding agent would", label: "Checking both as a coding agent would" }
9318
9524
  };
9319
- function briefRefusal(brief2) {
9320
- if (!brief2.in_short.trim()) return "it does not say in short what we build";
9321
- if (!brief2.experience.some((s) => s.trim())) return "it does not walk through the experience";
9322
- if (!brief2.measure.signal.trim()) return "it does not say how we will know";
9525
+ function specRefusal(spec2) {
9526
+ if (!spec2.experience.some((s) => s.trim())) return "it does not walk through the experience";
9527
+ if (!spec2.measure.signal.trim()) return "it does not say how we will know";
9323
9528
  return null;
9324
9529
  }
9325
9530
  function agentSpecRefusal(markdown) {
@@ -9327,19 +9532,25 @@ function agentSpecRefusal(markdown) {
9327
9532
  if (!/^##\s+Acceptance criteria\b/im.test(markdown)) return "it has no acceptance criteria";
9328
9533
  return null;
9329
9534
  }
9330
- function specsRefusal({ brief: brief2, markdown }) {
9331
- const b = briefRefusal(brief2);
9332
- if (b) return `the brief ${b}`;
9535
+ function specsRefusal({ spec: spec2, markdown }) {
9536
+ const b = specRefusal(spec2);
9537
+ if (b) return `the spec ${b}`;
9333
9538
  const a = agentSpecRefusal(markdown);
9334
9539
  return a ? `the agent spec ${a}` : null;
9335
9540
  }
9541
+ function specRecord(mode, answer) {
9542
+ if (mode === "write") return { spec: answer.spec, markdown: answer.markdown, solution: null };
9543
+ if (!answer.solution) throw new Error("a rework writes the whole solution again, and this one came back without it");
9544
+ return { spec: answer.spec, markdown: answer.markdown, solution: answer.solution };
9545
+ }
9336
9546
  function specSolutionOf(params) {
9337
9547
  const id = params?.solution;
9338
9548
  return typeof id === "string" ? id : null;
9339
9549
  }
9340
9550
  async function queueSpecs(client, ask7) {
9551
+ const mode = ask7.mode ?? "write";
9341
9552
  const [{ data: solution }, { data: active }, { data: clone }] = await Promise.all([
9342
- client.from("solutions").select("id, ref, status, opportunity_id").eq("project_id", ask7.projectId).eq("ref", ask7.ref.trim().toUpperCase()).maybeSingle(),
9553
+ client.from("solutions").select("id, ref, status, opportunity_id, specced_at").eq("project_id", ask7.projectId).eq("ref", ask7.ref.trim().toUpperCase()).maybeSingle(),
9343
9554
  client.from("jobs").select("params").eq("project_id", ask7.projectId).eq("skill", SPEC_SKILL).in("status", [...ACTIVE_STATUSES]),
9344
9555
  // The clone on the machine that will run it: the daemon reads the code only in its own (`resolveCwd`).
9345
9556
  client.from("clones").select("repo_path").eq("project_id", ask7.projectId).eq("daemon_id", ask7.daemonId).maybeSingle()
@@ -9347,13 +9558,15 @@ async function queueSpecs(client, ask7) {
9347
9558
  if (!solution || !solution.opportunity_id) throw new Error(`No solution ${ask7.ref.toUpperCase()} on the Roadmap.`);
9348
9559
  if (solution.status === "rejected") throw new Error(`${solution.ref} was thrown out: it gets no specs.`);
9349
9560
  if ((active ?? []).some((j) => specSolutionOf(j.params) === solution.id)) throw new Error(`Nano is already writing the specs of ${solution.ref}.`);
9561
+ if (mode === "write" && solution.specced_at) throw new Error(`${solution.ref} has its specs already: rework the solution to write it all again.`);
9562
+ if (mode === "rework" && !solution.specced_at) throw new Error(`${solution.ref} has no specs yet: write them first.`);
9350
9563
  const { data, error } = await client.from("jobs").insert({
9351
9564
  project_id: ask7.projectId,
9352
9565
  skill: SPEC_SKILL,
9353
9566
  requested_by: ask7.userId,
9354
9567
  trigger: "user",
9355
9568
  target_daemon_id: ask7.daemonId,
9356
- params: { solution: solution.id, ...clone?.repo_path ? { repo_path: clone.repo_path } : {} }
9569
+ params: { solution: solution.id, ...mode === "rework" ? { rework: true } : {}, ...clone?.repo_path ? { repo_path: clone.repo_path } : {} }
9357
9570
  }).select("id").single();
9358
9571
  if (error?.code === "23505") throw new Error(`Nano is already writing the specs of ${solution.ref}.`);
9359
9572
  if (error || !data) throw new Error(error?.message ?? "could not ask Nano for the specs");
@@ -9364,40 +9577,30 @@ async function queueSpecs(client, ask7) {
9364
9577
  import { z as z15 } from "zod";
9365
9578
  var SpecParams = z15.object({
9366
9579
  solution: z15.guid(),
9580
+ /** *Rework the solution* (spec 74): the whole solution written again, as a new version. */
9581
+ rework: z15.literal(true).optional(),
9367
9582
  repo_path: z15.string().optional(),
9368
9583
  background: z15.boolean().optional()
9369
9584
  }).strict();
9370
9585
  function specAsk(params) {
9371
9586
  const parsed = SpecParams.safeParse(params);
9372
- if (parsed.success) return { ok: true, solution: parsed.data.solution };
9587
+ if (parsed.success) return { ok: true, solution: parsed.data.solution, mode: parsed.data.rework ? "rework" : "write" };
9373
9588
  const issue = parsed.error.issues[0];
9374
9589
  const unknown2 = issue.code === "unrecognized_keys" ? issue.keys?.join(", ") : null;
9375
9590
  return { ok: false, why: unknown2 ? `this run does not take ${unknown2}` : `${issue.path.join(".") || "params"}: ${issue.message}` };
9376
9591
  }
9377
- var Journey = z15.object({
9378
- kind: z15.literal("journey"),
9379
- steps: z15.array(prose(1)).max(9).describe('The user\u2019s path, three to seven steps, each a few words from their side of the screen: "Opens the trial", "Picks a closed search".'),
9380
- fork: z15.object({
9381
- after: z15.number().int().describe("The step the path forks after, counted from 1."),
9382
- when: prose(1).describe('When it forks, a few words: "No closed search".'),
9383
- then: prose(1).describe("Where it goes instead, a few words.")
9384
- }).nullable().default(null).describe("One fork at most, only when the path really splits.")
9592
+ var Diagram = z15.object({
9593
+ caption: prose(1).describe('What it shows, a few words: "Starting the trial on a closed search".'),
9594
+ mermaid: z15.string().min(1).max(4e3).describe("The diagram in Mermaid syntax, whichever type helps most: flowchart, sequenceDiagram, stateDiagram-v2, journey, or a flowchart with two subgraphs for before and after. At most about twelve nodes, labels of a few words in quotes. No styling: no classDef, style, colours, click or links.")
9385
9595
  });
9386
- var BeforeAfter = z15.object({
9387
- kind: z15.literal("before_after"),
9388
- before: z15.array(prose(1)).max(6).describe("What the user lives today, two to five short lines."),
9389
- after: z15.array(prose(1)).max(6).describe("What they will live, two to five short lines, side by side with before.")
9390
- });
9391
- var BriefFields = {
9392
- questions: z15.array(z15.object({ ask: prose(1).describe("The question, one sentence."), blocks: prose(1).describe('What it blocks, a few words: "what the trial screen says".') })).max(5).default([]).describe("To finish this brief: up to three questions only the founder can answer. Decide everything else yourself."),
9393
- in_short: prose(1).describe("In short: three sentences at most \u2014 what we build, for whom, what changes for them. The first could be read alone."),
9394
- diagrams: z15.array(z15.discriminatedUnion("kind", [Journey, BeforeAfter])).max(2).default([]).describe("At most two. The journey whenever the experience has three steps or more; before and after when the solution changes something the product already does."),
9596
+ var SpecFields = {
9597
+ questions: z15.array(z15.object({ ask: prose(1).describe("The question, one sentence."), blocks: prose(1).describe('What it blocks, a few words: "what the trial screen says".') })).max(5).default([]).describe("Questions for you: up to three questions only the founder can answer, and any place the code shows the solution as settled is wrong. Decide everything else yourself."),
9598
+ in_short: prose(1).describe("In short: three sentences at most \u2014 what we build, for whom, what changes for them. The first could be read alone. Read first, right after the problem: not the proposed solution again, its gist."),
9599
+ diagrams: z15.array(Diagram).max(2).default([]).describe("Optional: only when a drawing makes it faster to understand than the words. At most two, in Mermaid, the shape that helps most."),
9395
9600
  experience: z15.array(prose(1)).max(8).describe("The experience: three to six steps, in the present tense, from the user\u2019s side of the screen. Under a hypothesis, the founder\u2019s check comes first, in one line."),
9396
- build: z15.array(prose(1)).max(8).describe("What we build: three to six points, in users\u2019 words \u2014 what is true the day it ships."),
9397
9601
  wont_do: z15.array(z15.object({ what: prose(1), why: prose(1) })).max(6).default([]).describe("What we won\u2019t do: two to four points, each with why or when."),
9398
9602
  measure: z15.object({
9399
9603
  signal: prose(1).describe("The signal, one sentence: who \xB7 does what \xB7 how many \xB7 by when. Small numbers for a product with few users. Never a baseline you did not measure."),
9400
- wrong_if: prose(1).describe("We were wrong if\u2026, one sentence. Under a hypothesis, it can show the problem itself wrong."),
9401
9604
  existing: z15.array(z15.object({ name: prose(1).describe("The event as the product records it, exactly."), counts: prose(1).describe("What it counts here, and where you found it.") })).max(8).default([]).describe("Events the product already records that measure this, found in the code or in the connected source. Only what you found."),
9402
9605
  added: z15.array(
9403
9606
  z15.object({
@@ -9411,22 +9614,26 @@ var BriefFields = {
9411
9614
  dependencies: z15.array(prose(1)).max(6).default([]).describe('What must exist first. Empty when nothing: the page says None. No analytics tool in the product: "An analytics tool" is one.')
9412
9615
  };
9413
9616
  var Notes2 = prose().nullable().default(null).describe("A sentence or two, for the Journal.");
9414
- var BriefAnswer = z15.object({
9415
- ...BriefFields,
9416
- code: z15.array(z15.object({ path: z15.string().max(200), holds: prose(1).describe("What it holds that matters here, one line.") })).max(16).default([]).describe("The files you read that matter for this change, each with what it holds, for the agent spec\u2019s *Start here*. Empty when the code was not there."),
9617
+ var Code = z15.array(z15.object({ path: z15.string().max(200), holds: prose(1).describe("What it holds that matters here, one line.") })).max(16).default([]).describe("The files you read that matter for this change, each with what it holds, for the agent spec\u2019s *Start here*. Empty when the code was not there.");
9618
+ var Rewritten = Written.omit({ restsOn: true });
9619
+ var SolutionAnswer = z15.object({
9620
+ solution: Rewritten.nullable().default(null).describe('Only when the snapshot says mode "rework": the whole solution, written again. Null in mode "write": those fields are settled.'),
9621
+ ...SpecFields,
9622
+ code: Code,
9417
9623
  notes: Notes2
9418
9624
  });
9625
+ var AgentMarkdown = z15.string().min(1).max(6e4);
9419
9626
  var AgentAnswer = z15.object({
9420
- markdown: prose(1).max(6e4).describe("The agent spec, whole, in markdown, with the sections named in your instructions."),
9627
+ markdown: AgentMarkdown.describe("The agent spec, whole, in markdown, with the sections named in your instructions."),
9421
9628
  notes: Notes2
9422
9629
  });
9423
9630
  var ReviewAnswer = z15.object({
9424
- brief: z15.object(BriefFields).describe("The brief, whole, corrected where it failed a reader; as it was where it did not."),
9425
- markdown: prose(1).max(6e4).describe("The agent spec, whole, corrected; as it was where it did not fail."),
9631
+ spec: z15.object(SpecFields).describe("The solution\u2019s sections, whole, corrected where they failed a reader; as they were where they did not."),
9632
+ markdown: AgentMarkdown.describe("The agent spec, whole, corrected; as it was where it did not fail."),
9426
9633
  exchanges: z15.array(z15.object({ asked: prose(1).describe("What the coding agent would have had to guess, as its question, one short sentence."), did: prose(1).describe("What you did: decided it (and what), or left it to the founder.") })).max(5).default([]).describe("Two to five: the questions a coding agent asked of the spec, and what you did about each. They go in the Journal."),
9427
9634
  changed: z15.array(z15.string().max(200)).max(10).default([]).describe("What you changed, a line each, for the Journal.")
9428
9635
  });
9429
- var SCHEMAS7 = { brief: BriefAnswer, agent: AgentAnswer, review: ReviewAnswer };
9636
+ var SCHEMAS7 = { solution: SolutionAnswer, agent: AgentAnswer, review: ReviewAnswer };
9430
9637
  function parseSpecAnswer(role7, json2) {
9431
9638
  return SCHEMAS7[role7].parse(json2);
9432
9639
  }
@@ -9435,32 +9642,32 @@ function specOutputSchema(role7) {
9435
9642
  }
9436
9643
 
9437
9644
  // ../daemon/src/git.ts
9438
- import { execFile as execFile4 } from "child_process";
9439
- import { promisify as promisify4 } from "util";
9440
- var execFileAsync4 = promisify4(execFile4);
9441
- var MAX_BUFFER = 4 * 1024 * 1024;
9645
+ import { execFile as execFile5 } from "child_process";
9646
+ import { promisify as promisify5 } from "util";
9647
+ var execFileAsync5 = promisify5(execFile5);
9648
+ var MAX_BUFFER2 = 4 * 1024 * 1024;
9442
9649
  var WIKI_DIR = ".nanopm/wiki/docs";
9443
- async function git2(cwd, args) {
9444
- const { stdout } = await execFileAsync4("git", ["-C", cwd, ...args], { maxBuffer: MAX_BUFFER, timeout: 2e4 });
9650
+ async function git3(cwd, args) {
9651
+ const { stdout } = await execFileAsync5("git", ["-C", cwd, ...args], { maxBuffer: MAX_BUFFER2, timeout: 2e4 });
9445
9652
  return stdout.trim();
9446
9653
  }
9447
9654
  async function listTrackedFiles(cwd) {
9448
- const lines = (args) => git2(cwd, args).then((out) => out ? out.split("\n") : [], () => []);
9655
+ const lines = (args) => git3(cwd, args).then((out) => out ? out.split("\n") : [], () => []);
9449
9656
  const [files, wiki] = await Promise.all([lines(["ls-files"]), lines(["ls-files", "--others", "--ignored", "--exclude-standard", "--", WIKI_DIR])]);
9450
9657
  return { files, wiki };
9451
9658
  }
9452
9659
  async function headCommit(cwd) {
9453
- return git2(cwd, ["rev-parse", "--short", "HEAD"]).then((sha) => sha || null, () => null);
9660
+ return git3(cwd, ["rev-parse", "--short", "HEAD"]).then((sha) => sha || null, () => null);
9454
9661
  }
9455
9662
  async function collectGitDigest(cwd, recent = 30) {
9456
9663
  try {
9457
- await git2(cwd, ["rev-parse", "--git-dir"]);
9664
+ await git3(cwd, ["rev-parse", "--git-dir"]);
9458
9665
  } catch {
9459
9666
  return null;
9460
9667
  }
9461
9668
  const safe = async (args) => {
9462
9669
  try {
9463
- return await git2(cwd, args);
9670
+ return await git3(cwd, args);
9464
9671
  } catch {
9465
9672
  return "";
9466
9673
  }
@@ -9557,6 +9764,7 @@ async function runSpecsJob({ client, job, adapter, spec: spec2, onEvent }) {
9557
9764
  const solution = row;
9558
9765
  if (!solution?.opportunity_id) throw new Error("That solution is not on the Roadmap.");
9559
9766
  if (solution.status === "rejected") throw new Error(`${solution.ref} was thrown out: it gets no specs.`);
9767
+ const mode = asked.mode;
9560
9768
  const clone = typeof job.params?.repo_path === "string";
9561
9769
  const [all, card, stance, numbers, connections, commit] = await Promise.all([
9562
9770
  readOpportunities(client, projectId),
@@ -9571,16 +9779,8 @@ async function runSpecsJob({ client, job, adapter, spec: spec2, onEvent }) {
9571
9779
  const path = leaf ? pathOf(tree, leaf.id) : [];
9572
9780
  const known = {
9573
9781
  today: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
9574
- solution: {
9575
- ref: solution.ref,
9576
- title: solution.title,
9577
- proposed_solution: solution.description,
9578
- future_changelog: parseChangelog(solution.changelog),
9579
- plan: solution.steps,
9580
- size: solution.size,
9581
- works_if: solution.assumption,
9582
- why: solution.reasoning
9583
- },
9782
+ mode,
9783
+ solution: snapshotOf({ ref: solution.ref, title: solution.title, description: solution.description, changelog: solution.changelog ?? "", steps: solution.steps, size: solution.size ?? "small", assumption: solution.assumption, reasoning: solution.reasoning }),
9584
9784
  problem: leaf ? problemOf(tree, leaf) : null,
9585
9785
  part_of: path.slice(1).map((p) => p.problem),
9586
9786
  objective: lensOf(card.goals),
@@ -9590,27 +9790,37 @@ async function runSpecsJob({ client, job, adapter, spec: spec2, onEvent }) {
9590
9790
  numbers
9591
9791
  };
9592
9792
  const reads = clone ? `the code read at ${commit ?? "its current commit"}` : "no clone here, so the code is not read";
9593
- await journal("spec.read", `Writing the specs of \u201C${clip3(solution.title, 80)}\u201D (${solution.ref}): ${reads}; ${connections.length ? `${connections.length} connected source${connections.length === 1 ? "" : "s"}` : "no analytics connected"}.`);
9793
+ const doing = mode === "rework" ? "Reworking" : "Writing the specs of";
9794
+ await journal("spec.read", `${doing} \u201C${clip3(solution.title, 80)}\u201D (${solution.ref}): ${reads}; ${connections.length ? `${connections.length} connected source${connections.length === 1 ? "" : "s"}` : "no analytics connected"}.`);
9594
9795
  const ctx = { adapter, spec: spec2, onEvent };
9595
9796
  const timings = [];
9596
- const time = (what, cost) => timings.push({ what, ms: cost.ms });
9797
+ const time = (what2, cost) => timings.push({ what: what2, ms: cost.ms });
9597
9798
  const repo = clone ? { repository: "the working directory", why_no_repository: null } : { repository: null, why_no_repository: "No clone of the product rode along with this run: work from what the snapshot says the product does." };
9598
9799
  const writerGrant = [...clone ? ["read_repo"] : [], ...connections.length ? ["read_data"] : []];
9599
- const brief2 = await ask3(ctx, "brief", { grant: writerGrant, prompt: { ...known, ...repo, connections } });
9600
- time("brief", brief2.cost);
9601
- const { code, notes: briefNotes, ...draft } = brief2.answer;
9602
- await step("spec.brief", `Brief written: ${count(draft.build.length, "point")} built, ${count(draft.questions.length, "question")} for you, ${count(draft.measure.added.length, "new event")}.${briefNotes ? ` ${briefNotes}` : ""}`, brief2.cost);
9800
+ const writing = { grant: writerGrant, prompt: { ...known, ...repo, connections } };
9801
+ const written = await ask3(ctx, "solution", writing);
9802
+ time("solution", written.cost);
9803
+ const { solution: rewritten, code, notes, ...draft } = written.answer;
9804
+ const fields = mode === "rework" && rewritten ? draftFields(rewritten) : null;
9805
+ const wrote = fields ? `Solution written again: \u201C${clip3(fields.title, 80)}\u201D` : `Sections written: ${count(draft.experience.length, "step")} of the experience`;
9806
+ await step("spec.solution", `${wrote}, ${count(draft.questions.length, "question")} for you, ${count(draft.measure.added.length, "new event")}.${notes ? ` ${notes}` : ""}`, written.cost);
9807
+ const whole = fields ? snapshotOf({ ref: solution.ref, ...fields }) : known.solution;
9808
+ const first = specRecord(mode, { spec: draft, markdown: "", solution: fields });
9809
+ const unreadable = specRefusal(first.spec) ?? (first.solution && leaf ? solutionRefusal(first.solution, leaf) : null);
9810
+ if (unreadable) throw new Error(`The solution cannot be recorded: ${unreadable}.`);
9811
+ const target = await recordSections(client, { projectId, jobId: job.id, solution, spec: first.spec, fields: first.solution });
9812
+ await journal("spec.sections", first.solution ? `${target.ref} \u201C${clip3(first.solution.title, 80)}\u201D takes ${solution.ref}\u2019s place on the board; its agent spec is on its way.` : `Sections on the sheet; the agent spec is on its way.`);
9603
9813
  const agent = await ask3(ctx, "agent", {
9604
9814
  grant: clone ? ["read_repo"] : [],
9605
- prompt: { today: known.today, ref: solution.ref, commit: commit ?? "not read", ...repo, brief: draft, code, solution: known.solution, problem: known.problem, product: known.product, constraints: stance }
9815
+ prompt: { today: known.today, ref: target.ref, commit: commit ?? "not read", ...repo, solution: whole, spec: draft, code, problem: known.problem, product: known.product, constraints: stance }
9606
9816
  });
9607
9817
  time("agent spec", agent.cost);
9608
9818
  await step("spec.agent", `Agent spec written.${agent.answer.notes ? ` ${agent.answer.notes}` : ""}`, agent.cost);
9609
- let final = { brief: draft, markdown: agent.answer.markdown };
9819
+ let final = { spec: first.spec, markdown: agent.answer.markdown };
9610
9820
  try {
9611
- const review2 = await ask3(ctx, "review", { prompt: { brief: draft, markdown: agent.answer.markdown, solution: known.solution, problem: known.problem } });
9821
+ const review2 = await ask3(ctx, "review", { prompt: { mode, solution: whole, spec: draft, markdown: agent.answer.markdown, problem: known.problem } });
9612
9822
  time("review", review2.cost);
9613
- const reviewed = { brief: review2.answer.brief, markdown: review2.answer.markdown };
9823
+ const reviewed = { spec: review2.answer.spec, markdown: review2.answer.markdown };
9614
9824
  const refused2 = specsRefusal(reviewed);
9615
9825
  if (refused2) await journal("spec.review_kept_out", `The review\u2019s version was left out (${refused2}): the writers\u2019 drafts are recorded.`);
9616
9826
  else final = reviewed;
@@ -9624,19 +9834,15 @@ async function runSpecsJob({ client, job, adapter, spec: spec2, onEvent }) {
9624
9834
  await journal("spec.review_failed", `The review did not finish (${clip3(errorMessage(err), 200)}): the writers\u2019 drafts are recorded.`);
9625
9835
  }
9626
9836
  const refused = specsRefusal(final);
9627
- if (refused) throw new Error(`The specs cannot be read: ${refused}.`);
9628
- const { error: published } = await client.rpc("publish_spec", {
9629
- p_project_id: projectId,
9630
- p_solution_id: solution.id,
9631
- p_brief: final.brief,
9632
- p_agent_spec: final.markdown
9633
- });
9837
+ if (refused) throw new Error(`The specs cannot be recorded: ${refused}.`);
9838
+ const { error: published } = await client.rpc("publish_solution_spec", { p_project_id: projectId, p_solution_id: target.id, p_spec: final.spec, p_agent_spec: final.markdown });
9634
9839
  if (published) throw new Error(`could not record the specs: ${published.message}`);
9635
- const summary = `Specs of \u201C${clip3(solution.title, 80)}\u201D (${solution.ref}) written: the brief, with ${count(final.brief.questions.length, "question")} for you, and the agent spec. Took ${duration(Date.now() - started)}: ${timings.map((t) => `${t.what} ${duration(t.ms)}`).join(", ")}.`;
9840
+ const what = first.solution ? `\u201C${clip3(solution.title, 80)}\u201D (${solution.ref}) reworked as ${target.ref} \u201C${clip3(first.solution.title, 80)}\u201D` : `Specs of \u201C${clip3(solution.title, 80)}\u201D (${solution.ref}) written`;
9841
+ const summary = `${what}: ${count(final.spec.questions.length, "question")} for you, and the agent spec. Took ${duration(Date.now() - started)}: ${timings.map((t) => `${t.what} ${duration(t.ms)}`).join(", ")}.`;
9636
9842
  await journal("spec.summary", summary);
9637
9843
  return { status: "succeeded", result: summary };
9638
9844
  }
9639
- var ROLE_NAME = { brief: "spec brief writer", agent: "spec agent writer", review: "spec reviewer" };
9845
+ var ROLE_NAME = { solution: "spec solution writer", agent: "spec agent writer", review: "spec reviewer" };
9640
9846
  async function ask3(ctx, role7, call2) {
9641
9847
  const cost = await runRole({
9642
9848
  adapter: ctx.adapter,
@@ -9653,6 +9859,33 @@ async function ask3(ctx, role7, call2) {
9653
9859
  });
9654
9860
  return { answer: stripDeep(parseSpecAnswer(role7, JSON.parse(cost.text))), cost };
9655
9861
  }
9862
+ async function recordSections(client, r) {
9863
+ if (!r.fields) {
9864
+ const { error: error2 } = await client.rpc("publish_solution_spec", { p_project_id: r.projectId, p_solution_id: r.solution.id, p_spec: r.spec });
9865
+ if (error2) throw new Error(`could not record the sections: ${error2.message}`);
9866
+ return { id: r.solution.id, ref: r.solution.ref };
9867
+ }
9868
+ const f = r.fields;
9869
+ const { data, error } = await client.rpc("rework_spec", {
9870
+ p_project_id: r.projectId,
9871
+ p_solution_id: r.solution.id,
9872
+ p_job_id: r.jobId,
9873
+ p_title: f.title,
9874
+ p_description: f.description ?? "",
9875
+ p_changelog: f.changelog,
9876
+ p_steps: f.steps,
9877
+ p_size: f.size,
9878
+ p_assumption: f.assumption ?? "",
9879
+ p_reasoning: f.reasoning,
9880
+ p_spec: r.spec
9881
+ });
9882
+ const made = data?.[0];
9883
+ if (error || !made) throw new Error(`could not record the reworked solution: ${error?.message ?? "no new version"}`);
9884
+ return made;
9885
+ }
9886
+ function snapshotOf(s) {
9887
+ return { ref: s.ref, title: s.title, proposed_solution: s.description, future_changelog: parseChangelog(s.changelog), plan: s.steps, size: s.size, works_if: s.assumption, why: s.reasoning };
9888
+ }
9656
9889
  function problemOf(tree, leaf) {
9657
9890
  return { ref: placeOf(tree, leaf.id) ?? leaf.ref, problem: leaf.problem, description: leaf.description ?? null, status: leaf.evidence_status, confirmed_by_founder: leaf.status === "confirmed" };
9658
9891
  }
@@ -9824,13 +10057,13 @@ var told = (r) => ({
9824
10057
  });
9825
10058
 
9826
10059
  // ../daemon/src/objective-definitions.ts
9827
- import { execFile as execFile5 } from "child_process";
10060
+ import { execFile as execFile6 } from "child_process";
9828
10061
  import { randomUUID as randomUUID11 } from "crypto";
9829
10062
  import { existsSync as existsSync3 } from "fs";
9830
- import { mkdir as mkdir3, readFile as readFile5 } from "fs/promises";
9831
- import { join as join9 } from "path";
9832
- import { promisify as promisify5 } from "util";
9833
- var execFileAsync5 = promisify5(execFile5);
10063
+ import { mkdir as mkdir3, readFile as readFile6 } from "fs/promises";
10064
+ import { join as join10 } from "path";
10065
+ import { promisify as promisify6 } from "util";
10066
+ var execFileAsync6 = promisify6(execFile6);
9834
10067
  var DEFINITIONS_TIMEOUT_MS = 9e4;
9835
10068
  var DEFINITIONS_THINKING_TOKENS = 2e3;
9836
10069
  var READ_TOOLS3 = /* @__PURE__ */ new Set(["Read", "Glob", "Grep", "StructuredOutput"]);
@@ -9839,7 +10072,7 @@ var COUNTING_LINES_MAX = 80;
9839
10072
  var PER_FILE = 4;
9840
10073
  async function countingLines(cwd) {
9841
10074
  try {
9842
- const { stdout } = await execFileAsync5("git", ["grep", "-n", "-I", "-i", "-E", COUNTING, "--", ".", ":!*lock*", ":!*.min.*", ":!*.map", ":!*.svg", ":!*.snap"], {
10075
+ const { stdout } = await execFileAsync6("git", ["grep", "-n", "-I", "-i", "-E", COUNTING, "--", ".", ":!*lock*", ":!*.min.*", ":!*.map", ":!*.svg", ":!*.snap"], {
9843
10076
  cwd,
9844
10077
  maxBuffer: 16 * 1024 * 1024,
9845
10078
  timeout: 5e3
@@ -9864,7 +10097,7 @@ function pickLines(raw) {
9864
10097
  }
9865
10098
  async function killRecorded(runDir) {
9866
10099
  try {
9867
- const pid = Number((await readFile5(join9(runDir, "harness.pid"), "utf8")).trim());
10100
+ const pid = Number((await readFile6(join10(runDir, "harness.pid"), "utf8")).trim());
9868
10101
  if (pid > 1) process.kill(pid, "SIGKILL");
9869
10102
  } catch {
9870
10103
  }
@@ -9884,7 +10117,7 @@ async function readDefinitions({ adapter, spec: spec2, publish: publish5, journa
9884
10117
  if (!hasRepo) return give({}, "No repository to read: the card offers the common definitions.", "ok");
9885
10118
  try {
9886
10119
  const skill = await loadSkill("define-objective");
9887
- const runDir = join9(spec2.runDir, "definitions");
10120
+ const runDir = join10(spec2.runDir, "definitions");
9888
10121
  await mkdir3(runDir, { recursive: true, mode: 448 });
9889
10122
  const schema = definitionsOutputSchema();
9890
10123
  const [lines, listed] = await Promise.all([countingLines(spec2.cwd), listTrackedFiles(spec2.cwd)]);
@@ -9946,7 +10179,7 @@ ${renderTrackedFiles(listed.files, void 0, listed.wiki)}
9946
10179
  if (!result) throw new Error("no answer");
9947
10180
  const parsed = DefinitionsAnswer.safeParse(JSON.parse(result));
9948
10181
  if (!parsed.success) throw new Error("an answer in the wrong shape");
9949
- const checked = checkProposals(parsed.data, (file) => existsSync3(join9(spec2.cwd, file)));
10182
+ const checked = checkProposals(parsed.data, (file) => existsSync3(join10(spec2.cwd, file)));
9950
10183
  const definitions2 = lever ? checked[lever] ? { [lever]: checked[lever] } : {} : checked;
9951
10184
  const kept = Object.values(definitions2).reduce((n, list) => n + (list?.length ?? 0), 0);
9952
10185
  const read6 = Object.values(definitions2).flat().filter((d) => d?.found).length;
@@ -10220,9 +10453,9 @@ function localDate(timezone, at) {
10220
10453
  // ../daemon/src/run-job.ts
10221
10454
  import { randomUUID as randomUUID12 } from "crypto";
10222
10455
  import { existsSync as existsSync4 } from "fs";
10223
- import { mkdir as mkdir4, readFile as readFile6, rm as rm3 } from "fs/promises";
10224
- import { homedir as homedir3, tmpdir } from "os";
10225
- import { join as join10, resolve as resolve4 } from "path";
10456
+ import { mkdir as mkdir5, readFile as readFile8, rm as rm4 } from "fs/promises";
10457
+ import { homedir as homedir4, tmpdir } from "os";
10458
+ import { join as join12, resolve as resolve4 } from "path";
10226
10459
 
10227
10460
  // ../daemon/src/events.ts
10228
10461
  var JobEventWriter = class {
@@ -10302,195 +10535,166 @@ function lighten(value) {
10302
10535
  return value ?? null;
10303
10536
  }
10304
10537
 
10305
- // ../daemon/src/repo.ts
10306
- import { execFile as execFile6 } from "child_process";
10307
- import { promisify as promisify6 } from "util";
10308
- var execFileAsync6 = promisify6(execFile6);
10309
- var MAX_BUFFER2 = 4 * 1024 * 1024;
10310
- var TIMEOUT_MS = 6e4;
10311
- async function git3(cwd, args, timeoutMs = TIMEOUT_MS) {
10312
- const { stdout } = await execFileAsync6("git", ["-C", cwd, ...args], { maxBuffer: MAX_BUFFER2, timeout: timeoutMs });
10313
- return stdout.trim();
10314
- }
10315
- var RepoError = class extends Error {
10316
- };
10317
- function branchName(ref) {
10318
- const slug = ref.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
10319
- if (!slug) throw new RepoError(`"${ref}" has no letters or digits to make a branch name from`);
10320
- return `nanopm/${slug}`;
10321
- }
10322
- async function readState(cwd) {
10323
- try {
10324
- await git3(cwd, ["rev-parse", "--git-dir"]);
10325
- } catch {
10326
- throw new RepoError(`${cwd} is not a git repository`);
10327
- }
10328
- const [branch, head, status2, remoteUrl] = await Promise.all([
10329
- git3(cwd, ["branch", "--show-current"]).catch(() => ""),
10330
- git3(cwd, ["rev-parse", "HEAD"]).catch(() => ""),
10331
- git3(cwd, ["status", "--porcelain"]).catch(() => ""),
10332
- git3(cwd, ["remote", "get-url", "origin"]).catch(() => "")
10333
- ]);
10334
- const dirty = status2.split("\n").filter(Boolean).map((l) => l.slice(3));
10335
- return { branch, head, clean: dirty.length === 0, dirty, hasRemote: Boolean(remoteUrl), remoteUrl: remoteUrl || void 0 };
10336
- }
10337
- async function ensureCleanTree(cwd) {
10338
- const state = await readState(cwd);
10339
- if (!state.head) {
10340
- throw new RepoError("this repository has no commits yet; make the first commit, then run nanopm next");
10341
- }
10342
- if (!state.clean) {
10343
- const shown = state.dirty.slice(0, 5).join(", ");
10344
- const more = state.dirty.length > 5 ? `, and ${state.dirty.length - 5} more` : "";
10345
- throw new RepoError(
10346
- `the working tree has uncommitted changes (${shown}${more}). NanoPM commits only its own work, so commit or stash yours first.`
10347
- );
10348
- }
10349
- return state;
10350
- }
10351
- async function createBranch(cwd, name) {
10352
- const existing = await git3(cwd, ["branch", "--list", name]).catch(() => "");
10353
- if (existing.trim()) {
10354
- throw new RepoError(`branch ${name} already exists. Delete it or finish what is on it first.`);
10355
- }
10356
- await git3(cwd, ["checkout", "-b", name]);
10357
- }
10358
- async function commitAll(cwd, message) {
10359
- await git3(cwd, ["add", "-A"]);
10360
- const staged = await git3(cwd, ["diff", "--cached", "--name-only"]).catch(() => "");
10361
- if (!staged) return null;
10362
- await git3(cwd, ["commit", "-m", message, "-m", "Written by NanoPM. Review before merging."]);
10363
- const sha = await git3(cwd, ["rev-parse", "--short", "HEAD"]);
10364
- return { sha, ...await diffStat(cwd) };
10365
- }
10366
- async function diffStat(cwd) {
10367
- const numstat = await git3(cwd, ["show", "--numstat", "--format=", "HEAD"]).catch(() => "");
10368
- let files = 0;
10369
- let insertions = 0;
10370
- let deletions = 0;
10371
- for (const line of numstat.split("\n").filter(Boolean)) {
10372
- const [add2 = "", del = ""] = line.split(" ");
10373
- files += 1;
10374
- insertions += Number(add2) || 0;
10375
- deletions += Number(del) || 0;
10376
- }
10377
- return { files, insertions, deletions };
10378
- }
10379
- async function pushBranch(cwd, name) {
10380
- const state = await readState(cwd);
10381
- if (!state.hasRemote) return { pushed: false, reason: "this repository has no origin remote" };
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;
10382
10549
  try {
10383
- const { stderr } = await execFileAsync6("git", ["-C", cwd, "push", "--set-upstream", "origin", name], {
10384
- maxBuffer: MAX_BUFFER2,
10385
- timeout: 12e4
10386
- });
10387
- return { pushed: true, reviewUrl: reviewUrlFrom(stderr) };
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)) } };
10388
10560
  } catch (err) {
10389
- const text2 = errorMessage(err);
10390
- return { pushed: false, reason: text2.split("\n").slice(-3).join(" ").trim() || "git push failed" };
10561
+ if (made) await removeWorktree(clone, dir);
10562
+ return { ok: false, outcome: failed(errorMessage(err)) };
10391
10563
  }
10392
10564
  }
10393
- function reviewUrlFrom(pushOutput) {
10394
- const match = /https?:\/\/\S+/.exec(pushOutput);
10395
- return match?.[0]?.replace(/[).,]+$/, "");
10396
- }
10397
- async function checkout(cwd, ref) {
10398
- await git3(cwd, ["checkout", ref]);
10399
- }
10400
- async function deleteBranch(cwd, name) {
10401
- await git3(cwd, ["branch", "-D", name]).catch(() => "");
10402
- }
10403
- async function changedFiles(cwd) {
10404
- const status2 = await git3(cwd, ["status", "--porcelain"]).catch(() => "");
10405
- return status2.split("\n").filter(Boolean).map((line) => line.slice(3).trim()).map((path) => path.includes(" -> ") ? path.split(" -> ")[1].trim() : path).map((path) => path.replace(/^"|"$/g, ""));
10406
- }
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 ? `
10407
10607
 
10408
- // ../daemon/src/next-run.ts
10409
- async function loadAcceptedSolution(client, projectId, solutionId) {
10410
- const { data, error } = await client.from("solutions").select("id, ref, title, reasoning, status, assumption, eval, sources").eq("id", solutionId).eq("project_id", projectId).is("superseded_by", null).maybeSingle();
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();
10411
10645
  if (error) throw new Error(`could not read the solution: ${error.message}`);
10412
10646
  if (!data) throw new Error("that solution is not on this project any more");
10413
- if (data.status !== "accepted" && data.status !== "building") {
10414
- throw new Error(`${data.ref} is ${data.status}. Only a solution you accepted can be built.`);
10415
- }
10416
- return { ...data, sources: data.sources ?? [] };
10417
- }
10418
- async function startSolution(client, projectId, ref, jobId) {
10419
- const { error } = await client.rpc("start_solution", { p_project_id: projectId, p_ref: ref, p_job_id: jobId });
10420
- if (error && !/only an accepted solution/.test(error.message)) throw new Error(pgMessage(error));
10421
- }
10422
- async function unstartSolution(client, projectId, ref) {
10423
- await client.rpc("unstart_solution", { p_project_id: projectId, p_ref: ref });
10647
+ return data;
10424
10648
  }
10425
- function renderSolution(solution) {
10426
- const lines = [`${solution.ref} ${solution.title}`, "", `Why: ${solution.reasoning}`];
10427
- if (solution.assumption) lines.push(`Bets on: ${solution.assumption}`);
10428
- if (solution.eval) lines.push(`We'll know: ${solution.eval}`);
10429
- if (solution.sources.length) {
10430
- lines.push(`Rests on: ${solution.sources.map((s) => `${s.kind}:${s.ref}`).join(", ")}`);
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
+ `);
10431
10664
  }
10432
- return lines.join("\n");
10433
10665
  }
10434
- async function openRepoSession(cwd, ref) {
10435
- const state = await ensureCleanTree(cwd);
10436
- if (!state.branch) {
10437
- throw new RepoError("this repository has a detached HEAD; check out a branch first");
10438
- }
10439
- const branch = branchName(ref);
10440
- await createBranch(cwd, branch);
10441
- return { branch, baseBranch: state.branch };
10666
+ function stepsOf(input) {
10667
+ const steps = input?.steps;
10668
+ return Array.isArray(steps) && steps.length && steps.every((s) => typeof s === "string") ? steps : null;
10442
10669
  }
10443
- async function closeRepoSession(cwd, session2, item) {
10444
- return closeBranch(cwd, session2, `${item.ref}: ${item.title}`, { commitErrorThrows: true });
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");
10445
10679
  }
10446
- async function abandonRepoSession(cwd, session2, item) {
10447
- return closeBranch(cwd, session2, item ? `${item.ref}: ${item.title}` : "WIP: nanopm did not finish", { commitErrorThrows: false });
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;
10448
10683
  }
10449
- async function closeBranch(cwd, session2, message, { commitErrorThrows }) {
10450
- let commit;
10451
- try {
10452
- commit = await commitAll(cwd, message);
10453
- } catch (err) {
10454
- await checkout(cwd, session2.baseBranch).catch(() => void 0);
10455
- if (commitErrorThrows) throw err;
10456
- commit = null;
10457
- }
10458
- if (!commit) {
10459
- await checkout(cwd, session2.baseBranch).catch(() => void 0);
10460
- await deleteBranch(cwd, session2.branch);
10461
- return { branch: session2.branch, pushed: false, note: "nothing was changed, so no branch was kept" };
10462
- }
10463
- const push = await pushBranch(cwd, session2.branch);
10464
- await checkout(cwd, session2.baseBranch).catch(() => void 0);
10465
- return {
10466
- branch: session2.branch,
10467
- commit,
10468
- pushed: push.pushed,
10469
- reviewUrl: push.reviewUrl,
10470
- note: push.pushed ? void 0 : `not pushed: ${push.reason}`
10471
- };
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");
10472
10691
  }
10473
- async function finishSolution(client, job, solution, result) {
10474
- const sources2 = [{ kind: "branch", ref: result.branch }];
10475
- if (result.commit?.sha) sources2.push({ kind: "git", ref: result.commit.sha });
10476
- if (result.reviewUrl) sources2.push({ kind: "url", ref: result.reviewUrl });
10477
- sources2.push({ kind: "job", ref: job.id });
10478
- const { error } = await client.rpc("finish_solution", {
10479
- p_project_id: job.project_id,
10480
- p_ref: solution.ref,
10481
- p_job_id: job.id,
10482
- p_sources: sources2
10483
- });
10484
- if (error) throw new Error(`the work is committed on ${result.branch}, but recording it failed: ${error.message}`);
10485
- }
10486
- function renderRepoResult(result) {
10487
- if (!result.commit) return result.note ?? "nothing changed";
10488
- const { files, insertions, deletions } = result.commit;
10489
- const stat2 = `${files} file${files === 1 ? "" : "s"} changed, ${insertions} insertion${insertions === 1 ? "" : "s"}, ${deletions} deletion${deletions === 1 ? "" : "s"}`;
10490
- const lines = [`${result.branch} \xB7 ${result.commit.sha} \xB7 ${stat2}`];
10491
- if (result.reviewUrl) lines.push(result.reviewUrl);
10492
- else if (result.note) lines.push(result.note);
10493
- return lines.join("\n");
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" });
10494
10698
  }
10495
10699
 
10496
10700
  // ../daemon/src/run-job.ts
@@ -10508,107 +10712,6 @@ async function askAboutSite(opts) {
10508
10712
  });
10509
10713
  }
10510
10714
 
10511
- // ../daemon/src/verify.ts
10512
- import { spawn as spawn3 } from "child_process";
10513
- var VERIFY_TIMEOUT_MS = 10 * 6e4;
10514
- var OUTPUT_TAIL = 4e3;
10515
- var DEFINES_THE_COMMAND = [
10516
- /(^|\/)package\.json$/,
10517
- /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb)$/,
10518
- /(^|\/)(Makefile|justfile|Justfile|Taskfile\.ya?ml)$/,
10519
- /(^|\/)\.husky\//,
10520
- /(^|\/)\.github\//,
10521
- /(^|\/)(pyproject\.toml|uv\.lock|Cargo\.toml|Cargo\.lock|Gemfile|Gemfile\.lock)$/
10522
- ];
10523
- function pathsNamedIn(command) {
10524
- return command.split(/[\s;&|()<>]+/).filter((t) => /[/.]/.test(t) && !/^-/.test(t) && t.length > 2).map((t) => t.replace(/^\.\//, "").replace(/^["']|["']$/g, ""));
10525
- }
10526
- function guardVerify(changed, command) {
10527
- for (const file of changed) {
10528
- const pattern = DEFINES_THE_COMMAND.find((p) => p.test(file));
10529
- if (pattern) {
10530
- return `the change touched ${file}, which decides what the verify command does, so the command was not run`;
10531
- }
10532
- }
10533
- const named = pathsNamedIn(command);
10534
- for (const file of changed) {
10535
- if (named.some((n) => file === n || file.endsWith(`/${n}`) || n.endsWith(file))) {
10536
- return `the change touched ${file}, which the verify command runs, so the command was not run`;
10537
- }
10538
- }
10539
- return null;
10540
- }
10541
- async function verify(opts) {
10542
- const skipped = guardVerify(opts.changed, opts.command);
10543
- if (skipped) return { ran: false, passed: false, skipped, output: "", durationMs: 0, timedOut: false };
10544
- return runCommand(opts);
10545
- }
10546
- async function runCommand(opts) {
10547
- const timeoutMs = opts.timeoutMs ?? VERIFY_TIMEOUT_MS;
10548
- const startedAt = Date.now();
10549
- const child = spawn3("sh", ["-c", opts.command], {
10550
- cwd: opts.cwd,
10551
- env: opts.env,
10552
- stdio: ["ignore", "pipe", "pipe"]
10553
- });
10554
- let output = "";
10555
- const collect6 = (chunk) => {
10556
- output += chunk.toString();
10557
- if (output.length > OUTPUT_TAIL * 4) output = output.slice(-OUTPUT_TAIL * 2);
10558
- };
10559
- child.stdout.on("data", collect6);
10560
- child.stderr.on("data", collect6);
10561
- let timedOut = false;
10562
- const timer = setTimeout(() => {
10563
- timedOut = true;
10564
- child.kill("SIGTERM");
10565
- setTimeout(() => child.kill("SIGKILL"), 5e3).unref();
10566
- }, timeoutMs);
10567
- const onAbort = () => child.kill("SIGTERM");
10568
- opts.signal?.addEventListener("abort", onAbort, { once: true });
10569
- const exitCode = await new Promise((resolve6) => {
10570
- child.on("close", (code) => resolve6(code));
10571
- child.on("error", () => resolve6(127));
10572
- });
10573
- clearTimeout(timer);
10574
- opts.signal?.removeEventListener("abort", onAbort);
10575
- return {
10576
- ran: true,
10577
- passed: exitCode === 0 && !timedOut,
10578
- output: tail(output, OUTPUT_TAIL),
10579
- exitCode: exitCode ?? void 0,
10580
- durationMs: Date.now() - startedAt,
10581
- timedOut
10582
- };
10583
- }
10584
- function tail(text2, max) {
10585
- const trimmed = text2.trimEnd();
10586
- if (trimmed.length <= max) return trimmed;
10587
- return `\u2026 ${trimmed.slice(-max)}`;
10588
- }
10589
- function renderVerifyResult(result, command) {
10590
- if (!result.ran) return result.skipped ?? "no verify command is set for this project";
10591
- const secs = (result.durationMs / 1e3).toFixed(0);
10592
- if (result.timedOut) return `the check NanoPM could not run itself, \`${command}\`, did not finish within ${secs}s and was stopped`;
10593
- return result.passed ? `the check NanoPM could not run itself, \`${command}\`, passed in ${secs}s` : `the check NanoPM could not run itself, \`${command}\`, failed (exit ${result.exitCode}) after ${secs}s`;
10594
- }
10595
- function retryPrompt(result, command) {
10596
- return [
10597
- `Your change does not pass this project's own check.`,
10598
- ``,
10599
- `The daemon ran \`${command}\` in the repository root and it failed with exit ${result.exitCode}.`,
10600
- `You did not run this and cannot run it; this output is the only evidence you get.`,
10601
- ``,
10602
- "```",
10603
- result.output || "(the command produced no output)",
10604
- "```",
10605
- ``,
10606
- `Fix what this reports, in the files you already changed where possible. Do not disable`,
10607
- `the check, do not edit what the command runs, and do not change unrelated code to make`,
10608
- `it pass. If the failure is not caused by your change, say so plainly and stop.`
10609
- ].join("\n");
10610
- }
10611
-
10612
10715
  // ../daemon/src/run-job.ts
10613
10716
  var QUICK_RUNS = { reword: runRewordJob, talk: runTalkJob };
10614
10717
  async function runJob(job, ctx) {
@@ -10660,8 +10763,8 @@ async function runJob(job, ctx) {
10660
10763
  return outcome;
10661
10764
  }
10662
10765
  async function execute(job, skill, ctx, events) {
10663
- const runDir = join10(nanopmHome(), "run", job.id);
10664
- await mkdir4(runDir, { recursive: true, mode: 448 });
10766
+ const runDir = join12(nanopmHome(), "run", job.id);
10767
+ await mkdir5(runDir, { recursive: true, mode: 448 });
10665
10768
  const resumeSessionId = job.resume_of ? await sessionOf(ctx.client, job.resume_of) : void 0;
10666
10769
  const sessionId = resumeSessionId ?? randomUUID12();
10667
10770
  await ctx.client.from("jobs").update({ harness_session_id: sessionId }).eq("id", job.id);
@@ -10705,14 +10808,14 @@ async function execute(job, skill, ctx, events) {
10705
10808
  clearTimeout(timer);
10706
10809
  clearInterval(heartbeat2);
10707
10810
  clearInterval(cancelPoll);
10708
- await rm3(runDir, { recursive: true, force: true });
10811
+ await rm4(runDir, { recursive: true, force: true });
10709
10812
  }
10710
10813
  }
10711
10814
  const quickRun2 = QUICK_RUNS[job.skill];
10712
10815
  if (quickRun2) {
10713
10816
  const cwd2 = scratchDir(job.id);
10714
10817
  try {
10715
- await mkdir4(cwd2, { recursive: true, mode: 448 });
10818
+ await mkdir5(cwd2, { recursive: true, mode: 448 });
10716
10819
  const caps = skill.manifest.capabilities;
10717
10820
  return await quickRun2({
10718
10821
  client: ctx.client,
@@ -10732,13 +10835,22 @@ async function execute(job, skill, ctx, events) {
10732
10835
  clearTimeout(timer);
10733
10836
  clearInterval(heartbeat2);
10734
10837
  clearInterval(cancelPoll);
10735
- await rm3(runDir, { recursive: true, force: true });
10736
- await rm3(cwd2, { recursive: true, force: true });
10838
+ await rm4(runDir, { recursive: true, force: true });
10839
+ await rm4(cwd2, { recursive: true, force: true });
10737
10840
  }
10738
10841
  }
10739
- const capabilities = capCapabilities(skill.manifest.capabilities, autonomy2);
10842
+ const capabilities = capCapabilities(skill.manifest.capabilities, autonomy2, { asked: skill.manifest.asked && job.requested_by === ctx.userId });
10740
10843
  const harnessWeb = await claudeSearches(searchProvider);
10741
- const cwd = await resolveCwd(ctx, job, params, scratchDir(job.id), capabilities.includes("write_repo"));
10844
+ let cwd = await resolveCwd(ctx, job, params, scratchDir(job.id), capabilities.includes("write_repo"));
10845
+ const prepared = job.skill === BUILD_SKILL ? await prepareBuild(ctx.client, job, cwd, adapter) : null;
10846
+ if (prepared && !prepared.ok) {
10847
+ clearTimeout(timer);
10848
+ clearInterval(heartbeat2);
10849
+ clearInterval(cancelPoll);
10850
+ return prepared.outcome;
10851
+ }
10852
+ const build2 = prepared?.build;
10853
+ if (build2) cwd = build2.dir;
10742
10854
  const definitionsRead = job.skill === "onboard" && !job.resume_of ? readDefinitions({
10743
10855
  adapter,
10744
10856
  spec: { cwd, runDir, systemPrompt: "", prompt: "", capabilities: [], mcp: [], maxTurns: 1, sessionId, env: harnessEnv(timeoutMs), signal: abort.signal },
@@ -10762,29 +10874,6 @@ async function execute(job, skill, ctx, events) {
10762
10874
  const read6 = site.pages.length ? `read ${site.pages.map((p) => new URL(p.url).pathname).join(", ")} on ${site.host}` : `could not read ${site.host}`;
10763
10875
  await logActivity(ctx, job, "scan.site", site.missed.length ? `${read6} (${site.missed.length} not read)` : read6);
10764
10876
  }
10765
- let item;
10766
- let session2;
10767
- if (capabilities.includes("write_repo")) {
10768
- const solutionId = params.solution_id;
10769
- if (typeof solutionId !== "string" || !solutionId) {
10770
- clearTimeout(timer);
10771
- clearInterval(heartbeat2);
10772
- clearInterval(cancelPoll);
10773
- return { status: "failed", error: { code: "harness_error", message: "this job names no solution to build" } };
10774
- }
10775
- try {
10776
- item = await loadAcceptedSolution(ctx.client, job.project_id, solutionId);
10777
- session2 = await openRepoSession(cwd, item.ref);
10778
- await startSolution(ctx.client, job.project_id, item.ref, job.id);
10779
- params.item = renderSolution(item);
10780
- await logActivity(ctx, job, "repo.branch", `working on ${session2.branch}`);
10781
- } catch (err) {
10782
- clearTimeout(timer);
10783
- clearInterval(heartbeat2);
10784
- clearInterval(cancelPoll);
10785
- return { status: "failed", error: { code: "harness_error", message: errorMessage(err) } };
10786
- }
10787
- }
10788
10877
  const spec2 = {
10789
10878
  cwd,
10790
10879
  runDir,
@@ -10804,20 +10893,21 @@ async function execute(job, skill, ctx, events) {
10804
10893
  ].filter(Boolean).join("\n\n"),
10805
10894
  capabilities,
10806
10895
  harnessWeb,
10896
+ network: build2?.network,
10807
10897
  withoutTools: skill.manifest.withoutTools,
10808
10898
  // The workspace reaches the memory server only for a job that changes files; that is
10809
- // what lets `remove_files` refuse everywhere else.
10899
+ // what lets the build's tools refuse everywhere else.
10810
10900
  mcp: ctx.mcpServers?.(job, timeoutMs, capabilities.includes("write_repo") ? cwd : void 0) ?? [],
10811
10901
  model: skill.manifest.model,
10812
10902
  maxTurns: skill.manifest.maxTurns,
10813
10903
  sessionId,
10814
10904
  resumeSessionId,
10815
- env: harnessEnv(timeoutMs),
10905
+ // Print mode kills a background task when the turn ends; a build waits on its commands (§9).
10906
+ env: build2 ? { ...harnessEnv(timeoutMs), CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" } : harnessEnv(timeoutMs),
10816
10907
  signal: abort.signal
10817
10908
  };
10818
10909
  const verifyCommand2 = project?.preferences?.verify_command?.trim() || null;
10819
10910
  let outcome;
10820
- let verified;
10821
10911
  let webCalls = 0;
10822
10912
  const onHarnessEvent = async (event) => {
10823
10913
  await events.harness(event);
@@ -10835,9 +10925,27 @@ async function execute(job, skill, ctx, events) {
10835
10925
  return result;
10836
10926
  };
10837
10927
  try {
10838
- if (job.skill === "personas") {
10928
+ if (build2) {
10839
10929
  try {
10840
- return await runPersonaJob({ client: ctx.client, job, adapter, spec: spec2, onEvent: onHarnessEvent });
10930
+ return await runBuildJob({
10931
+ client: ctx.client,
10932
+ job,
10933
+ adapter,
10934
+ build: build2,
10935
+ spec: spec2,
10936
+ verifyCommand: verifyCommand2,
10937
+ events,
10938
+ onEvent: onHarnessEvent,
10939
+ log: (step, summary, result) => logActivity(ctx, job, step, summary, result ? { outcome: result } : {})
10940
+ });
10941
+ } catch (error) {
10942
+ if (cancelled) return { status: "cancelled" };
10943
+ return { status: "failed", error: { code: timedOut ? "timeout" : "harness_error", message: errorMessage(error) } };
10944
+ }
10945
+ }
10946
+ if (job.skill === "personas") {
10947
+ try {
10948
+ return await runPersonaJob({ client: ctx.client, job, adapter, spec: spec2, onEvent: onHarnessEvent });
10841
10949
  } catch (error) {
10842
10950
  if (cancelled) return { status: "cancelled" };
10843
10951
  return { status: "failed", error: { code: timedOut ? "timeout" : "harness_error", message: errorMessage(error) } };
@@ -10915,83 +11023,23 @@ async function execute(job, skill, ctx, events) {
10915
11023
  return { status: "failed", error: { code: timedOut ? "timeout" : "harness_error", message: errorMessage(error) } };
10916
11024
  }
10917
11025
  }
10918
- for (let attempt7 = 1; attempt7 <= 2; attempt7 += 1) {
10919
- const attemptSpec = attempt7 === 1 ? spec2 : { ...spec2, prompt: retryPrompt(verified, verifyCommand2), resumeSessionId: sessionId };
10920
- let attemptOutcome;
10921
- webCalls = 0;
10922
- for await (const event of adapter.run(attemptSpec)) {
10923
- await onHarnessEvent(event);
10924
- if (event.type === "finished") {
10925
- attemptOutcome = { status: "succeeded", result: event.result, usage: event.usage };
10926
- } else if (event.type === "failed") {
10927
- attemptOutcome = { status: "failed", error: { code: event.code, message: event.message } };
10928
- }
11026
+ for await (const event of adapter.run(spec2)) {
11027
+ await onHarnessEvent(event);
11028
+ if (event.type === "finished") {
11029
+ outcome = { status: "succeeded", result: event.result, usage: event.usage };
11030
+ } else if (event.type === "failed") {
11031
+ outcome = { status: "failed", error: { code: event.code, message: event.message } };
10929
11032
  }
10930
- if (attemptOutcome) await noteIfItNeverSearched(attemptOutcome, capabilities.includes("read_web"));
10931
- outcome = mergeAttempts(outcome, attemptOutcome);
10932
- if (attemptOutcome?.status !== "succeeded" || !session2 || !verifyCommand2) break;
10933
- verified = await verify({
10934
- cwd,
10935
- command: verifyCommand2,
10936
- env: harnessEnv(timeoutMs),
10937
- changed: await changedFiles(cwd),
10938
- signal: abort.signal
10939
- });
10940
- await logActivity(ctx, job, "repo.verify", renderVerifyResult(verified, verifyCommand2), {
10941
- outcome: verified.passed ? "ok" : "error",
10942
- usage: { duration_ms: verified.durationMs }
10943
- });
10944
- await events.write("text", { text: renderVerifyResult(verified, verifyCommand2) });
10945
- if (verified.passed || !verified.ran || attempt7 === 2) break;
10946
11033
  }
11034
+ if (outcome) await noteIfItNeverSearched(outcome, capabilities.includes("read_web"));
10947
11035
  } finally {
10948
11036
  clearTimeout(timer);
10949
11037
  clearInterval(heartbeat2);
10950
11038
  clearInterval(cancelPoll);
10951
11039
  await definitionsRead;
10952
- await rm3(runDir, { recursive: true, force: true });
10953
- }
10954
- if (outcome?.status === "succeeded" && verified && verified.ran && !verified.passed) {
10955
- outcome = {
10956
- ...outcome,
10957
- status: "failed",
10958
- error: {
10959
- code: "harness_error",
10960
- message: `${renderVerifyResult(verified, verifyCommand2)}. The work is on the branch; the solution stays accepted.`
10961
- }
10962
- };
10963
- }
10964
- if (outcome?.status === "succeeded") {
10965
- if (!session2 || !item) return outcome;
10966
- try {
10967
- const repo = await closeRepoSession(cwd, session2, item);
10968
- await logActivity(ctx, job, "repo.commit", renderRepoResult(repo).split("\n")[0]);
10969
- if (!repo.commit) {
10970
- await unstartSolution(ctx.client, job.project_id, item.ref).catch(() => void 0);
10971
- return {
10972
- ...outcome,
10973
- status: "failed",
10974
- error: { code: "harness_error", message: "the skill finished without changing anything" }
10975
- };
10976
- }
10977
- await finishSolution(ctx.client, job, item, repo);
10978
- return { ...outcome, result: [outcome.result, "", renderRepoResult(repo)].filter((l) => l !== void 0).join("\n") };
10979
- } catch (err) {
10980
- return {
10981
- ...outcome,
10982
- status: "failed",
10983
- error: { code: "harness_error", message: errorMessage(err) }
10984
- };
10985
- }
10986
- }
10987
- if (session2) {
10988
- const kept = await abandonRepoSession(cwd, session2, verified?.ran && !verified.passed ? item : void 0).catch(() => void 0);
10989
- if (item) await unstartSolution(ctx.client, job.project_id, item.ref).catch(() => void 0);
10990
- if (kept?.commit) {
10991
- await logActivity(ctx, job, "repo.commit", renderRepoResult(kept).split("\n")[0], { outcome: "error" });
10992
- if (outcome) outcome = { ...outcome, result: [outcome.result, "", renderRepoResult(kept)].filter(Boolean).join("\n") };
10993
- }
11040
+ await rm4(runDir, { recursive: true, force: true });
10994
11041
  }
11042
+ if (outcome?.status === "succeeded") return outcome;
10995
11043
  if (cancelled) return { status: "cancelled" };
10996
11044
  if (timedOut) {
10997
11045
  return { status: "failed", error: { code: "timeout", message: `exceeded the ${skill.manifest.timeout} cap` } };
@@ -11002,10 +11050,6 @@ async function sessionOf(client, jobId) {
11002
11050
  const { data } = await client.from("jobs").select("harness_session_id").eq("id", jobId).maybeSingle();
11003
11051
  return data?.harness_session_id ?? void 0;
11004
11052
  }
11005
- function mergeAttempts(first, second) {
11006
- if (!first?.usage || !second?.usage) return second ?? first;
11007
- return { ...second, usage: sumUsage(first.usage, second.usage) };
11008
- }
11009
11053
  async function askForTheSite(ctx, job, project, events) {
11010
11054
  const started = Date.now();
11011
11055
  const search = await searchForSite(project.name);
@@ -11031,7 +11075,7 @@ function harnessWebFor(searchProvider, machineCanSearch) {
11031
11075
  }
11032
11076
  async function machineWebSearch() {
11033
11077
  try {
11034
- const parsed = JSON.parse(await readFile6(join10(nanopmHome(), "daemon.json"), "utf8"));
11078
+ const parsed = JSON.parse(await readFile8(join12(nanopmHome(), "daemon.json"), "utf8"));
11035
11079
  return parsed.webSearch === true;
11036
11080
  } catch {
11037
11081
  return false;
@@ -11073,7 +11117,7 @@ function buildHeader(job, project, cwd, harnessWeb = false, memory = false) {
11073
11117
  return lines.join("\n");
11074
11118
  }
11075
11119
  function scratchDir(jobId) {
11076
- return join10(tmpdir(), `nanopm-run-${jobId}`);
11120
+ return join12(tmpdir(), `nanopm-run-${jobId}`);
11077
11121
  }
11078
11122
  async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
11079
11123
  const refuse2 = (why) => {
@@ -11082,12 +11126,12 @@ async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
11082
11126
  const requested = params.repo_path;
11083
11127
  if (typeof requested !== "string" || !requested) {
11084
11128
  if (mustBeTheClone) refuse2("the job named no repository");
11085
- await mkdir4(fallback, { recursive: true, mode: 448 });
11129
+ await mkdir5(fallback, { recursive: true, mode: 448 });
11086
11130
  return fallback;
11087
11131
  }
11088
11132
  if (!job.project_id) {
11089
11133
  if (mustBeTheClone) refuse2("the job belongs to no project");
11090
- await mkdir4(fallback, { recursive: true, mode: 448 });
11134
+ await mkdir5(fallback, { recursive: true, mode: 448 });
11091
11135
  return fallback;
11092
11136
  }
11093
11137
  const { data: clone } = await ctx.client.from("clones").select("repo_path").eq("project_id", job.project_id).eq("daemon_id", ctx.daemonId).maybeSingle();
@@ -11095,12 +11139,12 @@ async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
11095
11139
  if (!allowed || resolve4(allowed) !== resolve4(requested)) {
11096
11140
  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`);
11097
11141
  console.error(`job ${job.id}: repo_path ${requested} is not this machine's clone; running in a scratch directory instead`);
11098
- await mkdir4(fallback, { recursive: true, mode: 448 });
11142
+ await mkdir5(fallback, { recursive: true, mode: 448 });
11099
11143
  return fallback;
11100
11144
  }
11101
11145
  if (existsSync4(allowed)) return resolve4(allowed);
11102
11146
  if (mustBeTheClone) refuse2(`${allowed} is not on this machine any more`);
11103
- await mkdir4(fallback, { recursive: true, mode: 448 });
11147
+ await mkdir5(fallback, { recursive: true, mode: 448 });
11104
11148
  return fallback;
11105
11149
  }
11106
11150
  function asData8(label, body) {
@@ -11109,10 +11153,10 @@ ${body}
11109
11153
  </${label}>`;
11110
11154
  }
11111
11155
  function harnessEnv(timeoutMs, source = process.env) {
11112
- const home = source.HOME ?? homedir3();
11156
+ const home = source.HOME ?? homedir4();
11113
11157
  const env = {
11114
11158
  HOME: home,
11115
- PATH: [source.PATH ?? "/usr/local/bin:/usr/bin:/bin", join10(home, ".local", "bin")].join(":"),
11159
+ PATH: [source.PATH ?? "/usr/local/bin:/usr/bin:/bin", join12(home, ".local", "bin")].join(":"),
11116
11160
  MCP_TOOL_TIMEOUT: String(timeoutMs)
11117
11161
  };
11118
11162
  for (const key of ["USER", "LANG", "TMPDIR", "SHELL", "ANTHROPIC_API_KEY", "CLAUDE_CONFIG_DIR"]) {
@@ -11289,12 +11333,12 @@ async function chainAfter(auth, job, outcome, log) {
11289
11333
  const parent = job.parent_job_id ? await parentOf(auth, job.parent_job_id) : null;
11290
11334
  const own = job.params ?? {};
11291
11335
  const deep = own.depth === "deep" && job.trigger === "hook";
11292
- const next2 = daemonQueues(job.skill, 0, parent?.skill ?? null, deep);
11293
- if (!next2.length) return;
11336
+ const next = daemonQueues(job.skill, 0, parent?.skill ?? null, deep);
11337
+ if (!next.length) return;
11294
11338
  if (await chainDepth(auth, job) >= CHAIN_DEPTH_MAX) return;
11295
11339
  const from = deep ? own.repo_path : parent?.params?.repo_path;
11296
11340
  const repo = typeof from === "string" ? { repo_path: from } : null;
11297
- for (const skill of next2) {
11341
+ for (const skill of next) {
11298
11342
  const params = { ...repo ?? {}, ...deep ? { depth: "deep", background: true } : {} };
11299
11343
  const { data, error } = await auth.client.from("jobs").insert({
11300
11344
  project_id: job.project_id,
@@ -11377,7 +11421,7 @@ var heldInThisProcess = false;
11377
11421
  function acquireLock() {
11378
11422
  if (heldInThisProcess) throw new Error("a nanopm daemon is already running in this process");
11379
11423
  mkdirSync(nanopmHome(), { recursive: true, mode: 448 });
11380
- const path = join11(nanopmHome(), "daemon.pid");
11424
+ const path = join13(nanopmHome(), "daemon.pid");
11381
11425
  for (; ; ) {
11382
11426
  try {
11383
11427
  const fd = openSync(path, "wx");
@@ -11464,13 +11508,13 @@ async function reclaimOwnJobs(client, daemonId) {
11464
11508
  async function harnessPids(runDir) {
11465
11509
  const dirs = [runDir];
11466
11510
  try {
11467
- for (const d of await readdir2(runDir, { withFileTypes: true })) if (d.isDirectory()) dirs.push(join11(runDir, d.name));
11511
+ for (const d of await readdir3(runDir, { withFileTypes: true })) if (d.isDirectory()) dirs.push(join13(runDir, d.name));
11468
11512
  } catch {
11469
11513
  }
11470
11514
  const pids = [];
11471
11515
  for (const dir of dirs) {
11472
11516
  try {
11473
- const pid = Number((await readFile7(join11(dir, "harness.pid"), "utf8")).trim());
11517
+ const pid = Number((await readFile9(join13(dir, "harness.pid"), "utf8")).trim());
11474
11518
  if (pid > 1) pids.push(pid);
11475
11519
  } catch {
11476
11520
  }
@@ -11478,20 +11522,20 @@ async function harnessPids(runDir) {
11478
11522
  return pids;
11479
11523
  }
11480
11524
  async function killOrphanedHarness(jobId) {
11481
- const runDir = join11(nanopmHome(), "run", jobId);
11525
+ const runDir = join13(nanopmHome(), "run", jobId);
11482
11526
  for (const pid of await harnessPids(runDir)) {
11483
11527
  try {
11484
11528
  process.kill(pid, "SIGTERM");
11485
11529
  } catch {
11486
11530
  }
11487
11531
  }
11488
- await rm4(runDir, { recursive: true, force: true });
11532
+ await rm5(runDir, { recursive: true, force: true });
11489
11533
  }
11490
11534
  async function registerDaemon(auth, version, opts = {}) {
11491
- const path = join11(nanopmHome(), "daemon.json");
11535
+ const path = join13(nanopmHome(), "daemon.json");
11492
11536
  let identity = null;
11493
11537
  try {
11494
- identity = JSON.parse(await readFile7(path, "utf8"));
11538
+ identity = JSON.parse(await readFile9(path, "utf8"));
11495
11539
  } catch {
11496
11540
  identity = null;
11497
11541
  }
@@ -11529,8 +11573,8 @@ async function registerDaemon(auth, version, opts = {}) {
11529
11573
  return data.id;
11530
11574
  }
11531
11575
  async function rememberDaemon(id, shared = false) {
11532
- await mkdir5(nanopmHome(), { recursive: true, mode: 448 });
11533
- await writeFile4(join11(nanopmHome(), "daemon.json"), JSON.stringify({ id, shared }, null, 2) + "\n", { mode: 384 });
11576
+ await mkdir6(nanopmHome(), { recursive: true, mode: 448 });
11577
+ await writeFile6(join13(nanopmHome(), "daemon.json"), JSON.stringify({ id, shared }, null, 2) + "\n", { mode: 384 });
11534
11578
  }
11535
11579
  async function heartbeat(client, daemonId) {
11536
11580
  await client.from("daemons").update({ status: "online", last_seen_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", daemonId);
@@ -11881,48 +11925,6 @@ function refuseFetch(url, allowlist, open = false) {
11881
11925
  return `${host} is not on this project's allowlist, so it was not read. The founder can allow it: if it is a competitor, \`nanopm market confirm <ref>\` on the entry that names them, or \`nanopm market add <name> ${host}\`. Until then, search results and the sites already allowed are what there is.`;
11882
11926
  }
11883
11927
 
11884
- // ../memory-mcp/src/repo-rules.ts
11885
- import { lstat, unlink } from "fs/promises";
11886
- import { relative as relative3, resolve as resolve5, sep as sep2 } from "path";
11887
- var REMOVE_MAX = 50;
11888
- function refuseRemove(workspace, requested) {
11889
- const root2 = resolve5(workspace);
11890
- const target = resolve5(root2, requested);
11891
- const rel = relative3(root2, target);
11892
- if (!rel || rel.startsWith("..") || rel.startsWith(`..${sep2}`)) return "outside the workspace";
11893
- if (rel === ".git" || rel.startsWith(`.git${sep2}`)) return "inside .git";
11894
- if (looksLikeASecret(rel)) return "looks like a secret, which the agent may neither read nor remove";
11895
- return null;
11896
- }
11897
- async function removeFiles(workspace, paths) {
11898
- const out = [];
11899
- for (const requested of paths.slice(0, REMOVE_MAX)) {
11900
- const refusal = refuseRemove(workspace, requested);
11901
- if (refusal) {
11902
- out.push({ path: requested, removed: false, reason: refusal });
11903
- continue;
11904
- }
11905
- const target = resolve5(workspace, requested);
11906
- try {
11907
- const stat2 = await lstat(target);
11908
- if (stat2.isSymbolicLink()) {
11909
- out.push({ path: requested, removed: false, reason: "a symlink; what it points at is not touched" });
11910
- continue;
11911
- }
11912
- if (stat2.isDirectory()) {
11913
- out.push({ path: requested, removed: false, reason: "a directory; name the files in it" });
11914
- continue;
11915
- }
11916
- await unlink(target);
11917
- out.push({ path: requested, removed: true });
11918
- } catch (err) {
11919
- const code = err.code;
11920
- out.push({ path: requested, removed: false, reason: code === "ENOENT" ? "not there" : err instanceof Error ? err.message : String(err) });
11921
- }
11922
- }
11923
- return out;
11924
- }
11925
-
11926
11928
  // ../memory-mcp/src/problem-rules.ts
11927
11929
  import { z as z20 } from "zod";
11928
11930
  var STATEMENT_MAX = 300;
@@ -12297,7 +12299,7 @@ ${renderMap()}`,
12297
12299
  }
12298
12300
  },
12299
12301
  async ({ ref, outcome, note }) => {
12300
- return json(await finishSolution2(ctx, ref, outcome, note));
12302
+ return json(await finishSolution(ctx, ref, outcome, note));
12301
12303
  }
12302
12304
  );
12303
12305
  server.registerTool(
@@ -12397,20 +12399,20 @@ ${renderMap()}`,
12397
12399
  }
12398
12400
  );
12399
12401
  server.registerTool(
12400
- "remove_files",
12402
+ "build_plan",
12401
12403
  {
12402
- description: "Delete files in the repository you are working in. Paths relative to the repository root, at most 50. Refused: anything outside the repository, inside .git, a symlink, a directory, or a file that looks like a secret. A file you mean to delete should be deleted with this, not emptied or replaced with a note \u2014 a stub is not a deletion.",
12403
- inputSchema: { paths: z21.array(z21.string().min(1).max(500)).min(1).max(REMOVE_MAX) }
12404
+ description: `Say the plan before you change anything: ${BUILD_STEPS_MIN} to ${BUILD_STEPS_MAX} steps, in order, each one change the founder can recognise, in their words (at most ${BUILD_STEP_MAX} characters). The last step is your own check of the whole change. Call it once; the founder sees it as a checklist.`,
12405
+ inputSchema: { steps: z21.array(z21.string().min(3).max(BUILD_STEP_MAX)).min(BUILD_STEPS_MIN).max(BUILD_STEPS_MAX) }
12404
12406
  },
12405
- async ({ paths }) => {
12406
- if (!ctx.workspace) return failure("this job has no repository to remove files from");
12407
- const outcomes = await removeFiles(ctx.workspace, paths);
12408
- const removed = outcomes.filter((o) => o.removed).map((o) => o.path);
12409
- await logActivity2(ctx, "repo.remove", `removed ${removed.length} of ${paths.length} files${removed.length ? `: ${trim2(removed.join(", "), 200)}` : ""}`, {
12410
- outcome: removed.length === paths.length ? "ok" : "error"
12411
- });
12412
- return json({ outcomes, note: removed.length ? "Removed. The commit at the end of this job will record the deletions." : "Nothing was removed." });
12413
- }
12407
+ async ({ steps }) => ctx.workspace ? json({ recorded: steps.length, note: "Recorded. Build step 1 now; call build_step when it is done." }) : failure("this job builds nothing")
12408
+ );
12409
+ server.registerTool(
12410
+ "build_step",
12411
+ {
12412
+ description: "A step of your plan is done and its checks pass: NanoPM commits the worktree now, with the step as the message, and ticks it for the founder. Call it once per step, in order, after the step works. Do not run git commit yourself.",
12413
+ inputSchema: { step: z21.number().int().min(1).max(BUILD_STEPS_MAX), summary: z21.string().min(3).max(300) }
12414
+ },
12415
+ async ({ step }) => ctx.workspace ? json({ step, note: "Committed by NanoPM. Go on with the next step, or stop when the plan is done." }) : failure("this job builds nothing")
12414
12416
  );
12415
12417
  server.registerTool(
12416
12418
  "web_search",
@@ -13197,7 +13199,7 @@ async function markFast(ctx, problemRef) {
13197
13199
  await logActivity2(ctx, "problems.fast", `${problem.ref} is one small change`);
13198
13200
  return { problem: problem.ref, fast: true };
13199
13201
  }
13200
- async function finishSolution2(ctx, ref, outcome, note) {
13202
+ async function finishSolution(ctx, ref, outcome, note) {
13201
13203
  if (ctx.actor === "mapping-review") throw new Error("A targeted collection cannot decide solution outcomes.");
13202
13204
  const upper = ref.toUpperCase();
13203
13205
  const { data: current } = await ctx.client.from("solutions").select("status").eq("project_id", ctx.projectId).eq("ref", upper).is("superseded_by", null).maybeSingle();
@@ -13495,7 +13497,8 @@ var MEMORY_TOOL_NAMES = [
13495
13497
  "play_store_search",
13496
13498
  "list_market",
13497
13499
  "write_market",
13498
- "remove_files",
13500
+ "build_plan",
13501
+ "build_step",
13499
13502
  // Talk to Nano (docs/✅nanopm-68-talk-to-nano.md): the map read, and changed as the founder asked.
13500
13503
  ...TALK_TOOLS
13501
13504
  ];
@@ -13506,7 +13509,7 @@ async function startMemoryServer(env = process.env) {
13506
13509
  }
13507
13510
 
13508
13511
  // src/commands/add.ts
13509
- import { readFile as readFile9 } from "fs/promises";
13512
+ import { readFile as readFile11 } from "fs/promises";
13510
13513
 
13511
13514
  // src/project.ts
13512
13515
  import { execFile as execFile7 } from "child_process";
@@ -13514,11 +13517,11 @@ import { basename } from "path";
13514
13517
  import { promisify as promisify7 } from "util";
13515
13518
 
13516
13519
  // src/local.ts
13517
- import { readFile as readFile8 } from "fs/promises";
13518
- import { join as join12 } from "path";
13520
+ import { readFile as readFile10 } from "fs/promises";
13521
+ import { join as join14 } from "path";
13519
13522
  async function localDaemonId() {
13520
13523
  try {
13521
- const parsed = JSON.parse(await readFile8(join12(nanopmHome(), "daemon.json"), "utf8"));
13524
+ const parsed = JSON.parse(await readFile10(join14(nanopmHome(), "daemon.json"), "utf8"));
13522
13525
  return parsed.id ?? null;
13523
13526
  } catch {
13524
13527
  return null;
@@ -13653,7 +13656,7 @@ async function add(file, opts) {
13653
13656
  text2 = await readStdin();
13654
13657
  } else {
13655
13658
  try {
13656
- text2 = await readFile9(file, "utf8");
13659
+ text2 = await readFile11(file, "utf8");
13657
13660
  } catch {
13658
13661
  throw new Error(`could not read ${file}`);
13659
13662
  }
@@ -13668,7 +13671,7 @@ async function add(file, opts) {
13668
13671
  }
13669
13672
 
13670
13673
  // src/commands/run.ts
13671
- import { readFile as readFile10 } from "fs/promises";
13674
+ import { readFile as readFile12 } from "fs/promises";
13672
13675
  import { createInterface as createInterface2 } from "readline/promises";
13673
13676
  import { setTimeout as sleep3 } from "timers/promises";
13674
13677
 
@@ -13835,7 +13838,7 @@ function phaseOf(tool) {
13835
13838
  if (name === "query") return "data";
13836
13839
  if (name === "web_search" || name === "web_fetch") return "web";
13837
13840
  if (REVIEW_WRITES.has(name)) return "review";
13838
- if (name === "remove_files") return "build";
13841
+ if (name === "build_plan" || name === "build_step") return "build";
13839
13842
  return null;
13840
13843
  }
13841
13844
  function touched(tool, input) {
@@ -14000,7 +14003,7 @@ function questionBlock(p) {
14000
14003
 
14001
14004
  // src/commands/run.ts
14002
14005
  async function answersFromFile(path) {
14003
- const raw = JSON.parse(await readFile10(path, "utf8"));
14006
+ const raw = JSON.parse(await readFile12(path, "utf8"));
14004
14007
  const entries = raw.map((a) => typeof a === "string" ? { answer: a } : a);
14005
14008
  return scriptedAnswerer(entries);
14006
14009
  }
@@ -14223,15 +14226,15 @@ async function askOne(auth, projectId, question, daemonId, opts, out) {
14223
14226
  // src/commands/daemon-service.ts
14224
14227
  import { execFile as execFile8 } from "child_process";
14225
14228
  import { existsSync as existsSync5 } from "fs";
14226
- import { mkdir as mkdir6, readFile as readFile11, rm as rm5, writeFile as writeFile5 } from "fs/promises";
14227
- import { homedir as homedir4 } from "os";
14228
- import { join as join13 } from "path";
14229
+ import { mkdir as mkdir7, readFile as readFile13, rm as rm6, writeFile as writeFile7 } from "fs/promises";
14230
+ import { homedir as homedir5 } from "os";
14231
+ import { join as join15 } from "path";
14229
14232
  import { promisify as promisify8 } from "util";
14230
14233
 
14231
14234
  // package.json
14232
14235
  var package_default = {
14233
14236
  name: "@nanopm/cli",
14234
- version: "0.1.8",
14237
+ version: "0.1.11",
14235
14238
  description: "An autonomous product manager for builders who run several small products. Runs on your machine, drives your own coding harness.",
14236
14239
  type: "module",
14237
14240
  license: "MIT",
@@ -14283,10 +14286,10 @@ var VERSION = package_default.version;
14283
14286
  var execFileAsync8 = promisify8(execFile8);
14284
14287
  var LAUNCHD_LABEL = "pm.nano.daemon";
14285
14288
  function plistPath() {
14286
- return join13(homedir4(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
14289
+ return join15(homedir5(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
14287
14290
  }
14288
14291
  function daemonLogPath() {
14289
- return join13(nanopmHome(), "daemon.log");
14292
+ return join15(nanopmHome(), "daemon.log");
14290
14293
  }
14291
14294
  function renderPlist(opts) {
14292
14295
  const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -14345,13 +14348,13 @@ async function installDaemon(out = (l) => console.log(l), opts = {}) {
14345
14348
  out(web.ok ? "Claude's own search answers; searches go through your subscription." : `Claude's search is not available on this machine (${web.message}); searches use DuckDuckGo.`);
14346
14349
  const bin = process.argv[1];
14347
14350
  if (!bin) throw new Error("cannot locate the nanopm binary");
14348
- const home = homedir4();
14349
- const path = [process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", join13(home, ".local", "bin")].join(":");
14350
- await mkdir6(join13(home, "Library", "LaunchAgents"), { recursive: true });
14351
- await mkdir6(nanopmHome(), { recursive: true, mode: 448 });
14351
+ 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 });
14354
+ await mkdir7(nanopmHome(), { recursive: true, mode: 448 });
14352
14355
  if (opts.shared !== void 0) await rememberShared(opts.shared);
14353
14356
  await found({ ...detection, authenticated: true });
14354
- await writeFile5(plistPath(), renderPlist({ node: process.execPath, bin, home, path, logPath: daemonLogPath(), nanopmHome: nanopmHome() }), { mode: 420 });
14357
+ await writeFile7(plistPath(), renderPlist({ node: process.execPath, bin, home, path, logPath: daemonLogPath(), nanopmHome: nanopmHome() }), { mode: 420 });
14355
14358
  const target = guiTarget();
14356
14359
  await execFileAsync8("launchctl", ["bootout", target, plistPath()]).catch(() => void 0);
14357
14360
  await execFileAsync8("launchctl", ["bootstrap", target, plistPath()]);
@@ -14372,7 +14375,7 @@ async function uninstallDaemon(out = (l) => console.log(l)) {
14372
14375
  requireMac();
14373
14376
  const target = guiTarget();
14374
14377
  await execFileAsync8("launchctl", ["bootout", target, plistPath()]).catch(() => void 0);
14375
- await rm5(plistPath(), { force: true });
14378
+ await rm6(plistPath(), { force: true });
14376
14379
  out(`Removed ${LAUNCHD_LABEL}.`);
14377
14380
  }
14378
14381
  async function daemonStatus(out = (l) => console.log(l)) {
@@ -14399,7 +14402,7 @@ async function daemonStatus(out = (l) => console.log(l)) {
14399
14402
  }
14400
14403
  async function daemonLogs(lines = 50) {
14401
14404
  try {
14402
- const text2 = await readFile11(daemonLogPath(), "utf8");
14405
+ const text2 = await readFile13(daemonLogPath(), "utf8");
14403
14406
  return text2.trimEnd().split("\n").slice(-lines).join("\n");
14404
14407
  } catch {
14405
14408
  return `no log at ${daemonLogPath()} yet`;
@@ -14407,41 +14410,41 @@ async function daemonLogs(lines = 50) {
14407
14410
  }
14408
14411
  async function localWebSearch() {
14409
14412
  try {
14410
- const parsed = JSON.parse(await readFile11(join13(nanopmHome(), "daemon.json"), "utf8"));
14413
+ const parsed = JSON.parse(await readFile13(join15(nanopmHome(), "daemon.json"), "utf8"));
14411
14414
  return parsed.webSearch === true;
14412
14415
  } catch {
14413
14416
  return false;
14414
14417
  }
14415
14418
  }
14416
14419
  async function rememberWebSearch(webSearch2) {
14417
- const path = join13(nanopmHome(), "daemon.json");
14420
+ const path = join15(nanopmHome(), "daemon.json");
14418
14421
  let parsed = {};
14419
14422
  try {
14420
- parsed = JSON.parse(await readFile11(path, "utf8"));
14423
+ parsed = JSON.parse(await readFile13(path, "utf8"));
14421
14424
  } catch {
14422
14425
  parsed = {};
14423
14426
  }
14424
- await mkdir6(nanopmHome(), { recursive: true, mode: 448 });
14425
- await writeFile5(path, JSON.stringify({ ...parsed, webSearch: webSearch2 }, null, 2) + "\n", { mode: 384 });
14427
+ await mkdir7(nanopmHome(), { recursive: true, mode: 448 });
14428
+ await writeFile7(path, JSON.stringify({ ...parsed, webSearch: webSearch2 }, null, 2) + "\n", { mode: 384 });
14426
14429
  }
14427
14430
  async function localShared() {
14428
14431
  try {
14429
- const parsed = JSON.parse(await readFile11(join13(nanopmHome(), "daemon.json"), "utf8"));
14432
+ const parsed = JSON.parse(await readFile13(join15(nanopmHome(), "daemon.json"), "utf8"));
14430
14433
  return Boolean(parsed.shared);
14431
14434
  } catch {
14432
14435
  return false;
14433
14436
  }
14434
14437
  }
14435
14438
  async function rememberShared(shared) {
14436
- const path = join13(nanopmHome(), "daemon.json");
14439
+ const path = join15(nanopmHome(), "daemon.json");
14437
14440
  let parsed = {};
14438
14441
  try {
14439
- parsed = JSON.parse(await readFile11(path, "utf8"));
14442
+ parsed = JSON.parse(await readFile13(path, "utf8"));
14440
14443
  } catch {
14441
14444
  parsed = {};
14442
14445
  }
14443
- await mkdir6(nanopmHome(), { recursive: true, mode: 448 });
14444
- await writeFile5(path, JSON.stringify({ ...parsed, shared }, null, 2) + "\n", { mode: 384 });
14446
+ await mkdir7(nanopmHome(), { recursive: true, mode: 448 });
14447
+ await writeFile7(path, JSON.stringify({ ...parsed, shared }, null, 2) + "\n", { mode: 384 });
14445
14448
  }
14446
14449
 
14447
14450
  // src/format/log.ts
@@ -14684,497 +14687,141 @@ The install line does the same: curl -fsSL https://nanopm.app/install | sh`);
14684
14687
  out(await restartDaemon() ? `Updated ${VERSION} \u2192 ${latest}; the daemon restarted on it.` : `Updated ${VERSION} \u2192 ${latest}.`);
14685
14688
  }
14686
14689
 
14687
- // src/format/roadmap.ts
14688
- var SHAPE = {
14689
- proposed: { mark: "\u25CB", colour: ANSI.amber, label: "proposed" },
14690
- accepted: { mark: "\u25D0", colour: ANSI.green, label: "accepted" },
14691
- building: { mark: "\u25D4", colour: ANSI.blue, label: "building" },
14692
- rejected: { mark: "\u2298", colour: ANSI.red, label: "rejected" },
14693
- done: { mark: "\u25CF", colour: ANSI.green, label: "done" },
14694
- dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
14695
- };
14696
- var WORK_KINDS = /* @__PURE__ */ new Set(["branch", "git", "url", "job"]);
14697
- function renderRoadmapItem(item, opts, problem, builtBy) {
14698
- const paint = painter(opts.color);
14699
- const shape2 = SHAPE[item.status] ?? { mark: "\xB7", colour: "", label: item.status };
14700
- const out = [`${paint(shape2.colour, shape2.mark)} ${paint(ANSI.bold, item.ref)} ${item.title}`];
14701
- if (problem) out.push(paint(ANSI.dim, ` answers ${problem.ref} ${clip(problem.statement, Math.max(30, columns() - 20))}`));
14702
- out.push("");
14703
- for (const line of wrap(item.reasoning, Math.max(40, columns() - 4))) out.push(` ${line}`);
14704
- out.push("");
14705
- const facts = [
14706
- ["status", shape2.label + (item.outcome ? ` \xB7 ${item.outcome}${item.outcome_note ? `: ${item.outcome_note}` : ""}` : "")],
14707
- ["rank", String(item.rank)],
14708
- ["who", item.actor === "founder" ? "you" : "NanoPM"],
14709
- ["bets on", item.assumption ?? "\u2014"],
14710
- ["we'll know", item.eval ?? "\u2014"]
14711
- ];
14712
- for (const [k, v] of facts) out.push(paint(ANSI.dim, ` ${k.padEnd(9)} ${v}`));
14713
- const sources2 = item.sources ?? [];
14714
- const restsOn = sources2.filter((s) => !WORK_KINDS.has(s.kind));
14715
- if (restsOn.length) {
14716
- out.push(paint(ANSI.dim, ` ${"rests on".padEnd(9)} ${restsOn.map((s) => `${s.kind}:${s.ref}`).join(", ")}`));
14717
- }
14718
- const where = renderWhere(sources2);
14719
- if (where) out.push(paint(ANSI.dim, ` ${"where".padEnd(9)} ${where}${builtBy ? ` \xB7 built by ${builtBy}` : ""}`));
14720
- return out.join("\n");
14721
- }
14722
- function renderWhere(sources2) {
14723
- const branch = sources2.find((s) => s.kind === "branch")?.ref;
14724
- const commit = sources2.find((s) => s.kind === "git")?.ref;
14725
- const url = sources2.find((s) => s.kind === "url")?.ref;
14726
- return [branch, commit, url].filter(Boolean).join(" \xB7 ");
14690
+ // src/commands/autonomy.ts
14691
+ async function readAutonomy(client, projectId) {
14692
+ const { data } = await client.from("project_preferences").select("autonomy").eq("project_id", projectId).maybeSingle();
14693
+ return data?.autonomy ?? "propose";
14727
14694
  }
14728
- var VERB = {
14729
- want: { text: "wanted", colour: ANSI.amber },
14730
- confirm: { text: "confirmed", colour: ANSI.green },
14731
- drop: { text: "dropped", colour: ANSI.red },
14732
- merge: { text: "merged into", colour: ANSI.blue },
14733
- separate: { text: "kept apart", colour: ANSI.blue },
14734
- weight: { text: "weighed", colour: ANSI.blue },
14735
- area: { text: "filed", colour: ANSI.blue },
14736
- accept: { text: "accepted", colour: ANSI.green },
14737
- reject: { text: "rejected", colour: ANSI.red },
14738
- first: { text: "put first", colour: ANSI.blue },
14739
- done: { text: "did", colour: ANSI.green },
14740
- dismiss: { text: "dismissed", colour: ANSI.red }
14695
+ var EXPLAINED = {
14696
+ propose: "NanoPM reads and proposes. It never changes a file.",
14697
+ execute_after_validation: "NanoPM changes files, on its own branch, after showing you what it will do and waiting for a yes.",
14698
+ execute_then_report: "NanoPM acts first and tells you after. Not available yet."
14741
14699
  };
14742
- function renderDecisions(rows2, opts) {
14743
- const paint = painter(opts.color);
14744
- if (!rows2.length) return "No decisions yet.";
14745
- const out = [];
14746
- for (const row of rows2) {
14747
- const when = row.created_at.slice(0, 10);
14748
- const verb = VERB[row.kind] ?? { text: row.kind, colour: ANSI.blue };
14749
- const about = row.solutions ? `${paint(ANSI.dim, row.solutions.ref)} ${row.solutions.title}` : row.problems ? paint(ANSI.dim, row.problems.ref) : row.market_entries ? `${paint(ANSI.dim, row.market_entries.ref)} ${row.market_entries.competitor ?? ""}`.trimEnd() : "";
14750
- const by = row.by ? paint(ANSI.dim, ` \xB7 by ${row.by}`) : "";
14751
- out.push(`${paint(ANSI.dim, when)} ${paint(verb.colour, verb.text)} ${about}${by}`.trimEnd());
14752
- if (row.body) {
14753
- for (const line of wrap(row.body, 72)) out.push(paint(ANSI.dim, ` ${line}`));
14700
+ async function autonomy(level, opts) {
14701
+ const auth = await authenticate();
14702
+ const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
14703
+ const current = await readAutonomy(auth.client, project.id);
14704
+ if (!level) {
14705
+ const lines = [`${project.name} is on "${current}".`, ""];
14706
+ for (const l of AUTONOMY_LEVELS) {
14707
+ lines.push(` ${l === current ? "\u25CF" : "\u25CB"} ${l.padEnd(26)} ${EXPLAINED[l]}`);
14754
14708
  }
14709
+ lines.push("", " nanopm autonomy <level>");
14710
+ return lines.join("\n");
14755
14711
  }
14756
- return out.join("\n");
14757
- }
14758
- var HUE = ["\x1B[34m", "\x1B[35m", "\x1B[36m", "\x1B[32m", "\x1B[95m"];
14759
- function themesOf(tree) {
14760
- return new Map(tree.map((b) => [b.id, { name: themeName(b), colour: HUE[themeHue(tree, b) - 1] }]));
14761
- }
14762
- function sizeWord(size, paint) {
14763
- return size === "big" ? `${paint(ANSI.bold, "\u25AA")} ${SIZE_WORD.big}` : size === "small" ? paint(ANSI.dim, SIZE_WORD.small) : "";
14712
+ const wanted = level.trim();
14713
+ if (!AUTONOMY_LEVELS.includes(wanted)) {
14714
+ throw new Error(`"${level}" is not a level. Pick one of: ${AUTONOMY_LEVELS.join(", ")}.`);
14715
+ }
14716
+ if (wanted === "execute_then_report") {
14717
+ throw new Error(
14718
+ "execute_then_report is not available yet. NanoPM asks before it changes anything until the review loop has been used in anger."
14719
+ );
14720
+ }
14721
+ if (wanted === current) return `${project.name} is already on "${current}".`;
14722
+ const { error } = await auth.client.from("project_preferences").update({ autonomy: wanted }).eq("project_id", project.id);
14723
+ if (error) {
14724
+ throw new Error(`could not change the dial: ${error.message}`);
14725
+ }
14726
+ return [`${project.name} is now on "${wanted}".`, EXPLAINED[wanted]].join("\n");
14764
14727
  }
14765
- function renderSolutionsBoard(board, opts) {
14766
- const paint = painter(opts.color);
14767
- const width = Math.max(40, columns());
14768
- const themes = themesOf(board.tree);
14769
- const out = [`${paint(ANSI.bold, "Roadmap")}${board.objective ? ` ${paint(ANSI.dim, `${board.objective.lever} \u2014 ${board.objective.sentence}`)}` : ""}`];
14770
- const chips = board.tree.flatMap((b) => {
14771
- const n = board.cards.filter((c) => c.branch.id === b.id).length;
14772
- const t = themes.get(b.id);
14773
- return n ? [`${paint(t.colour, "\u25CF")} ${t.name} ${paint(ANSI.dim, String(n))}`] : [];
14774
- });
14775
- const legend = chips.reduce((lines, chip) => {
14776
- const last = lines.at(-1);
14777
- if (last !== void 0 && visibleLength(last) + 3 + visibleLength(chip) <= width) lines[lines.length - 1] = `${last} ${chip}`;
14778
- else lines.push(chip);
14779
- return lines;
14780
- }, []);
14781
- out.push("", ...legend);
14782
- if (!board.cards.some((c) => c.pick.placed_by === "founder")) out.push(paint(ANSI.dim, "Nano placed these in the order of your opportunity map. Move them where you want them."));
14783
- const refWidth = Math.max(5, ...board.cards.map((c) => c.pick.ref.length));
14784
- const gutter = " ".repeat(refWidth + 4);
14785
- const titleWidth = Math.min(Math.max(...board.cards.map((c) => c.pick.title.length)), width - gutter.length - 8);
14786
- for (const h of HORIZONS) {
14787
- const column = board.cards.filter((c) => c.horizon === h);
14788
- out.push("", `${paint(ANSI.bold, HORIZON_WORD[h])} ${paint(ANSI.dim, String(column.length))}`);
14789
- if (!column.length) out.push(paint(ANSI.dim, ` Move a card here: nanopm roadmap move <ref> ${h}`));
14790
- for (const c of column) {
14791
- const t = themes.get(c.branch.id);
14792
- out.push(` ${paint(ANSI.dim, c.pick.ref.padEnd(refWidth))} ${paint(t.colour, "\u25CF")} ${paint(ANSI.dim, t.name)}`);
14793
- out.push(`${gutter}${clip(c.pick.title, titleWidth).padEnd(titleWidth)} ${sizeWord(c.pick.size, paint)}`.trimEnd());
14794
- const depth = board.working.get(c.leaf.id);
14795
- if (depth) out.push(`${gutter}${paint(ANSI.blue, "\u25CF")} ${paint(ANSI.dim, WORK_WORD[depth])}`);
14728
+ async function verifyCommand(command, opts) {
14729
+ const auth = await authenticate();
14730
+ const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
14731
+ const { data } = await auth.client.from("project_preferences").select("verify_command").eq("project_id", project.id).maybeSingle();
14732
+ const current = data?.verify_command ?? null;
14733
+ if (!command && !opts.clear) {
14734
+ if (!current) {
14735
+ return [
14736
+ `${project.name} has no check, so nothing verifies what NanoPM writes.`,
14737
+ "",
14738
+ ' nanopm verify "npm run lint && npm test"',
14739
+ "",
14740
+ "A build runs it in its sandboxed copy of the repository before its last step,",
14741
+ "fixes what it reports, and says the result in the pull request."
14742
+ ].join("\n");
14796
14743
  }
14744
+ return [`${project.name} verifies with:`, "", ` ${current}`, "", ' nanopm verify "<command>" \xB7 nanopm verify --clear'].join("\n");
14797
14745
  }
14798
- if (board.earlier) out.push("", paint(ANSI.bold, "On the problem list"), "", board.earlier);
14799
- out.push(
14800
- "",
14801
- paint(ANSI.dim, 'nanopm roadmap <ref> \xB7 nanopm roadmap move <ref> now|next|later [position] \xB7 nanopm reject <ref> ["why"]'),
14802
- paint(ANSI.dim, "nanopm opportunities brainstorm <ref> [--deep]: solutions to another problem, or deeper ones")
14803
- );
14804
- return out.join("\n");
14746
+ const next = opts.clear ? null : command.trim();
14747
+ if (next !== null && !next) throw new Error('a check needs a command: nanopm verify "npm test"');
14748
+ const { error } = await auth.client.from("project_preferences").update({ verify_command: next }).eq("project_id", project.id);
14749
+ if (error) throw new Error(`could not save the check: ${error.message}`);
14750
+ if (next === null) return `${project.name} no longer runs a check. Nothing will verify what NanoPM writes.`;
14751
+ return [`${project.name} will verify with:`, "", ` ${next}`, "", "A build runs it in its sandboxed copy of the repository before its last step."].join("\n");
14805
14752
  }
14806
- function renderSolutionSheet(s, at, opts) {
14753
+
14754
+ // src/format/opportunities.ts
14755
+ function renderOpportunities(rows2, lens, opts) {
14807
14756
  const paint = painter(opts.color);
14808
14757
  const width = Math.max(40, columns());
14809
- const theme = at.branch ? themesOf(at.tree).get(at.branch.id) : void 0;
14810
14758
  const out = [];
14811
- if (theme) out.push(`${paint(theme.colour, "\u25CF")} ${paint(ANSI.dim, theme.name)}`);
14812
- const size = sizeWord(s.size, paint);
14813
- out.push(`${paint(ANSI.dim, s.ref)} ${paint(ANSI.bold, s.title)}${size ? ` ${size}` : ""}`);
14814
- const pad = " ".repeat(12);
14815
- const heading = (text2) => out.push("", ` ${paint(ANSI.bold, text2)}`);
14816
- heading("Problem we are solving");
14817
- if (at.leaf) {
14818
- const solves = wrap(at.leaf.problem, width - 6 - at.leaf.ref.length - 2);
14819
- solves[solves.length - 1] += ` ${paint(ANSI.dim, at.leaf.ref)}`;
14820
- for (const line of solves) out.push(` ${line}`);
14821
- if (at.branch && at.branch.id !== at.leaf.id) {
14822
- for (const line of wrap(`Part of: ${at.branch.problem} ${at.branch.ref}`, width - 6)) out.push(` ${paint(ANSI.dim, line)}`);
14823
- }
14824
- } else {
14825
- out.push(paint(ANSI.dim, " The problem it answered is no longer on the map."));
14759
+ if (!lens) return "No objective yet: the map is drawn for it. Say what matters most right now on Your business, then `nanopm opportunities run`.";
14760
+ out.push(`${paint(ANSI.bold, "Opportunities")} ${paint(ANSI.dim, `${lens.lever} \u2014 ${lens.sentence}`)}`);
14761
+ out.push(paint(ANSI.dim, lens.definition ? `Counts: ${lens.definition}` : "What it counts is not defined yet: drawn against the lever and your sentence."));
14762
+ if (opts.previous) out.push(paint(ANSI.amber, "Drawn for your previous objective. `nanopm opportunities run` draws it for this one."));
14763
+ if (!rows2.length) {
14764
+ out.push("", "No map yet. `nanopm opportunities run` draws it from what NanoPM knows; add --deep to check it against your users, code and data.");
14765
+ return out.join("\n");
14826
14766
  }
14827
- const described = paragraphs(s.description);
14828
- if (described.length) {
14829
- heading("Proposed solution");
14830
- described.forEach((p, i) => {
14831
- if (i) out.push("");
14832
- for (const line of wrap(p, width - 6)) out.push(` ${line}`);
14833
- });
14834
- }
14835
- const changelog = parseChangelog(s.changelog);
14836
- if (changelog) {
14837
- heading("Future changelog");
14838
- for (const line of wrap(changelog.headline, width - 6)) out.push(` ${paint(ANSI.bold, line)}`);
14839
- for (const point of changelog.points) wrap(point, width - 8).forEach((line, i) => out.push(` ${i ? " " : "\u2022"} ${line}`));
14840
- }
14841
- if (s.steps.length) {
14842
- heading("Plan");
14843
- s.steps.forEach((step, i) => {
14844
- const who = STEP_WHO_WORD[step.who] ?? step.who;
14845
- wrap(step.do, width - pad.length - 8).forEach((line, j) => out.push(`${j ? pad : ` \u2610 ${who.padEnd(6)}`}${line}`));
14846
- if (step.checks) out.push(pad + paint(ANSI.amber, i === 0 ? "checks the problem first" : "checks the problem"));
14847
- });
14848
- }
14849
- const card = at.leaf ? at.cards.find((c) => c.leaf.id === at.leaf.id) : void 0;
14850
- const others = card ? card.others.filter((o) => o.id !== s.id) : [];
14851
- out.push("");
14852
- if (s.status === "rejected") out.push(paint(ANSI.dim, " Thrown out."));
14853
- else if (card?.pick.id === s.id) {
14854
- const column = at.cards.filter((c) => c.horizon === card.horizon);
14855
- const yours = s.placed_by === "founder" ? ", where you put it" : "";
14856
- out.push(paint(ANSI.dim, ` On the board: ${HORIZON_WORD[card.horizon]}, ${column.indexOf(card) + 1} of ${column.length}${yours}.`));
14857
- } else out.push(paint(ANSI.dim, card ? ` Not on the board: ${card.pick.ref} is this problem's card.` : " Not on the board."));
14858
- if (others.length) out.push(paint(ANSI.dim, ` Other solutions to this problem: ${others.map((o) => o.ref).join(", ")}.`));
14859
- if (s.status === "proposed") {
14860
- const move = card?.pick.id === s.id ? `nanopm roadmap move ${s.ref} now|next|later [position] \xB7 ` : "";
14861
- out.push(paint(ANSI.dim, ` ${move}nanopm reject ${s.ref} ["why"]`));
14767
+ const tree = liveTree(rows2);
14768
+ const live = rows2.filter(isLive);
14769
+ const evidenced = live.filter((r) => r.evidence_status === "evidence_backed").length;
14770
+ out.push(paint(ANSI.dim, `${live.length} on the map: ${evidenced} evidence-backed, ${live.length - evidenced} hypotheses \u2014 what users or their behaviour have not shown yet.`));
14771
+ const who = (r) => r.personas.map((p) => opts.names?.get(p) ?? p).join(", ");
14772
+ const one = (r, place) => {
14773
+ const indent = " ".repeat(r.depth - 1);
14774
+ const yours = r.status === "confirmed" ? paint(ANSI.green, " yours") : "";
14775
+ out.push("", `${indent}${paint(ANSI.bold, place.padStart(2))} ${r.depth === 1 ? paint(ANSI.bold, r.problem) : r.problem}${yours} ${paint(ANSI.dim, r.ref)}`);
14776
+ const pad = `${indent} `;
14777
+ const facts = [PRIORITY_WORD[r.priority], EVIDENCE_STATUS_WORD[r.evidence_status], NEXT_ACTION_WORD[r.next_action], who(r)].filter(Boolean).join(" \xB7 ");
14778
+ out.push(`${pad}${paint(ANSI.dim, facts)}`);
14779
+ const gist = r.description ? paragraphs(r.description)[0] : null;
14780
+ if (gist) for (const l of wrap(gist, width - pad.length)) out.push(`${pad}${l}`);
14781
+ if (r.learn) for (const l of wrap(`To learn more: ${r.learn}`, width - pad.length)) out.push(`${pad}${paint(ANSI.dim, l)}`);
14782
+ if (r.founder_rank !== null && r.now_rank) out.push(`${pad}${paint(ANSI.dim, `Its place is yours; NanoPM would now put it at #${r.now_rank}.`)}`);
14783
+ if (r.reconsider) for (const l of wrap(`Worth reconsidering: ${r.reconsider}`, width - pad.length)) out.push(`${pad}${paint(ANSI.amber, l)}`);
14784
+ for (const c of r.children) one(c, `${place}.${c.rank}`);
14785
+ };
14786
+ for (const r of tree) one(r, String(r.rank));
14787
+ const aside = rows2.filter((r) => r.status === "set_aside");
14788
+ if (aside.length) {
14789
+ out.push("", paint(ANSI.bold, `Set aside (${aside.length})`));
14790
+ for (const r of aside) out.push(` ${paint(ANSI.dim, r.ref.padEnd(8))} ${r.problem}${r.set_aside_reason ? paint(ANSI.dim, ` \u2014 ${r.set_aside_reason}`) : ""}`);
14862
14791
  }
14792
+ out.push("", paint(ANSI.dim, '`nanopm opportunities confirm <ref>`, `set-aside <ref> "why"`, `move <ref> <place>`, `reword <ref> "\u2026"`, `brainstorm <ref> [--deep]`, `run [--deep] [--note "\u2026"] [--use signals,data,repo,market] [--skip \u2026]`.'));
14863
14793
  return out.join("\n");
14864
14794
  }
14865
14795
 
14866
- // src/format/problems.ts
14867
- var SHAPE2 = {
14868
- candidate: { mark: "\u25CB", colour: ANSI.dim, label: "candidate" },
14869
- confirmed: { mark: "\u25C9", colour: ANSI.amber, label: "confirmed" },
14870
- addressed: { mark: "\u25CF", colour: ANSI.green, label: "addressed" },
14871
- dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
14872
- };
14873
- function renderImportance(problem, opts) {
14874
- const paint = painter(opts.color);
14875
- if (problem.importance_basis === "evidence") {
14876
- return paint(ANSI.bold, String(Math.round(problem.importance)).padStart(3));
14877
- }
14878
- const value = problem.importance;
14879
- const word = value >= 67 ? "high" : value >= 34 ? "med" : value > 0 ? "low" : "\u2014";
14880
- return paint(ANSI.dim, `~${word}`.padStart(5));
14881
- }
14882
- function renderProblems(problems2, duplicates, opts) {
14883
- const paint = painter(opts.color);
14884
- if (!problems2.length) {
14885
- return [
14886
- "No problems on the list yet.",
14887
- "",
14888
- paint(ANSI.dim, " nanopm review reads your data, then works out what they say about your users"),
14889
- paint(ANSI.dim, " nanopm opportunities what to work on, for your objective")
14890
- ].join("\n");
14891
- }
14892
- const refWidth = Math.max(...problems2.map((p) => p.ref.length));
14893
- const out = [];
14894
- const map = groupProblemMap(problems2.map((p) => ({ ...p, parentRef: p.parent_ref })));
14895
- const ordered = [...map.branches.flatMap((b) => [b.parent, ...b.children]), ...map.unplaced];
14896
- for (const problem of ordered) {
14897
- const shape2 = SHAPE2[problem.status] ?? { mark: "\xB7", colour: ANSI.dim, label: problem.status };
14898
- const score = renderImportance(problem, opts);
14899
- const head = `${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, problem.ref.padEnd(refWidth))} ${score} ${problem.parent_ref ? paint(ANSI.dim, "\u21B3 ") : ""}${clip(firstSentence(problem.statement), Math.max(40, columns() - refWidth - 14))}`;
14900
- out.push(head);
14901
- if (problem.mapping) out.push(paint(ANSI.dim, ` ${problem.mapping.level === 1 ? "Need" : "Difficulty"} \xB7 ${problem.mapping.knowledge} \xB7 ${problem.mapping.origin === "expert_inference" ? "expert inference" : "source analysis"}`));
14902
- const indent = ` ${" ".repeat(refWidth)} `;
14903
- const tail2 = problem.proofs > 0 ? `${problem.proofs} proof${problem.proofs === 1 ? "" : "s"}${problem.proof_kinds?.length ? ` \xB7 ${problem.proof_kinds.join(", ")}` : ""}` : problem.importance_note ? `no proof yet \xB7 ${problem.importance_note}` : "no proof yet";
14904
- out.push(paint(ANSI.dim, indent + clip(tail2, Math.max(40, columns() - indent.length))));
14905
- }
14906
- const unproven = problems2.filter((p) => p.proofs === 0).length;
14907
- if (unproven) {
14908
- out.push("");
14909
- out.push(
14910
- paint(
14911
- ANSI.dim,
14912
- `${unproven} of ${problems2.length} rest on judgement, not proof. A tilde marks those.`
14913
- )
14914
- );
14915
- }
14916
- if (duplicates.length) {
14917
- out.push("");
14918
- for (const pair of duplicates.slice(0, 3)) {
14919
- const why = pair.flagged ? `NanoPM is not sure ${pair.a} and ${pair.b} are different problems` : `${pair.a} and ${pair.b} read alike`;
14920
- out.push(paint(ANSI.amber, `? ${why}`));
14921
- }
14922
- out.push(paint(ANSI.dim, ` nanopm decisions why \xB7 nanopm problems merge ${duplicates[0].b} ${duplicates[0].a}`));
14923
- }
14924
- if (opts.manualMerges !== void 0) {
14925
- out.push("");
14926
- out.push(
14927
- paint(
14928
- ANSI.dim,
14929
- `${opts.manualMerges} merge${opts.manualMerges === 1 ? "" : "s"} you have had to make by hand.`
14930
- )
14931
- );
14932
- }
14933
- out.push("");
14934
- out.push(paint(ANSI.dim, " nanopm problems <ref> \xB7 nanopm problems merge <from> <into> \xB7 nanopm problems weight <ref> <0-100>"));
14935
- return out.join("\n");
14936
- }
14937
- function renderProblem(problem, evidence2, addressedBy, opts) {
14796
+ // src/format/needs.ts
14797
+ var MARK = { observed: "\u25CF", stated: "\u2013", inferred: "\u25CB" };
14798
+ function renderNeeds(persona, tree, opts) {
14938
14799
  const paint = painter(opts.color);
14939
- const shape2 = SHAPE2[problem.status] ?? { mark: "\xB7", colour: ANSI.dim, label: problem.status };
14940
- const width = columns();
14800
+ const width = Math.max(40, columns());
14941
14801
  const out = [];
14942
- out.push(`${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, problem.ref)} ${paint(ANSI.bold, shape2.label)}`);
14943
- out.push("");
14944
- for (const line of wrap(problem.statement, width)) out.push(line);
14945
- out.push("");
14946
- const facts = [];
14947
- const m = problem.mapping ?? null;
14948
- if (m) {
14949
- const story = problemStory(m, { proofs: problem.proofs, kinds: problem.proof_kinds ?? [], provenance: problem.provenance ?? "inferred", authorKind: problem.author_kind ?? "nanopm", createdAt: problem.created_at ?? "" });
14950
- out.push(...wrap(story.story, width));
14951
- out.push("");
14952
- out.push(...wrap(story.doubt, width).map((l) => paint(ANSI.dim, l)));
14953
- out.push(...wrap(story.assumes ? `${story.test} ${story.assumes}` : story.test, width).map((l) => paint(ANSI.dim, l)));
14954
- out.push("");
14955
- out.push(paint(ANSI.dim, "how I got here"));
14956
- facts.push(["kind", `${m.level === 1 ? "a need" : "a difficulty"} \xB7 ${m.knowledge} \xB7 ${m.origin === "expert_inference" ? "expert inference" : "source analysis"}`]);
14957
- if (m.parent_reason) facts.push(["grouping", m.parent_reason]);
14958
- }
14959
- if (problem.segment) facts.push(["who", problem.segment]);
14960
- facts.push([
14961
- "weight",
14962
- problem.importance_basis === "evidence" ? `${Math.round(problem.importance)} from ${problem.proofs} piece${problem.proofs === 1 ? "" : "s"} of proof` : `${Math.round(problem.importance)}, a judgement${problem.importance_note ? ` \u2014 ${problem.importance_note}` : ""}`
14963
- ]);
14964
- if (problem.parent_ref) facts.push(["part of", problem.parent_ref]);
14965
- for (const [label, value] of facts) {
14966
- out.push(`${paint(ANSI.dim, label.padEnd(9))}${clip(value, width - 9)}`);
14967
- }
14968
- if (evidence2.length) {
14969
- out.push("");
14970
- out.push(paint(ANSI.dim, "sources"));
14971
- for (const item of evidence2) {
14972
- const when = (item.occurred_at ?? item.created_at).slice(0, 10);
14973
- const where = item.metric_key ?? item.source_ref ?? item.kind;
14974
- for (const line of wrap(item.body, width - 2)) out.push(` ${line}`);
14975
- out.push(paint(ANSI.dim, ` \u2014 ${item.relation ?? "supports"} \xB7 ${item.kind} \xB7 ${where} \xB7 ${when}`));
14976
- }
14977
- } else {
14978
- out.push("");
14979
- out.push(paint(ANSI.dim, "No proof yet. This ranking is a judgement, and the next review may settle it."));
14802
+ const counts = needCounts(tree);
14803
+ out.push(`${paint(ANSI.bold, `${persona.name}\u2019s needs`)} ${paint(ANSI.dim, `${counts.needs} needs \xB7 ${counts.difficulties + counts.precise} difficulties`)}`);
14804
+ if (!tree.length) {
14805
+ out.push("", `Nothing mapped yet. \`nanopm needs --refresh ${persona.slug}\` maps it in depth; add --short for a quick pass.`);
14806
+ return out.join("\n");
14980
14807
  }
14981
- if (addressedBy.length) {
14808
+ const line = (n, depth) => {
14809
+ const indent = " ".repeat(depth);
14810
+ const lead = n.level === 1 ? `${n.emoji ?? " "} ` : `${MARK[n.provenance]} `;
14811
+ const where = n.provenance === "inferred" ? paint(ANSI.dim, ` guess ${n.confidence ?? 0}%`) : n.sources.length ? paint(ANSI.dim, ` ${n.sources.length} source${n.sources.length === 1 ? "" : "s"}`) : "";
14812
+ const state = n.status === "dropped" ? paint(ANSI.dim, " dropped") : n.status === "confirmed" || n.author_kind === "user" ? "" : paint(ANSI.amber, " to review");
14813
+ const label = n.status === "dropped" ? paint(ANSI.dim, n.label) : n.level === 1 ? paint(ANSI.bold, n.label) : n.label;
14814
+ const body = wrap(`${label}${where}${state}${n.proposal ? paint(ANSI.amber, " \xB7 NanoPM suggests a new wording") : ""}`, width - indent.length - 12);
14815
+ out.push(`${indent}${lead}${paint(ANSI.dim, n.ref.padEnd(8))} ${body[0] ?? ""}`);
14816
+ for (const more of body.slice(1)) out.push(`${indent} ${more}`);
14817
+ for (const c of n.children) line(c, depth + 1);
14818
+ };
14819
+ for (const need of tree) {
14982
14820
  out.push("");
14983
- out.push(paint(ANSI.dim, "addressed by"));
14984
- for (const item of addressedBy) {
14985
- out.push(` ${paint(ANSI.dim, item.ref)} ${clip(item.title, width - 12)} ${paint(ANSI.dim, `(${item.status})`)}`);
14986
- }
14821
+ line(need, 0);
14987
14822
  }
14988
- out.push("");
14989
- out.push(paint(ANSI.dim, " nanopm problems \xB7 nanopm roadmap"));
14990
- return out.join("\n");
14991
- }
14992
-
14993
- // src/format/tree.ts
14994
- var SOLUTION_SHAPE = {
14995
- proposed: { mark: "\u25CB", colour: ANSI.amber, label: "proposed" },
14996
- accepted: { mark: "\u25D0", colour: ANSI.green, label: "accepted" },
14997
- building: { mark: "\u25D4", colour: ANSI.blue, label: "building" },
14998
- done: { mark: "\u25CF", colour: ANSI.green, label: "done" },
14999
- rejected: { mark: "\u2298", colour: ANSI.red, label: "rejected" },
15000
- dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
15001
- };
15002
- var PROBLEM_SHAPE = {
15003
- candidate: { mark: "\u25CB", colour: ANSI.dim },
15004
- confirmed: { mark: "\u25C9", colour: ANSI.amber },
15005
- addressed: { mark: "\u25CF", colour: ANSI.green },
15006
- dropped: { mark: "\u2298", colour: ANSI.dim }
15007
- };
15008
- var NO_AREA = "no area yet";
15009
- var SHOWN = /* @__PURE__ */ new Set(["proposed", "accepted", "building", "done"]);
15010
- function renderTree(problems2, solutions, strategy, opts) {
15011
- const paint = painter(opts.color);
15012
- const out = [];
15013
- const width = Math.max(40, columns());
15014
- if (strategy) {
15015
- out.push(paint(ANSI.bold, "Strategy"));
15016
- for (const line of wrap(strategy, width - 4)) out.push(` ${line}`);
15017
- out.push("");
15018
- }
15019
- const shown = solutions.filter((s) => SHOWN.has(s.status));
15020
- const open = problems2.filter((p) => p.status !== "dropped" && (!opts.alongside || shown.some((s) => s.problem_id === p.id)));
15021
- if (opts.alongside && !open.length) return "";
15022
- if (!open.length && !shown.length) {
15023
- out.push("No problems yet. `nanopm opportunities` shows what to work on.");
15024
- return out.join("\n");
15025
- }
15026
- const byProblem = /* @__PURE__ */ new Map();
15027
- for (const s of shown) byProblem.set(s.problem_id, [...byProblem.get(s.problem_id) ?? [], s]);
15028
- const refWidth = Math.max(...[...open.map((p) => p.ref), ...shown.map((s) => s.ref)].map((r) => r.length), 5);
15029
- for (const [area, rows2] of groupByArea(open)) {
15030
- out.push(paint(ANSI.bold, area));
15031
- for (const p of rows2) {
15032
- const shape2 = PROBLEM_SHAPE[p.status] ?? { mark: "\xB7", colour: "" };
15033
- const kind = p.kind === "founder" ? paint(ANSI.dim, " (founder)") : "";
15034
- const proof = p.proofs ? `${p.proofs} proof${p.proofs === 1 ? "" : "s"}` : "no proof";
15035
- const tail2 = `${renderImportance(p, opts)} ${paint(ANSI.dim, proof)}`;
15036
- const head = ` ${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, p.ref.padEnd(refWidth))} `;
15037
- out.push(head + clip(p.statement, Math.max(30, width - visibleLength(head) - visibleLength(tail2) - 2)) + kind + " " + tail2);
15038
- const under = byProblem.get(p.id) ?? [];
15039
- if (!under.length) {
15040
- out.push(paint(ANSI.dim, ` ${" ".repeat(refWidth + 2)} no solution yet`));
15041
- continue;
15042
- }
15043
- for (const s of under) out.push(...solutionLines(s, refWidth, width, paint));
15044
- }
15045
- out.push("");
15046
- }
15047
- const waiting = shown.filter((s) => s.status === "proposed").length;
15048
- if (waiting) {
15049
- out.push(`${waiting} waiting on you.`);
15050
- out.push(paint(ANSI.dim, ' nanopm accept <ref> \xB7 nanopm reject <ref> "why" \xB7 nanopm roadmap <ref> \xB7 nanopm roadmap --board'));
15051
- } else if (!opts.alongside) {
15052
- out.push(paint(ANSI.dim, "Nothing waiting on you."));
15053
- }
15054
- return out.join("\n").trimEnd();
15055
- }
15056
- function solutionLines(s, refWidth, width, paint, indent = 6) {
15057
- const shape2 = SOLUTION_SHAPE[s.status] ?? { mark: "\xB7", colour: "", label: s.status };
15058
- const pad = " ".repeat(indent);
15059
- const STATE = { accepted: "chosen", building: "building", done: "done" };
15060
- const tail2 = s.actor === "founder" ? "yours to do" : s.outcome ? s.outcome : s.status === "proposed" ? s.assumption ? clip(s.assumption, 40) : null : STATE[s.status] ?? null;
15061
- const badge = tail2 ? paint(ANSI.dim, ` ${tail2}`) : "";
15062
- const head = `${pad}${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, s.ref.padEnd(refWidth))} `;
15063
- const lines = [head + clip(s.title, Math.max(30, width - visibleLength(head) - visibleLength(badge))) + badge];
15064
- const reason2 = `${pad} ${" ".repeat(refWidth)} `;
15065
- lines.push(paint(ANSI.dim, reason2 + clip(firstSentence(s.reasoning), Math.max(30, width - reason2.length))));
15066
- return lines;
15067
- }
15068
- function groupByArea(problems2) {
15069
- const groups = /* @__PURE__ */ new Map();
15070
- for (const p of problems2) {
15071
- const area = p.area ?? NO_AREA;
15072
- groups.set(area, [...groups.get(area) ?? [], p]);
15073
- }
15074
- const named = [...groups.entries()].filter(([a]) => a !== NO_AREA);
15075
- const rest = groups.get(NO_AREA);
15076
- return rest ? [...named, [NO_AREA, rest]] : named;
15077
- }
15078
- var BOARD_COLUMNS = [
15079
- { status: "proposed", label: "Proposed" },
15080
- { status: "accepted", label: "Chosen" },
15081
- { status: "building", label: "Building" },
15082
- { status: "done", label: "Done" }
15083
- ];
15084
- function renderBoard(problems2, solutions, opts) {
15085
- const paint = painter(opts.color);
15086
- const out = [];
15087
- const byId = new Map(problems2.map((p) => [p.id, p]));
15088
- const width = Math.max(40, columns());
15089
- const refWidth = Math.max(...solutions.map((s) => s.ref.length), 5);
15090
- for (const col of BOARD_COLUMNS) {
15091
- const rows2 = solutions.filter((s) => s.status === col.status);
15092
- out.push(`${paint(ANSI.bold, col.label)} ${paint(ANSI.dim, String(rows2.length))}`);
15093
- if (!rows2.length) out.push(paint(ANSI.dim, " \u2014"));
15094
- for (const s of rows2) {
15095
- const shape2 = SOLUTION_SHAPE[s.status];
15096
- const problem = byId.get(s.problem_id);
15097
- const head = ` ${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, s.ref.padEnd(refWidth))} `;
15098
- out.push(head + clip(s.title, Math.max(30, width - visibleLength(head))));
15099
- const under = [problem ? `${problem.ref} ${clip(problem.statement, 50)}` : "its problem was dropped", s.assumption ? clip(s.assumption, 40) : null].filter(Boolean).join(" \xB7 ");
15100
- out.push(paint(ANSI.dim, ` ${" ".repeat(refWidth + 2)} ${under}`));
15101
- }
15102
- out.push("");
15103
- }
15104
- return out.join("\n").trimEnd();
15105
- }
15106
-
15107
- // src/format/opportunities.ts
15108
- function renderOpportunities(rows2, lens, opts) {
15109
- const paint = painter(opts.color);
15110
- const width = Math.max(40, columns());
15111
- const out = [];
15112
- if (!lens) return "No objective yet: the map is drawn for it. Say what matters most right now on Your business, then `nanopm opportunities run`.";
15113
- out.push(`${paint(ANSI.bold, "Opportunities")} ${paint(ANSI.dim, `${lens.lever} \u2014 ${lens.sentence}`)}`);
15114
- out.push(paint(ANSI.dim, lens.definition ? `Counts: ${lens.definition}` : "What it counts is not defined yet: drawn against the lever and your sentence."));
15115
- if (opts.previous) out.push(paint(ANSI.amber, "Drawn for your previous objective. `nanopm opportunities run` draws it for this one."));
15116
- if (!rows2.length) {
15117
- out.push("", "No map yet. `nanopm opportunities run` draws it from what NanoPM knows; add --deep to check it against your users, code and data.");
15118
- return out.join("\n");
15119
- }
15120
- const tree = liveTree(rows2);
15121
- const live = rows2.filter(isLive);
15122
- const evidenced = live.filter((r) => r.evidence_status === "evidence_backed").length;
15123
- out.push(paint(ANSI.dim, `${live.length} on the map: ${evidenced} evidence-backed, ${live.length - evidenced} hypotheses \u2014 what users or their behaviour have not shown yet.`));
15124
- const who = (r) => r.personas.map((p) => opts.names?.get(p) ?? p).join(", ");
15125
- const one = (r, place) => {
15126
- const indent = " ".repeat(r.depth - 1);
15127
- const yours = r.status === "confirmed" ? paint(ANSI.green, " yours") : "";
15128
- out.push("", `${indent}${paint(ANSI.bold, place.padStart(2))} ${r.depth === 1 ? paint(ANSI.bold, r.problem) : r.problem}${yours} ${paint(ANSI.dim, r.ref)}`);
15129
- const pad = `${indent} `;
15130
- const facts = [PRIORITY_WORD[r.priority], EVIDENCE_STATUS_WORD[r.evidence_status], NEXT_ACTION_WORD[r.next_action], who(r)].filter(Boolean).join(" \xB7 ");
15131
- out.push(`${pad}${paint(ANSI.dim, facts)}`);
15132
- const gist = r.description ? paragraphs(r.description)[0] : null;
15133
- if (gist) for (const l of wrap(gist, width - pad.length)) out.push(`${pad}${l}`);
15134
- if (r.learn) for (const l of wrap(`To learn more: ${r.learn}`, width - pad.length)) out.push(`${pad}${paint(ANSI.dim, l)}`);
15135
- if (r.founder_rank !== null && r.now_rank) out.push(`${pad}${paint(ANSI.dim, `Its place is yours; NanoPM would now put it at #${r.now_rank}.`)}`);
15136
- if (r.reconsider) for (const l of wrap(`Worth reconsidering: ${r.reconsider}`, width - pad.length)) out.push(`${pad}${paint(ANSI.amber, l)}`);
15137
- for (const c of r.children) one(c, `${place}.${c.rank}`);
15138
- };
15139
- for (const r of tree) one(r, String(r.rank));
15140
- const aside = rows2.filter((r) => r.status === "set_aside");
15141
- if (aside.length) {
15142
- out.push("", paint(ANSI.bold, `Set aside (${aside.length})`));
15143
- for (const r of aside) out.push(` ${paint(ANSI.dim, r.ref.padEnd(8))} ${r.problem}${r.set_aside_reason ? paint(ANSI.dim, ` \u2014 ${r.set_aside_reason}`) : ""}`);
15144
- }
15145
- out.push("", paint(ANSI.dim, '`nanopm opportunities confirm <ref>`, `set-aside <ref> "why"`, `move <ref> <place>`, `reword <ref> "\u2026"`, `brainstorm <ref> [--deep]`, `run [--deep] [--note "\u2026"] [--use signals,data,repo,market] [--skip \u2026]`.'));
15146
- return out.join("\n");
15147
- }
15148
-
15149
- // src/format/needs.ts
15150
- var MARK = { observed: "\u25CF", stated: "\u2013", inferred: "\u25CB" };
15151
- function renderNeeds(persona, tree, opts) {
15152
- const paint = painter(opts.color);
15153
- const width = Math.max(40, columns());
15154
- const out = [];
15155
- const counts = needCounts(tree);
15156
- out.push(`${paint(ANSI.bold, `${persona.name}\u2019s needs`)} ${paint(ANSI.dim, `${counts.needs} needs \xB7 ${counts.difficulties + counts.precise} difficulties`)}`);
15157
- if (!tree.length) {
15158
- out.push("", `Nothing mapped yet. \`nanopm needs --refresh ${persona.slug}\` maps it in depth; add --short for a quick pass.`);
15159
- return out.join("\n");
15160
- }
15161
- const line = (n, depth) => {
15162
- const indent = " ".repeat(depth);
15163
- const lead = n.level === 1 ? `${n.emoji ?? " "} ` : `${MARK[n.provenance]} `;
15164
- const where = n.provenance === "inferred" ? paint(ANSI.dim, ` guess ${n.confidence ?? 0}%`) : n.sources.length ? paint(ANSI.dim, ` ${n.sources.length} source${n.sources.length === 1 ? "" : "s"}`) : "";
15165
- const state = n.status === "dropped" ? paint(ANSI.dim, " dropped") : n.status === "confirmed" || n.author_kind === "user" ? "" : paint(ANSI.amber, " to review");
15166
- const label = n.status === "dropped" ? paint(ANSI.dim, n.label) : n.level === 1 ? paint(ANSI.bold, n.label) : n.label;
15167
- const body = wrap(`${label}${where}${state}${n.proposal ? paint(ANSI.amber, " \xB7 NanoPM suggests a new wording") : ""}`, width - indent.length - 12);
15168
- out.push(`${indent}${lead}${paint(ANSI.dim, n.ref.padEnd(8))} ${body[0] ?? ""}`);
15169
- for (const more of body.slice(1)) out.push(`${indent} ${more}`);
15170
- for (const c of n.children) line(c, depth + 1);
15171
- };
15172
- for (const need of tree) {
15173
- out.push("");
15174
- line(need, 0);
15175
- }
15176
- out.push("", paint(ANSI.dim, `\u25CF observed \u2013 stated by you \u25CB NanoPM\u2019s guess. \`nanopm needs confirm <ref>\`, \`nanopm needs drop <ref> "why"\`, \`nanopm needs dig <ref>\`.`));
15177
- if (opts.others?.length) out.push(paint(ANSI.dim, `Other personas: ${opts.others.join(", ")}.`));
14823
+ out.push("", paint(ANSI.dim, `\u25CF observed \u2013 stated by you \u25CB NanoPM\u2019s guess. \`nanopm needs confirm <ref>\`, \`nanopm needs drop <ref> "why"\`, \`nanopm needs dig <ref>\`.`));
14824
+ if (opts.others?.length) out.push(paint(ANSI.dim, `Other personas: ${opts.others.join(", ")}.`));
15178
14825
  return out.join("\n");
15179
14826
  }
15180
14827
 
@@ -15335,51 +14982,471 @@ async function setAsideOpportunity(ref, why, opts) {
15335
14982
  await rpc2("set_aside_opportunity", { p_ref: ref, p_reason: why ?? "not now" }, opts);
15336
14983
  return `${ref.toUpperCase()} is set aside for this objective, with the narrower problems under it. It comes back if the objective changes.`;
15337
14984
  }
15338
- async function moveOpportunity(ref, place, opts) {
15339
- const rank2 = Number(place);
15340
- if (!Number.isInteger(rank2) || rank2 < 1) throw new Error("say where: nanopm opportunities move <ref> <place>, a place from 1");
15341
- await rpc2("move_opportunity", { p_ref: ref, p_rank: rank2 }, opts);
15342
- return `${ref.toUpperCase()} is at #${rank2} among its siblings, and the place is yours. Moved above a higher priority, it takes that priority.`;
14985
+ async function moveOpportunity(ref, place, opts) {
14986
+ const rank2 = Number(place);
14987
+ if (!Number.isInteger(rank2) || rank2 < 1) throw new Error("say where: nanopm opportunities move <ref> <place>, a place from 1");
14988
+ await rpc2("move_opportunity", { p_ref: ref, p_rank: rank2 }, opts);
14989
+ return `${ref.toUpperCase()} is at #${rank2} among its siblings, and the place is yours. Moved above a higher priority, it takes that priority.`;
14990
+ }
14991
+ async function rewordOpportunity(ref, problem, opts) {
14992
+ const text2 = problem?.trim().replace(/\s+/g, " ") ?? "";
14993
+ if (!text2) throw new Error('say it: nanopm opportunities reword <ref> "the problem, in your words"');
14994
+ if (text2.length > PROBLEM_MAX) throw new Error(`one line, under ${PROBLEM_MAX} characters`);
14995
+ await rpc2("restate_opportunity", { p_ref: ref, p_problem: text2 }, opts);
14996
+ return `Kept in your words: ${text2}`;
14997
+ }
14998
+ var AWAKE_MS = 12e4;
14999
+ async function awakeDaemon(client, userId) {
15000
+ const since = new Date(Date.now() - AWAKE_MS).toISOString();
15001
+ const [{ data }, local] = await Promise.all([
15002
+ client.from("daemons").select("id, hostname").eq("user_id", userId).in("status", ["online", "running"]).gte("last_seen_at", since).order("last_seen_at", { ascending: false }),
15003
+ localDaemonId()
15004
+ ]);
15005
+ return data?.find((d) => d.id === local) ?? data?.[0] ?? null;
15006
+ }
15007
+ async function brainstorm(ref, opts) {
15008
+ const auth = await authenticate();
15009
+ const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
15010
+ const [lens, all, busy] = await Promise.all([lensOf2(auth.client, project.id), loadOpportunities(auth.client, project.id), workingOn(auth.client, project.id)]);
15011
+ const deep = Boolean(opts.deep);
15012
+ const plan = brainstormPlan(shownMap(all, lens?.key ?? null).shown, ref, deep, new Set(busy.keys()));
15013
+ const daemon = await awakeDaemon(auth.client, auth.userId);
15014
+ 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 brainstorm again.");
15015
+ const repo = deep ? await detectRepo(opts.cwd) : null;
15016
+ if (repo) await rememberClone(auth.client, project.id, repo.root);
15017
+ for (const params of plan.jobs) {
15018
+ const { error } = await auth.client.from("jobs").insert({ project_id: project.id, skill: SOLUTIONS_SKILL, params: { ...params, ...repo ? { repo_path: repo.root } : {} }, requested_by: auth.userId, trigger: "user", target_daemon_id: daemon.id });
15019
+ if (error) throw new Error(`could not queue the brainstorm: ${error.message}`);
15020
+ }
15021
+ const n = plan.leaves.length;
15022
+ const minutes = deep ? DEEP_MINUTES * n : FLASH_MINUTES;
15023
+ const what = plan.problem.children.length ? `the ${n} narrower problem${n === 1 ? "" : "s"} under ${plan.problem.ref}` : `${plan.problem.ref} \u201C${clip(plan.problem.problem, 60)}\u201D`;
15024
+ const how = deep ? `${DEPTH_WORD.deep}${n > 1 ? ", one after the other" : ""}` : `${DEPTH_WORD.quick}, a first pass`;
15025
+ const out = [`${deep ? "Going deeper on" : "Brainstorming"} ${what}: ${how}, about ${minutes} minutes on ${daemon.hostname}.`];
15026
+ if (plan.problem.children.length) for (const l of plan.leaves) out.push(` ${l.ref.padEnd(8)} ${clip(l.problem, Math.max(30, columns() - 11))}`);
15027
+ if (plan.busy) out.push(`${plan.busy} left out: Nano is already finding solutions to ${plan.busy === 1 ? "it" : "them"}.`);
15028
+ out.push("The solutions land on `nanopm roadmap`.");
15029
+ return out.join("\n");
15030
+ }
15031
+
15032
+ // src/format/roadmap.ts
15033
+ var SHAPE = {
15034
+ proposed: { mark: "\u25CB", colour: ANSI.amber, label: "proposed" },
15035
+ accepted: { mark: "\u25D0", colour: ANSI.green, label: "accepted" },
15036
+ building: { mark: "\u25D4", colour: ANSI.blue, label: "building" },
15037
+ rejected: { mark: "\u2298", colour: ANSI.red, label: "rejected" },
15038
+ done: { mark: "\u25CF", colour: ANSI.green, label: "done" },
15039
+ dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
15040
+ };
15041
+ var WORK_KINDS = /* @__PURE__ */ new Set(["branch", "git", "url", "job"]);
15042
+ function renderRoadmapItem(item, opts, problem, builtBy) {
15043
+ const paint = painter(opts.color);
15044
+ const shape2 = SHAPE[item.status] ?? { mark: "\xB7", colour: "", label: item.status };
15045
+ const out = [`${paint(shape2.colour, shape2.mark)} ${paint(ANSI.bold, item.ref)} ${item.title}`];
15046
+ if (problem) out.push(paint(ANSI.dim, ` answers ${problem.ref} ${clip(problem.statement, Math.max(30, columns() - 20))}`));
15047
+ out.push("");
15048
+ for (const line of wrap(item.reasoning, Math.max(40, columns() - 4))) out.push(` ${line}`);
15049
+ out.push("");
15050
+ const facts = [
15051
+ ["status", shape2.label + (item.outcome ? ` \xB7 ${item.outcome}${item.outcome_note ? `: ${item.outcome_note}` : ""}` : "")],
15052
+ ["rank", String(item.rank)],
15053
+ ["who", item.actor === "founder" ? "you" : "NanoPM"],
15054
+ ["bets on", item.assumption ?? "\u2014"],
15055
+ ["we'll know", item.eval ?? "\u2014"]
15056
+ ];
15057
+ for (const [k, v] of facts) out.push(paint(ANSI.dim, ` ${k.padEnd(9)} ${v}`));
15058
+ const sources2 = item.sources ?? [];
15059
+ const restsOn = sources2.filter((s) => !WORK_KINDS.has(s.kind));
15060
+ if (restsOn.length) {
15061
+ out.push(paint(ANSI.dim, ` ${"rests on".padEnd(9)} ${restsOn.map((s) => `${s.kind}:${s.ref}`).join(", ")}`));
15062
+ }
15063
+ const where = renderWhere(sources2);
15064
+ if (where) out.push(paint(ANSI.dim, ` ${"where".padEnd(9)} ${where}${builtBy ? ` \xB7 built by ${builtBy}` : ""}`));
15065
+ return out.join("\n");
15066
+ }
15067
+ function renderWhere(sources2) {
15068
+ const branch = sources2.find((s) => s.kind === "branch")?.ref;
15069
+ const commit = sources2.find((s) => s.kind === "git")?.ref;
15070
+ const url = sources2.find((s) => s.kind === "url")?.ref;
15071
+ return [branch, commit, url].filter(Boolean).join(" \xB7 ");
15072
+ }
15073
+ var VERB = {
15074
+ want: { text: "wanted", colour: ANSI.amber },
15075
+ confirm: { text: "confirmed", colour: ANSI.green },
15076
+ drop: { text: "dropped", colour: ANSI.red },
15077
+ merge: { text: "merged into", colour: ANSI.blue },
15078
+ separate: { text: "kept apart", colour: ANSI.blue },
15079
+ weight: { text: "weighed", colour: ANSI.blue },
15080
+ area: { text: "filed", colour: ANSI.blue },
15081
+ accept: { text: "accepted", colour: ANSI.green },
15082
+ reject: { text: "rejected", colour: ANSI.red },
15083
+ first: { text: "put first", colour: ANSI.blue },
15084
+ done: { text: "did", colour: ANSI.green },
15085
+ dismiss: { text: "dismissed", colour: ANSI.red }
15086
+ };
15087
+ function renderDecisions(rows2, opts) {
15088
+ const paint = painter(opts.color);
15089
+ if (!rows2.length) return "No decisions yet.";
15090
+ const out = [];
15091
+ for (const row of rows2) {
15092
+ const when = row.created_at.slice(0, 10);
15093
+ const verb = VERB[row.kind] ?? { text: row.kind, colour: ANSI.blue };
15094
+ const about = row.solutions ? `${paint(ANSI.dim, row.solutions.ref)} ${row.solutions.title}` : row.problems ? paint(ANSI.dim, row.problems.ref) : row.market_entries ? `${paint(ANSI.dim, row.market_entries.ref)} ${row.market_entries.competitor ?? ""}`.trimEnd() : "";
15095
+ const by = row.by ? paint(ANSI.dim, ` \xB7 by ${row.by}`) : "";
15096
+ out.push(`${paint(ANSI.dim, when)} ${paint(verb.colour, verb.text)} ${about}${by}`.trimEnd());
15097
+ if (row.body) {
15098
+ for (const line of wrap(row.body, 72)) out.push(paint(ANSI.dim, ` ${line}`));
15099
+ }
15100
+ }
15101
+ return out.join("\n");
15102
+ }
15103
+ var HUE = ["\x1B[34m", "\x1B[35m", "\x1B[36m", "\x1B[32m", "\x1B[95m"];
15104
+ function themesOf(tree) {
15105
+ return new Map(tree.map((b) => [b.id, { name: themeName(b), colour: HUE[themeHue(tree, b) - 1] }]));
15106
+ }
15107
+ function sizeWord(size, paint) {
15108
+ return size === "big" ? `${paint(ANSI.bold, "\u25AA")} ${SIZE_WORD.big}` : size === "small" ? paint(ANSI.dim, SIZE_WORD.small) : "";
15109
+ }
15110
+ function renderSolutionsBoard(board, opts) {
15111
+ const paint = painter(opts.color);
15112
+ const width = Math.max(40, columns());
15113
+ const themes = themesOf(board.tree);
15114
+ const out = [`${paint(ANSI.bold, "Roadmap")}${board.objective ? ` ${paint(ANSI.dim, `${board.objective.lever} \u2014 ${board.objective.sentence}`)}` : ""}`];
15115
+ const chips = board.tree.flatMap((b) => {
15116
+ const n = board.cards.filter((c) => c.branch.id === b.id).length;
15117
+ const t = themes.get(b.id);
15118
+ return n ? [`${paint(t.colour, "\u25CF")} ${t.name} ${paint(ANSI.dim, String(n))}`] : [];
15119
+ });
15120
+ const legend = chips.reduce((lines, chip) => {
15121
+ const last = lines.at(-1);
15122
+ if (last !== void 0 && visibleLength(last) + 3 + visibleLength(chip) <= width) lines[lines.length - 1] = `${last} ${chip}`;
15123
+ else lines.push(chip);
15124
+ return lines;
15125
+ }, []);
15126
+ out.push("", ...legend);
15127
+ if (!board.cards.some((c) => c.pick.placed_by === "founder")) out.push(paint(ANSI.dim, "Nano placed these in the order of your opportunity map. Move them where you want them."));
15128
+ const refWidth = Math.max(5, ...board.cards.map((c) => c.pick.ref.length));
15129
+ const gutter = " ".repeat(refWidth + 4);
15130
+ const titleWidth = Math.min(Math.max(...board.cards.map((c) => c.pick.title.length)), width - gutter.length - 8);
15131
+ for (const h of HORIZONS) {
15132
+ const column = board.cards.filter((c) => c.horizon === h);
15133
+ out.push("", `${paint(ANSI.bold, HORIZON_WORD[h])} ${paint(ANSI.dim, String(column.length))}`);
15134
+ if (!column.length) out.push(paint(ANSI.dim, ` Move a card here: nanopm roadmap move <ref> ${h}`));
15135
+ for (const c of column) {
15136
+ const t = themes.get(c.branch.id);
15137
+ out.push(` ${paint(ANSI.dim, c.pick.ref.padEnd(refWidth))} ${paint(t.colour, "\u25CF")} ${paint(ANSI.dim, t.name)}`);
15138
+ out.push(`${gutter}${clip(c.pick.title, titleWidth).padEnd(titleWidth)} ${sizeWord(c.pick.size, paint)}`.trimEnd());
15139
+ const depth = board.working.get(c.leaf.id);
15140
+ if (depth) out.push(`${gutter}${paint(ANSI.blue, "\u25CF")} ${paint(ANSI.dim, WORK_WORD[depth])}`);
15141
+ }
15142
+ }
15143
+ if (board.earlier) out.push("", paint(ANSI.bold, "On the problem list"), "", board.earlier);
15144
+ out.push(
15145
+ "",
15146
+ paint(ANSI.dim, 'nanopm roadmap <ref> \xB7 nanopm roadmap move <ref> now|next|later [position] \xB7 nanopm reject <ref> ["why"]'),
15147
+ paint(ANSI.dim, "nanopm opportunities brainstorm <ref> [--deep]: solutions to another problem, or deeper ones")
15148
+ );
15149
+ return out.join("\n");
15150
+ }
15151
+ function renderSolutionSheet(s, at, opts) {
15152
+ const paint = painter(opts.color);
15153
+ const width = Math.max(40, columns());
15154
+ const theme = at.branch ? themesOf(at.tree).get(at.branch.id) : void 0;
15155
+ const out = [];
15156
+ if (theme) out.push(`${paint(theme.colour, "\u25CF")} ${paint(ANSI.dim, theme.name)}`);
15157
+ const size = sizeWord(s.size, paint);
15158
+ out.push(`${paint(ANSI.dim, s.ref)} ${paint(ANSI.bold, s.title)}${size ? ` ${size}` : ""}`);
15159
+ const pad = " ".repeat(12);
15160
+ const heading = (text2) => out.push("", ` ${paint(ANSI.bold, text2)}`);
15161
+ heading("Problem we are solving");
15162
+ if (at.leaf) {
15163
+ const solves = wrap(at.leaf.problem, width - 6 - at.leaf.ref.length - 2);
15164
+ solves[solves.length - 1] += ` ${paint(ANSI.dim, at.leaf.ref)}`;
15165
+ for (const line of solves) out.push(` ${line}`);
15166
+ if (at.branch && at.branch.id !== at.leaf.id) {
15167
+ for (const line of wrap(`Part of: ${at.branch.problem} ${at.branch.ref}`, width - 6)) out.push(` ${paint(ANSI.dim, line)}`);
15168
+ }
15169
+ } else {
15170
+ out.push(paint(ANSI.dim, " The problem it answered is no longer on the map."));
15171
+ }
15172
+ const described = paragraphs(s.description);
15173
+ if (described.length) {
15174
+ heading("Proposed solution");
15175
+ described.forEach((p, i) => {
15176
+ if (i) out.push("");
15177
+ for (const line of wrap(p, width - 6)) out.push(` ${line}`);
15178
+ });
15179
+ }
15180
+ const changelog = parseChangelog(s.changelog);
15181
+ if (changelog) {
15182
+ heading("Future changelog");
15183
+ for (const line of wrap(changelog.headline, width - 6)) out.push(` ${paint(ANSI.bold, line)}`);
15184
+ for (const point of changelog.points) wrap(point, width - 8).forEach((line, i) => out.push(` ${i ? " " : "\u2022"} ${line}`));
15185
+ }
15186
+ if (s.steps.length) {
15187
+ heading("Plan");
15188
+ s.steps.forEach((step, i) => {
15189
+ const who = STEP_WHO_WORD[step.who] ?? step.who;
15190
+ wrap(step.do, width - pad.length - 8).forEach((line, j) => out.push(`${j ? pad : ` \u2610 ${who.padEnd(6)}`}${line}`));
15191
+ if (step.checks) out.push(pad + paint(ANSI.amber, i === 0 ? "checks the problem first" : "checks the problem"));
15192
+ });
15193
+ }
15194
+ const card = at.leaf ? at.cards.find((c) => c.leaf.id === at.leaf.id) : void 0;
15195
+ const others = card ? card.others.filter((o) => o.id !== s.id) : [];
15196
+ out.push("");
15197
+ if (s.status === "rejected") out.push(paint(ANSI.dim, " Thrown out."));
15198
+ else if (card?.pick.id === s.id) {
15199
+ const column = at.cards.filter((c) => c.horizon === card.horizon);
15200
+ const yours = s.placed_by === "founder" ? ", where you put it" : "";
15201
+ out.push(paint(ANSI.dim, ` On the board: ${HORIZON_WORD[card.horizon]}, ${column.indexOf(card) + 1} of ${column.length}${yours}.`));
15202
+ } else out.push(paint(ANSI.dim, card ? ` Not on the board: ${card.pick.ref} is this problem's card.` : " Not on the board."));
15203
+ if (others.length) out.push(paint(ANSI.dim, ` Other solutions to this problem: ${others.map((o) => o.ref).join(", ")}.`));
15204
+ if (s.status === "proposed") {
15205
+ const move = card?.pick.id === s.id ? `nanopm roadmap move ${s.ref} now|next|later [position] \xB7 ` : "";
15206
+ out.push(paint(ANSI.dim, ` ${move}nanopm reject ${s.ref} ["why"]`));
15207
+ }
15208
+ return out.join("\n");
15209
+ }
15210
+
15211
+ // src/format/problems.ts
15212
+ var SHAPE2 = {
15213
+ candidate: { mark: "\u25CB", colour: ANSI.dim, label: "candidate" },
15214
+ confirmed: { mark: "\u25C9", colour: ANSI.amber, label: "confirmed" },
15215
+ addressed: { mark: "\u25CF", colour: ANSI.green, label: "addressed" },
15216
+ dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
15217
+ };
15218
+ function renderImportance(problem, opts) {
15219
+ const paint = painter(opts.color);
15220
+ if (problem.importance_basis === "evidence") {
15221
+ return paint(ANSI.bold, String(Math.round(problem.importance)).padStart(3));
15222
+ }
15223
+ const value = problem.importance;
15224
+ const word = value >= 67 ? "high" : value >= 34 ? "med" : value > 0 ? "low" : "\u2014";
15225
+ return paint(ANSI.dim, `~${word}`.padStart(5));
15226
+ }
15227
+ function renderProblems(problems2, duplicates, opts) {
15228
+ const paint = painter(opts.color);
15229
+ if (!problems2.length) {
15230
+ return [
15231
+ "No problems on the list yet.",
15232
+ "",
15233
+ paint(ANSI.dim, " nanopm review reads your data, then works out what they say about your users"),
15234
+ paint(ANSI.dim, " nanopm opportunities what to work on, for your objective")
15235
+ ].join("\n");
15236
+ }
15237
+ const refWidth = Math.max(...problems2.map((p) => p.ref.length));
15238
+ const out = [];
15239
+ const map = groupProblemMap(problems2.map((p) => ({ ...p, parentRef: p.parent_ref })));
15240
+ const ordered = [...map.branches.flatMap((b) => [b.parent, ...b.children]), ...map.unplaced];
15241
+ for (const problem of ordered) {
15242
+ const shape2 = SHAPE2[problem.status] ?? { mark: "\xB7", colour: ANSI.dim, label: problem.status };
15243
+ const score = renderImportance(problem, opts);
15244
+ const head = `${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, problem.ref.padEnd(refWidth))} ${score} ${problem.parent_ref ? paint(ANSI.dim, "\u21B3 ") : ""}${clip(firstSentence(problem.statement), Math.max(40, columns() - refWidth - 14))}`;
15245
+ out.push(head);
15246
+ if (problem.mapping) out.push(paint(ANSI.dim, ` ${problem.mapping.level === 1 ? "Need" : "Difficulty"} \xB7 ${problem.mapping.knowledge} \xB7 ${problem.mapping.origin === "expert_inference" ? "expert inference" : "source analysis"}`));
15247
+ const indent = ` ${" ".repeat(refWidth)} `;
15248
+ const tail = problem.proofs > 0 ? `${problem.proofs} proof${problem.proofs === 1 ? "" : "s"}${problem.proof_kinds?.length ? ` \xB7 ${problem.proof_kinds.join(", ")}` : ""}` : problem.importance_note ? `no proof yet \xB7 ${problem.importance_note}` : "no proof yet";
15249
+ out.push(paint(ANSI.dim, indent + clip(tail, Math.max(40, columns() - indent.length))));
15250
+ }
15251
+ const unproven = problems2.filter((p) => p.proofs === 0).length;
15252
+ if (unproven) {
15253
+ out.push("");
15254
+ out.push(
15255
+ paint(
15256
+ ANSI.dim,
15257
+ `${unproven} of ${problems2.length} rest on judgement, not proof. A tilde marks those.`
15258
+ )
15259
+ );
15260
+ }
15261
+ if (duplicates.length) {
15262
+ out.push("");
15263
+ for (const pair of duplicates.slice(0, 3)) {
15264
+ const why = pair.flagged ? `NanoPM is not sure ${pair.a} and ${pair.b} are different problems` : `${pair.a} and ${pair.b} read alike`;
15265
+ out.push(paint(ANSI.amber, `? ${why}`));
15266
+ }
15267
+ out.push(paint(ANSI.dim, ` nanopm decisions why \xB7 nanopm problems merge ${duplicates[0].b} ${duplicates[0].a}`));
15268
+ }
15269
+ if (opts.manualMerges !== void 0) {
15270
+ out.push("");
15271
+ out.push(
15272
+ paint(
15273
+ ANSI.dim,
15274
+ `${opts.manualMerges} merge${opts.manualMerges === 1 ? "" : "s"} you have had to make by hand.`
15275
+ )
15276
+ );
15277
+ }
15278
+ out.push("");
15279
+ out.push(paint(ANSI.dim, " nanopm problems <ref> \xB7 nanopm problems merge <from> <into> \xB7 nanopm problems weight <ref> <0-100>"));
15280
+ return out.join("\n");
15281
+ }
15282
+ function renderProblem(problem, evidence2, addressedBy, opts) {
15283
+ const paint = painter(opts.color);
15284
+ const shape2 = SHAPE2[problem.status] ?? { mark: "\xB7", colour: ANSI.dim, label: problem.status };
15285
+ const width = columns();
15286
+ const out = [];
15287
+ out.push(`${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, problem.ref)} ${paint(ANSI.bold, shape2.label)}`);
15288
+ out.push("");
15289
+ for (const line of wrap(problem.statement, width)) out.push(line);
15290
+ out.push("");
15291
+ const facts = [];
15292
+ const m = problem.mapping ?? null;
15293
+ if (m) {
15294
+ const story = problemStory(m, { proofs: problem.proofs, kinds: problem.proof_kinds ?? [], provenance: problem.provenance ?? "inferred", authorKind: problem.author_kind ?? "nanopm", createdAt: problem.created_at ?? "" });
15295
+ out.push(...wrap(story.story, width));
15296
+ out.push("");
15297
+ out.push(...wrap(story.doubt, width).map((l) => paint(ANSI.dim, l)));
15298
+ out.push(...wrap(story.assumes ? `${story.test} ${story.assumes}` : story.test, width).map((l) => paint(ANSI.dim, l)));
15299
+ out.push("");
15300
+ out.push(paint(ANSI.dim, "how I got here"));
15301
+ facts.push(["kind", `${m.level === 1 ? "a need" : "a difficulty"} \xB7 ${m.knowledge} \xB7 ${m.origin === "expert_inference" ? "expert inference" : "source analysis"}`]);
15302
+ if (m.parent_reason) facts.push(["grouping", m.parent_reason]);
15303
+ }
15304
+ if (problem.segment) facts.push(["who", problem.segment]);
15305
+ facts.push([
15306
+ "weight",
15307
+ problem.importance_basis === "evidence" ? `${Math.round(problem.importance)} from ${problem.proofs} piece${problem.proofs === 1 ? "" : "s"} of proof` : `${Math.round(problem.importance)}, a judgement${problem.importance_note ? ` \u2014 ${problem.importance_note}` : ""}`
15308
+ ]);
15309
+ if (problem.parent_ref) facts.push(["part of", problem.parent_ref]);
15310
+ for (const [label, value] of facts) {
15311
+ out.push(`${paint(ANSI.dim, label.padEnd(9))}${clip(value, width - 9)}`);
15312
+ }
15313
+ if (evidence2.length) {
15314
+ out.push("");
15315
+ out.push(paint(ANSI.dim, "sources"));
15316
+ for (const item of evidence2) {
15317
+ const when = (item.occurred_at ?? item.created_at).slice(0, 10);
15318
+ const where = item.metric_key ?? item.source_ref ?? item.kind;
15319
+ for (const line of wrap(item.body, width - 2)) out.push(` ${line}`);
15320
+ out.push(paint(ANSI.dim, ` \u2014 ${item.relation ?? "supports"} \xB7 ${item.kind} \xB7 ${where} \xB7 ${when}`));
15321
+ }
15322
+ } else {
15323
+ out.push("");
15324
+ out.push(paint(ANSI.dim, "No proof yet. This ranking is a judgement, and the next review may settle it."));
15325
+ }
15326
+ if (addressedBy.length) {
15327
+ out.push("");
15328
+ out.push(paint(ANSI.dim, "addressed by"));
15329
+ for (const item of addressedBy) {
15330
+ out.push(` ${paint(ANSI.dim, item.ref)} ${clip(item.title, width - 12)} ${paint(ANSI.dim, `(${item.status})`)}`);
15331
+ }
15332
+ }
15333
+ out.push("");
15334
+ out.push(paint(ANSI.dim, " nanopm problems \xB7 nanopm roadmap"));
15335
+ return out.join("\n");
15336
+ }
15337
+
15338
+ // src/format/tree.ts
15339
+ var SOLUTION_SHAPE = {
15340
+ proposed: { mark: "\u25CB", colour: ANSI.amber, label: "proposed" },
15341
+ accepted: { mark: "\u25D0", colour: ANSI.green, label: "accepted" },
15342
+ building: { mark: "\u25D4", colour: ANSI.blue, label: "building" },
15343
+ done: { mark: "\u25CF", colour: ANSI.green, label: "done" },
15344
+ rejected: { mark: "\u2298", colour: ANSI.red, label: "rejected" },
15345
+ dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
15346
+ };
15347
+ var PROBLEM_SHAPE = {
15348
+ candidate: { mark: "\u25CB", colour: ANSI.dim },
15349
+ confirmed: { mark: "\u25C9", colour: ANSI.amber },
15350
+ addressed: { mark: "\u25CF", colour: ANSI.green },
15351
+ dropped: { mark: "\u2298", colour: ANSI.dim }
15352
+ };
15353
+ var NO_AREA = "no area yet";
15354
+ var SHOWN = /* @__PURE__ */ new Set(["proposed", "accepted", "building", "done"]);
15355
+ function renderTree(problems2, solutions, strategy, opts) {
15356
+ const paint = painter(opts.color);
15357
+ const out = [];
15358
+ const width = Math.max(40, columns());
15359
+ if (strategy) {
15360
+ out.push(paint(ANSI.bold, "Strategy"));
15361
+ for (const line of wrap(strategy, width - 4)) out.push(` ${line}`);
15362
+ out.push("");
15363
+ }
15364
+ const shown = solutions.filter((s) => SHOWN.has(s.status));
15365
+ const open = problems2.filter((p) => p.status !== "dropped" && (!opts.alongside || shown.some((s) => s.problem_id === p.id)));
15366
+ if (opts.alongside && !open.length) return "";
15367
+ if (!open.length && !shown.length) {
15368
+ out.push("No problems yet. `nanopm opportunities` shows what to work on.");
15369
+ return out.join("\n");
15370
+ }
15371
+ const byProblem = /* @__PURE__ */ new Map();
15372
+ for (const s of shown) byProblem.set(s.problem_id, [...byProblem.get(s.problem_id) ?? [], s]);
15373
+ const refWidth = Math.max(...[...open.map((p) => p.ref), ...shown.map((s) => s.ref)].map((r) => r.length), 5);
15374
+ for (const [area, rows2] of groupByArea(open)) {
15375
+ out.push(paint(ANSI.bold, area));
15376
+ for (const p of rows2) {
15377
+ const shape2 = PROBLEM_SHAPE[p.status] ?? { mark: "\xB7", colour: "" };
15378
+ const kind = p.kind === "founder" ? paint(ANSI.dim, " (founder)") : "";
15379
+ const proof = p.proofs ? `${p.proofs} proof${p.proofs === 1 ? "" : "s"}` : "no proof";
15380
+ const tail = `${renderImportance(p, opts)} ${paint(ANSI.dim, proof)}`;
15381
+ const head = ` ${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, p.ref.padEnd(refWidth))} `;
15382
+ out.push(head + clip(p.statement, Math.max(30, width - visibleLength(head) - visibleLength(tail) - 2)) + kind + " " + tail);
15383
+ const under = byProblem.get(p.id) ?? [];
15384
+ if (!under.length) {
15385
+ out.push(paint(ANSI.dim, ` ${" ".repeat(refWidth + 2)} no solution yet`));
15386
+ continue;
15387
+ }
15388
+ for (const s of under) out.push(...solutionLines(s, refWidth, width, paint));
15389
+ }
15390
+ out.push("");
15391
+ }
15392
+ const waiting = shown.filter((s) => s.status === "proposed").length;
15393
+ if (waiting) {
15394
+ out.push(`${waiting} waiting on you.`);
15395
+ out.push(paint(ANSI.dim, ' nanopm accept <ref> \xB7 nanopm reject <ref> "why" \xB7 nanopm roadmap <ref> \xB7 nanopm roadmap --board'));
15396
+ } else if (!opts.alongside) {
15397
+ out.push(paint(ANSI.dim, "Nothing waiting on you."));
15398
+ }
15399
+ return out.join("\n").trimEnd();
15343
15400
  }
15344
- async function rewordOpportunity(ref, problem, opts) {
15345
- const text2 = problem?.trim().replace(/\s+/g, " ") ?? "";
15346
- if (!text2) throw new Error('say it: nanopm opportunities reword <ref> "the problem, in your words"');
15347
- if (text2.length > PROBLEM_MAX) throw new Error(`one line, under ${PROBLEM_MAX} characters`);
15348
- await rpc2("restate_opportunity", { p_ref: ref, p_problem: text2 }, opts);
15349
- return `Kept in your words: ${text2}`;
15401
+ function solutionLines(s, refWidth, width, paint, indent = 6) {
15402
+ const shape2 = SOLUTION_SHAPE[s.status] ?? { mark: "\xB7", colour: "", label: s.status };
15403
+ const pad = " ".repeat(indent);
15404
+ const STATE = { accepted: "chosen", building: "building", done: "done" };
15405
+ const tail = s.actor === "founder" ? "yours to do" : s.outcome ? s.outcome : s.status === "proposed" ? s.assumption ? clip(s.assumption, 40) : null : STATE[s.status] ?? null;
15406
+ const badge = tail ? paint(ANSI.dim, ` ${tail}`) : "";
15407
+ const head = `${pad}${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, s.ref.padEnd(refWidth))} `;
15408
+ const lines = [head + clip(s.title, Math.max(30, width - visibleLength(head) - visibleLength(badge))) + badge];
15409
+ const reason2 = `${pad} ${" ".repeat(refWidth)} `;
15410
+ lines.push(paint(ANSI.dim, reason2 + clip(firstSentence(s.reasoning), Math.max(30, width - reason2.length))));
15411
+ return lines;
15350
15412
  }
15351
- var AWAKE_MS = 12e4;
15352
- async function awakeDaemon(client, userId) {
15353
- const since = new Date(Date.now() - AWAKE_MS).toISOString();
15354
- const [{ data }, local] = await Promise.all([
15355
- client.from("daemons").select("id, hostname").eq("user_id", userId).in("status", ["online", "running"]).gte("last_seen_at", since).order("last_seen_at", { ascending: false }),
15356
- localDaemonId()
15357
- ]);
15358
- return data?.find((d) => d.id === local) ?? data?.[0] ?? null;
15413
+ function groupByArea(problems2) {
15414
+ const groups = /* @__PURE__ */ new Map();
15415
+ for (const p of problems2) {
15416
+ const area = p.area ?? NO_AREA;
15417
+ groups.set(area, [...groups.get(area) ?? [], p]);
15418
+ }
15419
+ const named = [...groups.entries()].filter(([a]) => a !== NO_AREA);
15420
+ const rest = groups.get(NO_AREA);
15421
+ return rest ? [...named, [NO_AREA, rest]] : named;
15359
15422
  }
15360
- async function brainstorm(ref, opts) {
15361
- const auth = await authenticate();
15362
- const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
15363
- const [lens, all, busy] = await Promise.all([lensOf2(auth.client, project.id), loadOpportunities(auth.client, project.id), workingOn(auth.client, project.id)]);
15364
- const deep = Boolean(opts.deep);
15365
- const plan = brainstormPlan(shownMap(all, lens?.key ?? null).shown, ref, deep, new Set(busy.keys()));
15366
- const daemon = await awakeDaemon(auth.client, auth.userId);
15367
- 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 brainstorm again.");
15368
- const repo = deep ? await detectRepo(opts.cwd) : null;
15369
- if (repo) await rememberClone(auth.client, project.id, repo.root);
15370
- for (const params of plan.jobs) {
15371
- const { error } = await auth.client.from("jobs").insert({ project_id: project.id, skill: SOLUTIONS_SKILL, params: { ...params, ...repo ? { repo_path: repo.root } : {} }, requested_by: auth.userId, trigger: "user", target_daemon_id: daemon.id });
15372
- if (error) throw new Error(`could not queue the brainstorm: ${error.message}`);
15423
+ var BOARD_COLUMNS = [
15424
+ { status: "proposed", label: "Proposed" },
15425
+ { status: "accepted", label: "Chosen" },
15426
+ { status: "building", label: "Building" },
15427
+ { status: "done", label: "Done" }
15428
+ ];
15429
+ function renderBoard(problems2, solutions, opts) {
15430
+ const paint = painter(opts.color);
15431
+ const out = [];
15432
+ const byId = new Map(problems2.map((p) => [p.id, p]));
15433
+ const width = Math.max(40, columns());
15434
+ const refWidth = Math.max(...solutions.map((s) => s.ref.length), 5);
15435
+ for (const col of BOARD_COLUMNS) {
15436
+ const rows2 = solutions.filter((s) => s.status === col.status);
15437
+ out.push(`${paint(ANSI.bold, col.label)} ${paint(ANSI.dim, String(rows2.length))}`);
15438
+ if (!rows2.length) out.push(paint(ANSI.dim, " \u2014"));
15439
+ for (const s of rows2) {
15440
+ const shape2 = SOLUTION_SHAPE[s.status];
15441
+ const problem = byId.get(s.problem_id);
15442
+ const head = ` ${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, s.ref.padEnd(refWidth))} `;
15443
+ out.push(head + clip(s.title, Math.max(30, width - visibleLength(head))));
15444
+ const under = [problem ? `${problem.ref} ${clip(problem.statement, 50)}` : "its problem was dropped", s.assumption ? clip(s.assumption, 40) : null].filter(Boolean).join(" \xB7 ");
15445
+ out.push(paint(ANSI.dim, ` ${" ".repeat(refWidth + 2)} ${under}`));
15446
+ }
15447
+ out.push("");
15373
15448
  }
15374
- const n = plan.leaves.length;
15375
- const minutes = deep ? DEEP_MINUTES * n : FLASH_MINUTES;
15376
- const what = plan.problem.children.length ? `the ${n} narrower problem${n === 1 ? "" : "s"} under ${plan.problem.ref}` : `${plan.problem.ref} \u201C${clip(plan.problem.problem, 60)}\u201D`;
15377
- const how = deep ? `${DEPTH_WORD.deep}${n > 1 ? ", one after the other" : ""}` : `${DEPTH_WORD.quick}, a first pass`;
15378
- const out = [`${deep ? "Going deeper on" : "Brainstorming"} ${what}: ${how}, about ${minutes} minutes on ${daemon.hostname}.`];
15379
- if (plan.problem.children.length) for (const l of plan.leaves) out.push(` ${l.ref.padEnd(8)} ${clip(l.problem, Math.max(30, columns() - 11))}`);
15380
- if (plan.busy) out.push(`${plan.busy} left out: Nano is already finding solutions to ${plan.busy === 1 ? "it" : "them"}.`);
15381
- out.push("The solutions land on `nanopm roadmap`.");
15382
- return out.join("\n");
15449
+ return out.join("\n").trimEnd();
15383
15450
  }
15384
15451
 
15385
15452
  // src/commands/roadmap.ts
@@ -15539,8 +15606,8 @@ async function decide(kind, ref, body, opts) {
15539
15606
  const row = Array.isArray(data) ? data[0] : void 0;
15540
15607
  if (!row) throw new Error(`nothing changed for ${ref}`);
15541
15608
  if (onMap) {
15542
- const next2 = card?.others[0];
15543
- const after = !card ? "" : next2 ? ` ${next2.ref} \u201C${next2.title}\u201D takes its card in ${HORIZON_WORD[card.horizon]}.` : ` Its card leaves the board: nothing else answers ${card.leaf.ref} yet. \`nanopm opportunities brainstorm ${card.leaf.ref}\` finds more.`;
15609
+ const next = card?.others[0];
15610
+ const after = !card ? "" : next ? ` ${next.ref} \u201C${next.title}\u201D takes its card in ${HORIZON_WORD[card.horizon]}.` : ` Its card leaves the board: nothing else answers ${card.leaf.ref} yet. \`nanopm opportunities brainstorm ${card.leaf.ref}\` finds more.`;
15544
15611
  return `Thrown out: ${row.ref}${body?.trim() ? ` \u2014 ${body.trim()}` : ""}.${after}`;
15545
15612
  }
15546
15613
  const verb = kind === "accept" ? "Accepted" : "Rejected";
@@ -15557,123 +15624,36 @@ function normalizeRef(input, items) {
15557
15624
  return given;
15558
15625
  }
15559
15626
 
15560
- // src/commands/next.ts
15561
- async function next(opts) {
15627
+ // src/commands/build.ts
15628
+ async function build(ref, opts) {
15562
15629
  const { renderer, out } = rendererFor(opts.out);
15563
15630
  const auth = await authenticate();
15564
15631
  const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
15565
- const autonomy2 = await readAutonomy(auth.client, project.id);
15566
- if (autonomy2 === "propose") {
15567
- throw new Error(
15568
- `${project.name} is on "propose", so NanoPM may look but not touch.
15569
- nanopm autonomy execute_after_validation to let it change files, asking you first`
15570
- );
15571
- }
15572
- const { data: items, error } = await auth.client.from("solutions").select("id, ref, title, rank, status").eq("project_id", project.id).is("superseded_by", null).eq("status", "accepted").eq("actor", "nanopm").order("rank");
15573
- if (error) throw new Error(`could not read the solutions: ${error.message}`);
15574
- const accepted = items ?? [];
15575
- if (!accepted.length) {
15576
- throw new Error("nothing is accepted yet. Run `nanopm roadmap`, then `nanopm accept <ref>`.");
15577
- }
15578
- let item = accepted[0];
15579
- if (opts.ref) {
15580
- const wanted = normalizeRef(opts.ref, accepted);
15581
- const found = accepted.find((i) => i.ref === wanted);
15582
- if (!found) {
15583
- throw new Error(`${opts.ref} is not an accepted solution NanoPM can build. Accepted: ${accepted.map((i) => i.ref).join(", ")}.`);
15584
- }
15585
- item = found;
15586
- }
15587
15632
  const repo = await detectRepo(opts.cwd);
15588
- if (!repo) throw new Error("run this inside the repository, so NanoPM changes the clone you are looking at");
15633
+ 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
+ ]);
15639
+ 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.");
15589
15640
  await rememberClone(auth.client, project.id, repo.root);
15590
- const daemonId = await localDaemonId();
15591
- const { data: job, error: queueError } = await auth.client.from("jobs").insert({
15592
- project_id: project.id,
15593
- skill: "next",
15594
- params: { solution_id: item.id, repo_path: repo.root },
15595
- requested_by: auth.userId,
15596
- target_daemon_id: daemonId
15597
- }).select("id").single();
15598
- if (queueError || !job) throw new Error(`could not queue the work: ${queueError?.message}`);
15599
- out(`${item.ref} ${item.title}`);
15641
+ const { jobId, solution } = await queueBuild(auth.client, {
15642
+ projectId: project.id,
15643
+ userId: auth.userId,
15644
+ daemonId: local,
15645
+ ref: normalizeRef(ref, refs ?? []),
15646
+ repoPath: repo.root,
15647
+ anyway: opts.anyway
15648
+ });
15649
+ out(`${solution.ref} ${solution.title}`);
15600
15650
  const answerer = opts.answersFile ? await answersFromFile(opts.answersFile) : answersFromTerminal(out);
15601
- return streamJob(auth.client, job.id, { pollMs: 700, out, renderer, cancelSignal: opts.cancelSignal, answerer, verbose: verboseWanted() });
15602
- }
15603
- async function readAutonomy(client, projectId) {
15604
- const { data } = await client.from("project_preferences").select("autonomy").eq("project_id", projectId).maybeSingle();
15605
- return data?.autonomy ?? "propose";
15606
- }
15607
- var EXPLAINED = {
15608
- propose: "NanoPM reads and proposes. It never changes a file.",
15609
- execute_after_validation: "NanoPM changes files, on its own branch, after showing you what it will do and waiting for a yes.",
15610
- execute_then_report: "NanoPM acts first and tells you after. Not available yet."
15611
- };
15612
- async function autonomy(level, opts) {
15613
- const auth = await authenticate();
15614
- const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
15615
- const current = await readAutonomy(auth.client, project.id);
15616
- if (!level) {
15617
- const lines = [`${project.name} is on "${current}".`, ""];
15618
- for (const l of AUTONOMY_LEVELS) {
15619
- lines.push(` ${l === current ? "\u25CF" : "\u25CB"} ${l.padEnd(26)} ${EXPLAINED[l]}`);
15620
- }
15621
- lines.push("", " nanopm autonomy <level>");
15622
- return lines.join("\n");
15623
- }
15624
- const wanted = level.trim();
15625
- if (!AUTONOMY_LEVELS.includes(wanted)) {
15626
- throw new Error(`"${level}" is not a level. Pick one of: ${AUTONOMY_LEVELS.join(", ")}.`);
15627
- }
15628
- if (wanted === "execute_then_report") {
15629
- throw new Error(
15630
- "execute_then_report is not available yet. NanoPM asks before it changes anything until the review loop has been used in anger."
15631
- );
15632
- }
15633
- if (wanted === current) return `${project.name} is already on "${current}".`;
15634
- const { error } = await auth.client.from("project_preferences").update({ autonomy: wanted }).eq("project_id", project.id);
15635
- if (error) {
15636
- throw new Error(`could not change the dial: ${error.message}`);
15637
- }
15638
- return [`${project.name} is now on "${wanted}".`, EXPLAINED[wanted]].join("\n");
15639
- }
15640
- async function verifyCommand(command, opts) {
15641
- const auth = await authenticate();
15642
- const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
15643
- const { data } = await auth.client.from("project_preferences").select("verify_command").eq("project_id", project.id).maybeSingle();
15644
- const current = data?.verify_command ?? null;
15645
- if (!command && !opts.clear) {
15646
- if (!current) {
15647
- return [
15648
- `${project.name} has no check, so nothing verifies what NanoPM writes.`,
15649
- "",
15650
- ' nanopm verify "npm run lint && npm test"',
15651
- "",
15652
- "The daemon runs it in the repository root after a change and before the solution is",
15653
- "marked done. A failure goes back to NanoPM once, to fix."
15654
- ].join("\n");
15655
- }
15656
- return [`${project.name} verifies with:`, "", ` ${current}`, "", ' nanopm verify "<command>" \xB7 nanopm verify --clear'].join("\n");
15657
- }
15658
- const next2 = opts.clear ? null : command.trim();
15659
- if (next2 !== null && !next2) throw new Error('a check needs a command: nanopm verify "npm test"');
15660
- const { error } = await auth.client.from("project_preferences").update({ verify_command: next2 }).eq("project_id", project.id);
15661
- if (error) throw new Error(`could not save the check: ${error.message}`);
15662
- if (next2 === null) return `${project.name} no longer runs a check. Nothing will verify what NanoPM writes.`;
15663
- return [
15664
- `${project.name} will verify with:`,
15665
- "",
15666
- ` ${next2}`,
15667
- "",
15668
- "It runs in the repository root. If your change touches what the command itself runs \u2014",
15669
- "package.json, a lockfile, the script it calls \u2014 the check is skipped and you are told,",
15670
- "because an agent that can rewrite its own examiner is not being examined."
15671
- ].join("\n");
15651
+ return streamJob(auth.client, jobId, { pollMs: 700, out, renderer, cancelSignal: opts.cancelSignal, answerer, verbose: verboseWanted() });
15672
15652
  }
15673
15653
 
15674
15654
  // src/detect-sources.ts
15675
- import { readFile as readFile12 } from "fs/promises";
15676
- import { join as join14 } from "path";
15655
+ import { readFile as readFile14 } from "fs/promises";
15656
+ import { join as join16 } from "path";
15677
15657
  var ENV_FILES = [".env", ".env.local", ".env.development.local", ".env.development", ".env.example"];
15678
15658
  var POSTGRES_KEYS = [
15679
15659
  "DATABASE_URL",
@@ -15723,7 +15703,7 @@ async function envFiles(repoRoot) {
15723
15703
  const out = [];
15724
15704
  for (const file of ENV_FILES) {
15725
15705
  try {
15726
- out.push({ file, env: parseEnv(await readFile12(join14(repoRoot, file), "utf8")) });
15706
+ out.push({ file, env: parseEnv(await readFile14(join16(repoRoot, file), "utf8")) });
15727
15707
  } catch {
15728
15708
  continue;
15729
15709
  }
@@ -15783,7 +15763,7 @@ function describeUrl(url) {
15783
15763
  // src/format/closing.ts
15784
15764
  function closingLines(project, exit, env = process.env, mode = "onboard") {
15785
15765
  if (exit !== 0) return [];
15786
- const build = mode === "start" ? `nanopm accept <ref> takes an option; when one needs code, in its repo: nanopm onboard --project ${project.id ?? "<id>"} attaches it, then nanopm next builds it` : "nanopm accept <ref> takes an option, nanopm next builds it";
15766
+ const build2 = mode === "start" ? `nanopm accept <ref> takes an option; when one needs code, in its repo: nanopm onboard --project ${project.id ?? "<id>"} attaches it, then nanopm build <ref> builds it` : "nanopm spec <ref> --write writes its specs, nanopm build <ref> builds it";
15787
15767
  const chain = chainedAfter(mode).map((skill) => skillSentence(skill));
15788
15768
  return [
15789
15769
  "",
@@ -15791,7 +15771,7 @@ function closingLines(project, exit, env = process.env, mode = "onboard") {
15791
15771
  ...chain.length ? [`NanoPM is now ${joinAnd(chain)}; the Roadmap fills in as it goes. Watch it at ${webUrl(env)}/p/${project.slug}/journal`] : [],
15792
15772
  // The map is no longer chained after onboarding, so the first three clauses are what
15793
15773
  // builds it: what users said, where they leave traces, then the map that reads both.
15794
- `Next: nanopm add <file> adds what users said \xB7 nanopm connect adds where they leave traces \xB7 nanopm opportunities run ranks what to work on \xB7 ${build} \xB7 nanopm schedule weekly monday 7 sets the review`,
15774
+ `Next: nanopm add <file> adds what users said \xB7 nanopm connect adds where they leave traces \xB7 nanopm opportunities run ranks what to work on \xB7 ${build2} \xB7 nanopm schedule weekly monday 7 sets the review`,
15795
15775
  "With a partner: nanopm invite <email> prints a link; they open it, or in their clone: nanopm login, then nanopm join <code>"
15796
15776
  ];
15797
15777
  }
@@ -15937,7 +15917,9 @@ async function spec(ref, opts) {
15937
15917
  const wanted = ref.trim().toUpperCase();
15938
15918
  const { data: solution } = await auth.client.from("solutions").select("id, ref, title, agent_spec").eq("project_id", project.id).eq("ref", wanted).maybeSingle();
15939
15919
  if (!solution) throw new Error(`no solution ${wanted}. Run \`nanopm roadmap\` to see them.`);
15940
- if (!opts.write) {
15920
+ if (opts.write && opts.rework) throw new Error("--write or --rework, not both.");
15921
+ const mode = opts.rework ? "rework" : opts.write ? "write" : null;
15922
+ if (!mode) {
15941
15923
  if (!solution.agent_spec) return `${solution.ref} has no specs yet: \`nanopm spec ${solution.ref} --write\`, or *Write the specs* on its sheet.`;
15942
15924
  return solution.agent_spec.trimEnd();
15943
15925
  }
@@ -15945,11 +15927,9 @@ async function spec(ref, opts) {
15945
15927
  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.");
15946
15928
  const repo = await detectRepo(opts.cwd);
15947
15929
  if (repo) await rememberClone(auth.client, project.id, repo.root);
15948
- await queueSpecs(auth.client, { projectId: project.id, userId: auth.userId, daemonId: daemon.id, ref: solution.ref });
15949
- return [
15950
- `Writing the specs of ${solution.ref} \u201C${clip(solution.title, 60)}\u201D: the brief and the agent spec, about ${SPEC_MINUTES} minutes on ${daemon.hostname}${repo ? ", reading this code" : ""}.`,
15951
- `They land on its sheet; \`nanopm spec ${solution.ref}\` prints the agent spec.`
15952
- ].join("\n");
15930
+ await queueSpecs(auth.client, { projectId: project.id, userId: auth.userId, daemonId: daemon.id, ref: solution.ref, mode });
15931
+ const where = `about ${SPEC_MINUTES} minutes on ${daemon.hostname}${repo ? ", reading this code" : ""}`;
15932
+ return mode === "rework" ? [`Reworking ${solution.ref} \u201C${clip(solution.title, 60)}\u201D: the whole solution and its agent spec, ${where}.`, "The new version takes its card on the Roadmap; `nanopm roadmap` names it."].join("\n") : [`Writing the specs of ${solution.ref} \u201C${clip(solution.title, 60)}\u201D: what it lacks and the agent spec, ${where}.`, `They land on its sheet; \`nanopm spec ${solution.ref}\` prints the agent spec.`].join("\n");
15953
15933
  }
15954
15934
 
15955
15935
  // src/commands/want.ts
@@ -16492,7 +16472,7 @@ async function seedWatches(watches2, read6) {
16492
16472
  }
16493
16473
 
16494
16474
  // src/commands/watch.ts
16495
- import { readFile as readFile13 } from "fs/promises";
16475
+ import { readFile as readFile15 } from "fs/promises";
16496
16476
  function cursorFor(source, watchId) {
16497
16477
  const all = source.cursor ?? {};
16498
16478
  return all[watchId] ?? {};
@@ -16553,7 +16533,7 @@ async function addWatch(labelOrId, id, opts) {
16553
16533
  const paint = painter(color);
16554
16534
  if (!labelOrId || !id) throw new Error('say which feed and what to call it: nanopm watch add <source> <id> --query "\u2026"');
16555
16535
  if (opts.query && opts.queryFile) throw new Error("--query or --query-file, not both");
16556
- const query = opts.queryFile ? await readFile13(opts.queryFile, "utf8") : opts.query ?? "";
16536
+ const query = opts.queryFile ? await readFile15(opts.queryFile, "utf8") : opts.query ?? "";
16557
16537
  const watch = watchInput(id, query, opts);
16558
16538
  const auth = await authenticate();
16559
16539
  const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
@@ -16740,9 +16720,9 @@ async function review(opts) {
16740
16720
  }
16741
16721
  const usable = connections.filter((c) => c.status === "ok");
16742
16722
  if (!usable.length) {
16743
- const failed = connections.map((c) => c.label).join(", ");
16723
+ const failed2 = connections.map((c) => c.label).join(", ");
16744
16724
  throw new Error(
16745
- `no connection is working (${failed}), so a review would have nothing to read.
16725
+ `no connection is working (${failed2}), so a review would have nothing to read.
16746
16726
  nanopm connect --list see what is wrong`
16747
16727
  );
16748
16728
  }
@@ -17377,12 +17357,12 @@ function renderDirection(rows2) {
17377
17357
  const out = [];
17378
17358
  for (const line of DIRECTION_ROWS) {
17379
17359
  const row = rows2.find((r) => r.key === directionKey(line));
17380
- const next2 = rows2.find((r) => r.key === directionKey(line, true));
17360
+ const next = rows2.find((r) => r.key === directionKey(line, true));
17381
17361
  out.push(`${DIRECTION_ROW_TITLE[line]}${mark(row)}`);
17382
17362
  out.push(` ${row?.content ?? "\u2014"}`);
17383
- if (next2) {
17384
- const because = reasonOf2(next2.sources);
17385
- out.push(` proposed: ${next2.content}${because ? ` \xB7 ${because}` : ""}`);
17363
+ if (next) {
17364
+ const because = reasonOf2(next.sources);
17365
+ out.push(` proposed: ${next.content}${because ? ` \xB7 ${because}` : ""}`);
17386
17366
  }
17387
17367
  }
17388
17368
  out.push("Objective");
@@ -17422,13 +17402,13 @@ function render2(rows2) {
17422
17402
  const out = [];
17423
17403
  for (const line of PITCH_LINE_KEYS) {
17424
17404
  const row = rows2.find((r) => r.key === pitchKey(line));
17425
- const next2 = rows2.find((r) => r.key === pitchKey(line, true));
17405
+ const next = rows2.find((r) => r.key === pitchKey(line, true));
17426
17406
  const mark2 = row && row.provenance !== "stated" ? ` \xB7 to review${row.confidence != null ? ` (${row.confidence}% sure)` : ""}` : "";
17427
17407
  out.push(`${PITCH_LINE_TITLE[line]}${mark2}`);
17428
17408
  out.push(` ${row?.content ?? "\u2014"}`);
17429
- if (next2) {
17430
- const because = reasonOf3(next2.sources);
17431
- out.push(` proposed: ${next2.content}${because ? ` \xB7 ${because}` : ""}`);
17409
+ if (next) {
17410
+ const because = reasonOf3(next.sources);
17411
+ out.push(` proposed: ${next.content}${because ? ` \xB7 ${because}` : ""}`);
17432
17412
  }
17433
17413
  }
17434
17414
  return out.join("\n");
@@ -17486,7 +17466,7 @@ async function invite(email, opts) {
17486
17466
  const [, ...rest] = inviteLines({ name: project.name, code, email: email.trim().toLowerCase(), web: webUrl() });
17487
17467
  return [`Invite to ${paint(ANSI.bold, project.name)}: ${paint(ANSI.bold, code)}`, ...rest.map((l) => l ? paint(ANSI.dim, l) : l)].join("\n");
17488
17468
  }
17489
- async function join15(code, opts) {
17469
+ async function join17(code, opts) {
17490
17470
  const normalized2 = code.trim().toUpperCase();
17491
17471
  if (!INVITE_CODE.test(normalized2)) throw new Error(`that does not look like an invite code: ten letters and digits, like ${"K7PQ2XM9RT"}`);
17492
17472
  const repo = await detectRepo(opts.cwd);
@@ -17941,8 +17921,8 @@ function buildProgram() {
17941
17921
  }
17942
17922
  console.log(await roadmap(ref, { cwd: process.cwd(), projectId: opts.project, board: opts.board, color: colorOf(opts) }));
17943
17923
  });
17944
- program.command("spec <ref>").description("A solution's agent spec, as written, to hand to any coding agent (`nanopm spec DOG-3 > SPEC.md`); `--write` asks Nano for its specs \u2014 the solution brief and the agent spec \u2014 reading this code, read-only").option("--project <id>", "project id (default: the project of the current repo)").option("--write", "write the specs, or write them again").action(async (ref, opts) => {
17945
- console.log(await spec(ref, { cwd: process.cwd(), projectId: opts.project, write: opts.write }));
17924
+ program.command("spec <ref>").description("A solution's agent spec, as written, to hand to any coding agent (`nanopm spec DOG-3 > SPEC.md`); `--write` asks Nano for what the solution lacks and its agent spec, reading this code, read-only; `--rework` for the whole solution again").option("--project <id>", "project id (default: the project of the current repo)").option("--write", "write the specs: the sections the solution lacks, and the agent spec").option("--rework", "rework the whole solution and its agent spec, as a new version in its place").action(async (ref, opts) => {
17925
+ console.log(await spec(ref, { cwd: process.cwd(), projectId: opts.project, write: opts.write, rework: opts.rework }));
17946
17926
  });
17947
17927
  for (const [kind, help] of [
17948
17928
  ["accept", "Accept a solution; NanoPM builds it when you ask, or you do it and say so with 'nanopm done'"],
@@ -18187,7 +18167,7 @@ function buildProgram() {
18187
18167
  console.log(await invite(email, { cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
18188
18168
  });
18189
18169
  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) => {
18190
- console.log(await join15(code, { cwd: process.cwd(), color: colorOf(opts) }));
18170
+ console.log(await join17(code, { cwd: process.cwd(), color: colorOf(opts) }));
18191
18171
  });
18192
18172
  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) => {
18193
18173
  console.log(await members({ cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
@@ -18209,16 +18189,10 @@ function buildProgram() {
18209
18189
  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) => {
18210
18190
  console.log(await status({ cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
18211
18191
  });
18212
- program.command("next [ref]").description("Build an accepted solution, or the top one: a branch, a diff, and a pull request").option("--project <id>", "project id (default: the project of the current repo)").option("--ref <ref>", "same as the positional argument").option("--answers <file>", "answer its questions from a file instead of the terminal").action(async (ref, opts) => {
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) => {
18213
18193
  const cancel = new AbortController();
18214
18194
  process.once("SIGINT", () => cancel.abort());
18215
- process.exitCode = await next({
18216
- cwd: process.cwd(),
18217
- projectId: opts.project,
18218
- ref: ref ?? opts.ref,
18219
- answersFile: opts.answers,
18220
- cancelSignal: cancel.signal
18221
- });
18195
+ process.exitCode = await build(ref, { cwd: process.cwd(), projectId: opts.project, anyway: opts.anyway, answersFile: opts.answers, cancelSignal: cancel.signal });
18222
18196
  });
18223
18197
  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) => {
18224
18198
  console.log(await autonomy(level, { cwd: process.cwd(), projectId: opts.project }));