@nanopm/cli 0.1.10 → 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
  }
@@ -9438,32 +9642,32 @@ function specOutputSchema(role7) {
9438
9642
  }
9439
9643
 
9440
9644
  // ../daemon/src/git.ts
9441
- import { execFile as execFile4 } from "child_process";
9442
- import { promisify as promisify4 } from "util";
9443
- var execFileAsync4 = promisify4(execFile4);
9444
- 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;
9445
9649
  var WIKI_DIR = ".nanopm/wiki/docs";
9446
- async function git2(cwd, args) {
9447
- 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 });
9448
9652
  return stdout.trim();
9449
9653
  }
9450
9654
  async function listTrackedFiles(cwd) {
9451
- 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") : [], () => []);
9452
9656
  const [files, wiki] = await Promise.all([lines(["ls-files"]), lines(["ls-files", "--others", "--ignored", "--exclude-standard", "--", WIKI_DIR])]);
9453
9657
  return { files, wiki };
9454
9658
  }
9455
9659
  async function headCommit(cwd) {
9456
- 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);
9457
9661
  }
9458
9662
  async function collectGitDigest(cwd, recent = 30) {
9459
9663
  try {
9460
- await git2(cwd, ["rev-parse", "--git-dir"]);
9664
+ await git3(cwd, ["rev-parse", "--git-dir"]);
9461
9665
  } catch {
9462
9666
  return null;
9463
9667
  }
9464
9668
  const safe = async (args) => {
9465
9669
  try {
9466
- return await git2(cwd, args);
9670
+ return await git3(cwd, args);
9467
9671
  } catch {
9468
9672
  return "";
9469
9673
  }
@@ -9853,13 +10057,13 @@ var told = (r) => ({
9853
10057
  });
9854
10058
 
9855
10059
  // ../daemon/src/objective-definitions.ts
9856
- import { execFile as execFile5 } from "child_process";
10060
+ import { execFile as execFile6 } from "child_process";
9857
10061
  import { randomUUID as randomUUID11 } from "crypto";
9858
10062
  import { existsSync as existsSync3 } from "fs";
9859
- import { mkdir as mkdir3, readFile as readFile5 } from "fs/promises";
9860
- import { join as join9 } from "path";
9861
- import { promisify as promisify5 } from "util";
9862
- 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);
9863
10067
  var DEFINITIONS_TIMEOUT_MS = 9e4;
9864
10068
  var DEFINITIONS_THINKING_TOKENS = 2e3;
9865
10069
  var READ_TOOLS3 = /* @__PURE__ */ new Set(["Read", "Glob", "Grep", "StructuredOutput"]);
@@ -9868,7 +10072,7 @@ var COUNTING_LINES_MAX = 80;
9868
10072
  var PER_FILE = 4;
9869
10073
  async function countingLines(cwd) {
9870
10074
  try {
9871
- 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"], {
9872
10076
  cwd,
9873
10077
  maxBuffer: 16 * 1024 * 1024,
9874
10078
  timeout: 5e3
@@ -9893,7 +10097,7 @@ function pickLines(raw) {
9893
10097
  }
9894
10098
  async function killRecorded(runDir) {
9895
10099
  try {
9896
- const pid = Number((await readFile5(join9(runDir, "harness.pid"), "utf8")).trim());
10100
+ const pid = Number((await readFile6(join10(runDir, "harness.pid"), "utf8")).trim());
9897
10101
  if (pid > 1) process.kill(pid, "SIGKILL");
9898
10102
  } catch {
9899
10103
  }
@@ -9913,7 +10117,7 @@ async function readDefinitions({ adapter, spec: spec2, publish: publish5, journa
9913
10117
  if (!hasRepo) return give({}, "No repository to read: the card offers the common definitions.", "ok");
9914
10118
  try {
9915
10119
  const skill = await loadSkill("define-objective");
9916
- const runDir = join9(spec2.runDir, "definitions");
10120
+ const runDir = join10(spec2.runDir, "definitions");
9917
10121
  await mkdir3(runDir, { recursive: true, mode: 448 });
9918
10122
  const schema = definitionsOutputSchema();
9919
10123
  const [lines, listed] = await Promise.all([countingLines(spec2.cwd), listTrackedFiles(spec2.cwd)]);
@@ -9975,7 +10179,7 @@ ${renderTrackedFiles(listed.files, void 0, listed.wiki)}
9975
10179
  if (!result) throw new Error("no answer");
9976
10180
  const parsed = DefinitionsAnswer.safeParse(JSON.parse(result));
9977
10181
  if (!parsed.success) throw new Error("an answer in the wrong shape");
9978
- const checked = checkProposals(parsed.data, (file) => existsSync3(join9(spec2.cwd, file)));
10182
+ const checked = checkProposals(parsed.data, (file) => existsSync3(join10(spec2.cwd, file)));
9979
10183
  const definitions2 = lever ? checked[lever] ? { [lever]: checked[lever] } : {} : checked;
9980
10184
  const kept = Object.values(definitions2).reduce((n, list) => n + (list?.length ?? 0), 0);
9981
10185
  const read6 = Object.values(definitions2).flat().filter((d) => d?.found).length;
@@ -10249,9 +10453,9 @@ function localDate(timezone, at) {
10249
10453
  // ../daemon/src/run-job.ts
10250
10454
  import { randomUUID as randomUUID12 } from "crypto";
10251
10455
  import { existsSync as existsSync4 } from "fs";
10252
- import { mkdir as mkdir4, readFile as readFile6, rm as rm3 } from "fs/promises";
10253
- import { homedir as homedir3, tmpdir } from "os";
10254
- 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";
10255
10459
 
10256
10460
  // ../daemon/src/events.ts
10257
10461
  var JobEventWriter = class {
@@ -10331,195 +10535,166 @@ function lighten(value) {
10331
10535
  return value ?? null;
10332
10536
  }
10333
10537
 
10334
- // ../daemon/src/repo.ts
10335
- import { execFile as execFile6 } from "child_process";
10336
- import { promisify as promisify6 } from "util";
10337
- var execFileAsync6 = promisify6(execFile6);
10338
- var MAX_BUFFER2 = 4 * 1024 * 1024;
10339
- var TIMEOUT_MS = 6e4;
10340
- async function git3(cwd, args, timeoutMs = TIMEOUT_MS) {
10341
- const { stdout } = await execFileAsync6("git", ["-C", cwd, ...args], { maxBuffer: MAX_BUFFER2, timeout: timeoutMs });
10342
- return stdout.trim();
10343
- }
10344
- var RepoError = class extends Error {
10345
- };
10346
- function branchName(ref) {
10347
- const slug = ref.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
10348
- if (!slug) throw new RepoError(`"${ref}" has no letters or digits to make a branch name from`);
10349
- return `nanopm/${slug}`;
10350
- }
10351
- async function readState(cwd) {
10352
- try {
10353
- await git3(cwd, ["rev-parse", "--git-dir"]);
10354
- } catch {
10355
- throw new RepoError(`${cwd} is not a git repository`);
10356
- }
10357
- const [branch, head, status2, remoteUrl] = await Promise.all([
10358
- git3(cwd, ["branch", "--show-current"]).catch(() => ""),
10359
- git3(cwd, ["rev-parse", "HEAD"]).catch(() => ""),
10360
- git3(cwd, ["status", "--porcelain"]).catch(() => ""),
10361
- git3(cwd, ["remote", "get-url", "origin"]).catch(() => "")
10362
- ]);
10363
- const dirty = status2.split("\n").filter(Boolean).map((l) => l.slice(3));
10364
- return { branch, head, clean: dirty.length === 0, dirty, hasRemote: Boolean(remoteUrl), remoteUrl: remoteUrl || void 0 };
10365
- }
10366
- async function ensureCleanTree(cwd) {
10367
- const state = await readState(cwd);
10368
- if (!state.head) {
10369
- throw new RepoError("this repository has no commits yet; make the first commit, then run nanopm next");
10370
- }
10371
- if (!state.clean) {
10372
- const shown = state.dirty.slice(0, 5).join(", ");
10373
- const more = state.dirty.length > 5 ? `, and ${state.dirty.length - 5} more` : "";
10374
- throw new RepoError(
10375
- `the working tree has uncommitted changes (${shown}${more}). NanoPM commits only its own work, so commit or stash yours first.`
10376
- );
10377
- }
10378
- return state;
10379
- }
10380
- async function createBranch(cwd, name) {
10381
- const existing = await git3(cwd, ["branch", "--list", name]).catch(() => "");
10382
- if (existing.trim()) {
10383
- throw new RepoError(`branch ${name} already exists. Delete it or finish what is on it first.`);
10384
- }
10385
- await git3(cwd, ["checkout", "-b", name]);
10386
- }
10387
- async function commitAll(cwd, message) {
10388
- await git3(cwd, ["add", "-A"]);
10389
- const staged = await git3(cwd, ["diff", "--cached", "--name-only"]).catch(() => "");
10390
- if (!staged) return null;
10391
- await git3(cwd, ["commit", "-m", message, "-m", "Written by NanoPM. Review before merging."]);
10392
- const sha = await git3(cwd, ["rev-parse", "--short", "HEAD"]);
10393
- return { sha, ...await diffStat(cwd) };
10394
- }
10395
- async function diffStat(cwd) {
10396
- const numstat = await git3(cwd, ["show", "--numstat", "--format=", "HEAD"]).catch(() => "");
10397
- let files = 0;
10398
- let insertions = 0;
10399
- let deletions = 0;
10400
- for (const line of numstat.split("\n").filter(Boolean)) {
10401
- const [add2 = "", del = ""] = line.split(" ");
10402
- files += 1;
10403
- insertions += Number(add2) || 0;
10404
- deletions += Number(del) || 0;
10405
- }
10406
- return { files, insertions, deletions };
10407
- }
10408
- async function pushBranch(cwd, name) {
10409
- const state = await readState(cwd);
10410
- 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;
10411
10549
  try {
10412
- const { stderr } = await execFileAsync6("git", ["-C", cwd, "push", "--set-upstream", "origin", name], {
10413
- maxBuffer: MAX_BUFFER2,
10414
- timeout: 12e4
10415
- });
10416
- 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)) } };
10417
10560
  } catch (err) {
10418
- const text2 = errorMessage(err);
10419
- 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)) };
10420
10563
  }
10421
10564
  }
10422
- function reviewUrlFrom(pushOutput) {
10423
- const match = /https?:\/\/\S+/.exec(pushOutput);
10424
- return match?.[0]?.replace(/[).,]+$/, "");
10425
- }
10426
- async function checkout(cwd, ref) {
10427
- await git3(cwd, ["checkout", ref]);
10428
- }
10429
- async function deleteBranch(cwd, name) {
10430
- await git3(cwd, ["branch", "-D", name]).catch(() => "");
10431
- }
10432
- async function changedFiles(cwd) {
10433
- const status2 = await git3(cwd, ["status", "--porcelain"]).catch(() => "");
10434
- 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, ""));
10435
- }
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 ? `
10436
10607
 
10437
- // ../daemon/src/next-run.ts
10438
- async function loadAcceptedSolution(client, projectId, solutionId) {
10439
- 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();
10440
10645
  if (error) throw new Error(`could not read the solution: ${error.message}`);
10441
10646
  if (!data) throw new Error("that solution is not on this project any more");
10442
- if (data.status !== "accepted" && data.status !== "building") {
10443
- throw new Error(`${data.ref} is ${data.status}. Only a solution you accepted can be built.`);
10444
- }
10445
- return { ...data, sources: data.sources ?? [] };
10446
- }
10447
- async function startSolution(client, projectId, ref, jobId) {
10448
- const { error } = await client.rpc("start_solution", { p_project_id: projectId, p_ref: ref, p_job_id: jobId });
10449
- if (error && !/only an accepted solution/.test(error.message)) throw new Error(pgMessage(error));
10450
- }
10451
- async function unstartSolution(client, projectId, ref) {
10452
- await client.rpc("unstart_solution", { p_project_id: projectId, p_ref: ref });
10647
+ return data;
10453
10648
  }
10454
- function renderSolution(solution) {
10455
- const lines = [`${solution.ref} ${solution.title}`, "", `Why: ${solution.reasoning}`];
10456
- if (solution.assumption) lines.push(`Bets on: ${solution.assumption}`);
10457
- if (solution.eval) lines.push(`We'll know: ${solution.eval}`);
10458
- if (solution.sources.length) {
10459
- 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
+ `);
10460
10664
  }
10461
- return lines.join("\n");
10462
10665
  }
10463
- async function openRepoSession(cwd, ref) {
10464
- const state = await ensureCleanTree(cwd);
10465
- if (!state.branch) {
10466
- throw new RepoError("this repository has a detached HEAD; check out a branch first");
10467
- }
10468
- const branch = branchName(ref);
10469
- await createBranch(cwd, branch);
10470
- 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;
10471
10669
  }
10472
- async function closeRepoSession(cwd, session2, item) {
10473
- 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");
10474
10679
  }
10475
- async function abandonRepoSession(cwd, session2, item) {
10476
- 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;
10477
10683
  }
10478
- async function closeBranch(cwd, session2, message, { commitErrorThrows }) {
10479
- let commit;
10480
- try {
10481
- commit = await commitAll(cwd, message);
10482
- } catch (err) {
10483
- await checkout(cwd, session2.baseBranch).catch(() => void 0);
10484
- if (commitErrorThrows) throw err;
10485
- commit = null;
10486
- }
10487
- if (!commit) {
10488
- await checkout(cwd, session2.baseBranch).catch(() => void 0);
10489
- await deleteBranch(cwd, session2.branch);
10490
- return { branch: session2.branch, pushed: false, note: "nothing was changed, so no branch was kept" };
10491
- }
10492
- const push = await pushBranch(cwd, session2.branch);
10493
- await checkout(cwd, session2.baseBranch).catch(() => void 0);
10494
- return {
10495
- branch: session2.branch,
10496
- commit,
10497
- pushed: push.pushed,
10498
- reviewUrl: push.reviewUrl,
10499
- note: push.pushed ? void 0 : `not pushed: ${push.reason}`
10500
- };
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");
10501
10691
  }
10502
- async function finishSolution(client, job, solution, result) {
10503
- const sources2 = [{ kind: "branch", ref: result.branch }];
10504
- if (result.commit?.sha) sources2.push({ kind: "git", ref: result.commit.sha });
10505
- if (result.reviewUrl) sources2.push({ kind: "url", ref: result.reviewUrl });
10506
- sources2.push({ kind: "job", ref: job.id });
10507
- const { error } = await client.rpc("finish_solution", {
10508
- p_project_id: job.project_id,
10509
- p_ref: solution.ref,
10510
- p_job_id: job.id,
10511
- p_sources: sources2
10512
- });
10513
- if (error) throw new Error(`the work is committed on ${result.branch}, but recording it failed: ${error.message}`);
10514
- }
10515
- function renderRepoResult(result) {
10516
- if (!result.commit) return result.note ?? "nothing changed";
10517
- const { files, insertions, deletions } = result.commit;
10518
- const stat2 = `${files} file${files === 1 ? "" : "s"} changed, ${insertions} insertion${insertions === 1 ? "" : "s"}, ${deletions} deletion${deletions === 1 ? "" : "s"}`;
10519
- const lines = [`${result.branch} \xB7 ${result.commit.sha} \xB7 ${stat2}`];
10520
- if (result.reviewUrl) lines.push(result.reviewUrl);
10521
- else if (result.note) lines.push(result.note);
10522
- 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" });
10523
10698
  }
10524
10699
 
10525
10700
  // ../daemon/src/run-job.ts
@@ -10537,107 +10712,6 @@ async function askAboutSite(opts) {
10537
10712
  });
10538
10713
  }
10539
10714
 
10540
- // ../daemon/src/verify.ts
10541
- import { spawn as spawn3 } from "child_process";
10542
- var VERIFY_TIMEOUT_MS = 10 * 6e4;
10543
- var OUTPUT_TAIL = 4e3;
10544
- var DEFINES_THE_COMMAND = [
10545
- /(^|\/)package\.json$/,
10546
- /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb)$/,
10547
- /(^|\/)(Makefile|justfile|Justfile|Taskfile\.ya?ml)$/,
10548
- /(^|\/)\.husky\//,
10549
- /(^|\/)\.github\//,
10550
- /(^|\/)(pyproject\.toml|uv\.lock|Cargo\.toml|Cargo\.lock|Gemfile|Gemfile\.lock)$/
10551
- ];
10552
- function pathsNamedIn(command) {
10553
- return command.split(/[\s;&|()<>]+/).filter((t) => /[/.]/.test(t) && !/^-/.test(t) && t.length > 2).map((t) => t.replace(/^\.\//, "").replace(/^["']|["']$/g, ""));
10554
- }
10555
- function guardVerify(changed, command) {
10556
- for (const file of changed) {
10557
- const pattern = DEFINES_THE_COMMAND.find((p) => p.test(file));
10558
- if (pattern) {
10559
- return `the change touched ${file}, which decides what the verify command does, so the command was not run`;
10560
- }
10561
- }
10562
- const named = pathsNamedIn(command);
10563
- for (const file of changed) {
10564
- if (named.some((n) => file === n || file.endsWith(`/${n}`) || n.endsWith(file))) {
10565
- return `the change touched ${file}, which the verify command runs, so the command was not run`;
10566
- }
10567
- }
10568
- return null;
10569
- }
10570
- async function verify(opts) {
10571
- const skipped = guardVerify(opts.changed, opts.command);
10572
- if (skipped) return { ran: false, passed: false, skipped, output: "", durationMs: 0, timedOut: false };
10573
- return runCommand(opts);
10574
- }
10575
- async function runCommand(opts) {
10576
- const timeoutMs = opts.timeoutMs ?? VERIFY_TIMEOUT_MS;
10577
- const startedAt = Date.now();
10578
- const child = spawn3("sh", ["-c", opts.command], {
10579
- cwd: opts.cwd,
10580
- env: opts.env,
10581
- stdio: ["ignore", "pipe", "pipe"]
10582
- });
10583
- let output = "";
10584
- const collect6 = (chunk) => {
10585
- output += chunk.toString();
10586
- if (output.length > OUTPUT_TAIL * 4) output = output.slice(-OUTPUT_TAIL * 2);
10587
- };
10588
- child.stdout.on("data", collect6);
10589
- child.stderr.on("data", collect6);
10590
- let timedOut = false;
10591
- const timer = setTimeout(() => {
10592
- timedOut = true;
10593
- child.kill("SIGTERM");
10594
- setTimeout(() => child.kill("SIGKILL"), 5e3).unref();
10595
- }, timeoutMs);
10596
- const onAbort = () => child.kill("SIGTERM");
10597
- opts.signal?.addEventListener("abort", onAbort, { once: true });
10598
- const exitCode = await new Promise((resolve6) => {
10599
- child.on("close", (code) => resolve6(code));
10600
- child.on("error", () => resolve6(127));
10601
- });
10602
- clearTimeout(timer);
10603
- opts.signal?.removeEventListener("abort", onAbort);
10604
- return {
10605
- ran: true,
10606
- passed: exitCode === 0 && !timedOut,
10607
- output: tail(output, OUTPUT_TAIL),
10608
- exitCode: exitCode ?? void 0,
10609
- durationMs: Date.now() - startedAt,
10610
- timedOut
10611
- };
10612
- }
10613
- function tail(text2, max) {
10614
- const trimmed = text2.trimEnd();
10615
- if (trimmed.length <= max) return trimmed;
10616
- return `\u2026 ${trimmed.slice(-max)}`;
10617
- }
10618
- function renderVerifyResult(result, command) {
10619
- if (!result.ran) return result.skipped ?? "no verify command is set for this project";
10620
- const secs = (result.durationMs / 1e3).toFixed(0);
10621
- if (result.timedOut) return `the check NanoPM could not run itself, \`${command}\`, did not finish within ${secs}s and was stopped`;
10622
- 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`;
10623
- }
10624
- function retryPrompt(result, command) {
10625
- return [
10626
- `Your change does not pass this project's own check.`,
10627
- ``,
10628
- `The daemon ran \`${command}\` in the repository root and it failed with exit ${result.exitCode}.`,
10629
- `You did not run this and cannot run it; this output is the only evidence you get.`,
10630
- ``,
10631
- "```",
10632
- result.output || "(the command produced no output)",
10633
- "```",
10634
- ``,
10635
- `Fix what this reports, in the files you already changed where possible. Do not disable`,
10636
- `the check, do not edit what the command runs, and do not change unrelated code to make`,
10637
- `it pass. If the failure is not caused by your change, say so plainly and stop.`
10638
- ].join("\n");
10639
- }
10640
-
10641
10715
  // ../daemon/src/run-job.ts
10642
10716
  var QUICK_RUNS = { reword: runRewordJob, talk: runTalkJob };
10643
10717
  async function runJob(job, ctx) {
@@ -10689,8 +10763,8 @@ async function runJob(job, ctx) {
10689
10763
  return outcome;
10690
10764
  }
10691
10765
  async function execute(job, skill, ctx, events) {
10692
- const runDir = join10(nanopmHome(), "run", job.id);
10693
- await mkdir4(runDir, { recursive: true, mode: 448 });
10766
+ const runDir = join12(nanopmHome(), "run", job.id);
10767
+ await mkdir5(runDir, { recursive: true, mode: 448 });
10694
10768
  const resumeSessionId = job.resume_of ? await sessionOf(ctx.client, job.resume_of) : void 0;
10695
10769
  const sessionId = resumeSessionId ?? randomUUID12();
10696
10770
  await ctx.client.from("jobs").update({ harness_session_id: sessionId }).eq("id", job.id);
@@ -10734,14 +10808,14 @@ async function execute(job, skill, ctx, events) {
10734
10808
  clearTimeout(timer);
10735
10809
  clearInterval(heartbeat2);
10736
10810
  clearInterval(cancelPoll);
10737
- await rm3(runDir, { recursive: true, force: true });
10811
+ await rm4(runDir, { recursive: true, force: true });
10738
10812
  }
10739
10813
  }
10740
10814
  const quickRun2 = QUICK_RUNS[job.skill];
10741
10815
  if (quickRun2) {
10742
10816
  const cwd2 = scratchDir(job.id);
10743
10817
  try {
10744
- await mkdir4(cwd2, { recursive: true, mode: 448 });
10818
+ await mkdir5(cwd2, { recursive: true, mode: 448 });
10745
10819
  const caps = skill.manifest.capabilities;
10746
10820
  return await quickRun2({
10747
10821
  client: ctx.client,
@@ -10761,13 +10835,22 @@ async function execute(job, skill, ctx, events) {
10761
10835
  clearTimeout(timer);
10762
10836
  clearInterval(heartbeat2);
10763
10837
  clearInterval(cancelPoll);
10764
- await rm3(runDir, { recursive: true, force: true });
10765
- await rm3(cwd2, { recursive: true, force: true });
10838
+ await rm4(runDir, { recursive: true, force: true });
10839
+ await rm4(cwd2, { recursive: true, force: true });
10766
10840
  }
10767
10841
  }
10768
- const capabilities = capCapabilities(skill.manifest.capabilities, autonomy2);
10842
+ const capabilities = capCapabilities(skill.manifest.capabilities, autonomy2, { asked: skill.manifest.asked && job.requested_by === ctx.userId });
10769
10843
  const harnessWeb = await claudeSearches(searchProvider);
10770
- 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;
10771
10854
  const definitionsRead = job.skill === "onboard" && !job.resume_of ? readDefinitions({
10772
10855
  adapter,
10773
10856
  spec: { cwd, runDir, systemPrompt: "", prompt: "", capabilities: [], mcp: [], maxTurns: 1, sessionId, env: harnessEnv(timeoutMs), signal: abort.signal },
@@ -10791,29 +10874,6 @@ async function execute(job, skill, ctx, events) {
10791
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}`;
10792
10875
  await logActivity(ctx, job, "scan.site", site.missed.length ? `${read6} (${site.missed.length} not read)` : read6);
10793
10876
  }
10794
- let item;
10795
- let session2;
10796
- if (capabilities.includes("write_repo")) {
10797
- const solutionId = params.solution_id;
10798
- if (typeof solutionId !== "string" || !solutionId) {
10799
- clearTimeout(timer);
10800
- clearInterval(heartbeat2);
10801
- clearInterval(cancelPoll);
10802
- return { status: "failed", error: { code: "harness_error", message: "this job names no solution to build" } };
10803
- }
10804
- try {
10805
- item = await loadAcceptedSolution(ctx.client, job.project_id, solutionId);
10806
- session2 = await openRepoSession(cwd, item.ref);
10807
- await startSolution(ctx.client, job.project_id, item.ref, job.id);
10808
- params.item = renderSolution(item);
10809
- await logActivity(ctx, job, "repo.branch", `working on ${session2.branch}`);
10810
- } catch (err) {
10811
- clearTimeout(timer);
10812
- clearInterval(heartbeat2);
10813
- clearInterval(cancelPoll);
10814
- return { status: "failed", error: { code: "harness_error", message: errorMessage(err) } };
10815
- }
10816
- }
10817
10877
  const spec2 = {
10818
10878
  cwd,
10819
10879
  runDir,
@@ -10833,20 +10893,21 @@ async function execute(job, skill, ctx, events) {
10833
10893
  ].filter(Boolean).join("\n\n"),
10834
10894
  capabilities,
10835
10895
  harnessWeb,
10896
+ network: build2?.network,
10836
10897
  withoutTools: skill.manifest.withoutTools,
10837
10898
  // The workspace reaches the memory server only for a job that changes files; that is
10838
- // what lets `remove_files` refuse everywhere else.
10899
+ // what lets the build's tools refuse everywhere else.
10839
10900
  mcp: ctx.mcpServers?.(job, timeoutMs, capabilities.includes("write_repo") ? cwd : void 0) ?? [],
10840
10901
  model: skill.manifest.model,
10841
10902
  maxTurns: skill.manifest.maxTurns,
10842
10903
  sessionId,
10843
10904
  resumeSessionId,
10844
- 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),
10845
10907
  signal: abort.signal
10846
10908
  };
10847
10909
  const verifyCommand2 = project?.preferences?.verify_command?.trim() || null;
10848
10910
  let outcome;
10849
- let verified;
10850
10911
  let webCalls = 0;
10851
10912
  const onHarnessEvent = async (event) => {
10852
10913
  await events.harness(event);
@@ -10864,6 +10925,24 @@ async function execute(job, skill, ctx, events) {
10864
10925
  return result;
10865
10926
  };
10866
10927
  try {
10928
+ if (build2) {
10929
+ try {
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
+ }
10867
10946
  if (job.skill === "personas") {
10868
10947
  try {
10869
10948
  return await runPersonaJob({ client: ctx.client, job, adapter, spec: spec2, onEvent: onHarnessEvent });
@@ -10944,83 +11023,23 @@ async function execute(job, skill, ctx, events) {
10944
11023
  return { status: "failed", error: { code: timedOut ? "timeout" : "harness_error", message: errorMessage(error) } };
10945
11024
  }
10946
11025
  }
10947
- for (let attempt7 = 1; attempt7 <= 2; attempt7 += 1) {
10948
- const attemptSpec = attempt7 === 1 ? spec2 : { ...spec2, prompt: retryPrompt(verified, verifyCommand2), resumeSessionId: sessionId };
10949
- let attemptOutcome;
10950
- webCalls = 0;
10951
- for await (const event of adapter.run(attemptSpec)) {
10952
- await onHarnessEvent(event);
10953
- if (event.type === "finished") {
10954
- attemptOutcome = { status: "succeeded", result: event.result, usage: event.usage };
10955
- } else if (event.type === "failed") {
10956
- attemptOutcome = { status: "failed", error: { code: event.code, message: event.message } };
10957
- }
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 } };
10958
11032
  }
10959
- if (attemptOutcome) await noteIfItNeverSearched(attemptOutcome, capabilities.includes("read_web"));
10960
- outcome = mergeAttempts(outcome, attemptOutcome);
10961
- if (attemptOutcome?.status !== "succeeded" || !session2 || !verifyCommand2) break;
10962
- verified = await verify({
10963
- cwd,
10964
- command: verifyCommand2,
10965
- env: harnessEnv(timeoutMs),
10966
- changed: await changedFiles(cwd),
10967
- signal: abort.signal
10968
- });
10969
- await logActivity(ctx, job, "repo.verify", renderVerifyResult(verified, verifyCommand2), {
10970
- outcome: verified.passed ? "ok" : "error",
10971
- usage: { duration_ms: verified.durationMs }
10972
- });
10973
- await events.write("text", { text: renderVerifyResult(verified, verifyCommand2) });
10974
- if (verified.passed || !verified.ran || attempt7 === 2) break;
10975
11033
  }
11034
+ if (outcome) await noteIfItNeverSearched(outcome, capabilities.includes("read_web"));
10976
11035
  } finally {
10977
11036
  clearTimeout(timer);
10978
11037
  clearInterval(heartbeat2);
10979
11038
  clearInterval(cancelPoll);
10980
11039
  await definitionsRead;
10981
- await rm3(runDir, { recursive: true, force: true });
10982
- }
10983
- if (outcome?.status === "succeeded" && verified && verified.ran && !verified.passed) {
10984
- outcome = {
10985
- ...outcome,
10986
- status: "failed",
10987
- error: {
10988
- code: "harness_error",
10989
- message: `${renderVerifyResult(verified, verifyCommand2)}. The work is on the branch; the solution stays accepted.`
10990
- }
10991
- };
10992
- }
10993
- if (outcome?.status === "succeeded") {
10994
- if (!session2 || !item) return outcome;
10995
- try {
10996
- const repo = await closeRepoSession(cwd, session2, item);
10997
- await logActivity(ctx, job, "repo.commit", renderRepoResult(repo).split("\n")[0]);
10998
- if (!repo.commit) {
10999
- await unstartSolution(ctx.client, job.project_id, item.ref).catch(() => void 0);
11000
- return {
11001
- ...outcome,
11002
- status: "failed",
11003
- error: { code: "harness_error", message: "the skill finished without changing anything" }
11004
- };
11005
- }
11006
- await finishSolution(ctx.client, job, item, repo);
11007
- return { ...outcome, result: [outcome.result, "", renderRepoResult(repo)].filter((l) => l !== void 0).join("\n") };
11008
- } catch (err) {
11009
- return {
11010
- ...outcome,
11011
- status: "failed",
11012
- error: { code: "harness_error", message: errorMessage(err) }
11013
- };
11014
- }
11015
- }
11016
- if (session2) {
11017
- const kept = await abandonRepoSession(cwd, session2, verified?.ran && !verified.passed ? item : void 0).catch(() => void 0);
11018
- if (item) await unstartSolution(ctx.client, job.project_id, item.ref).catch(() => void 0);
11019
- if (kept?.commit) {
11020
- await logActivity(ctx, job, "repo.commit", renderRepoResult(kept).split("\n")[0], { outcome: "error" });
11021
- if (outcome) outcome = { ...outcome, result: [outcome.result, "", renderRepoResult(kept)].filter(Boolean).join("\n") };
11022
- }
11040
+ await rm4(runDir, { recursive: true, force: true });
11023
11041
  }
11042
+ if (outcome?.status === "succeeded") return outcome;
11024
11043
  if (cancelled) return { status: "cancelled" };
11025
11044
  if (timedOut) {
11026
11045
  return { status: "failed", error: { code: "timeout", message: `exceeded the ${skill.manifest.timeout} cap` } };
@@ -11031,10 +11050,6 @@ async function sessionOf(client, jobId) {
11031
11050
  const { data } = await client.from("jobs").select("harness_session_id").eq("id", jobId).maybeSingle();
11032
11051
  return data?.harness_session_id ?? void 0;
11033
11052
  }
11034
- function mergeAttempts(first, second) {
11035
- if (!first?.usage || !second?.usage) return second ?? first;
11036
- return { ...second, usage: sumUsage(first.usage, second.usage) };
11037
- }
11038
11053
  async function askForTheSite(ctx, job, project, events) {
11039
11054
  const started = Date.now();
11040
11055
  const search = await searchForSite(project.name);
@@ -11060,7 +11075,7 @@ function harnessWebFor(searchProvider, machineCanSearch) {
11060
11075
  }
11061
11076
  async function machineWebSearch() {
11062
11077
  try {
11063
- const parsed = JSON.parse(await readFile6(join10(nanopmHome(), "daemon.json"), "utf8"));
11078
+ const parsed = JSON.parse(await readFile8(join12(nanopmHome(), "daemon.json"), "utf8"));
11064
11079
  return parsed.webSearch === true;
11065
11080
  } catch {
11066
11081
  return false;
@@ -11102,7 +11117,7 @@ function buildHeader(job, project, cwd, harnessWeb = false, memory = false) {
11102
11117
  return lines.join("\n");
11103
11118
  }
11104
11119
  function scratchDir(jobId) {
11105
- return join10(tmpdir(), `nanopm-run-${jobId}`);
11120
+ return join12(tmpdir(), `nanopm-run-${jobId}`);
11106
11121
  }
11107
11122
  async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
11108
11123
  const refuse2 = (why) => {
@@ -11111,12 +11126,12 @@ async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
11111
11126
  const requested = params.repo_path;
11112
11127
  if (typeof requested !== "string" || !requested) {
11113
11128
  if (mustBeTheClone) refuse2("the job named no repository");
11114
- await mkdir4(fallback, { recursive: true, mode: 448 });
11129
+ await mkdir5(fallback, { recursive: true, mode: 448 });
11115
11130
  return fallback;
11116
11131
  }
11117
11132
  if (!job.project_id) {
11118
11133
  if (mustBeTheClone) refuse2("the job belongs to no project");
11119
- await mkdir4(fallback, { recursive: true, mode: 448 });
11134
+ await mkdir5(fallback, { recursive: true, mode: 448 });
11120
11135
  return fallback;
11121
11136
  }
11122
11137
  const { data: clone } = await ctx.client.from("clones").select("repo_path").eq("project_id", job.project_id).eq("daemon_id", ctx.daemonId).maybeSingle();
@@ -11124,12 +11139,12 @@ async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
11124
11139
  if (!allowed || resolve4(allowed) !== resolve4(requested)) {
11125
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`);
11126
11141
  console.error(`job ${job.id}: repo_path ${requested} is not this machine's clone; running in a scratch directory instead`);
11127
- await mkdir4(fallback, { recursive: true, mode: 448 });
11142
+ await mkdir5(fallback, { recursive: true, mode: 448 });
11128
11143
  return fallback;
11129
11144
  }
11130
11145
  if (existsSync4(allowed)) return resolve4(allowed);
11131
11146
  if (mustBeTheClone) refuse2(`${allowed} is not on this machine any more`);
11132
- await mkdir4(fallback, { recursive: true, mode: 448 });
11147
+ await mkdir5(fallback, { recursive: true, mode: 448 });
11133
11148
  return fallback;
11134
11149
  }
11135
11150
  function asData8(label, body) {
@@ -11138,10 +11153,10 @@ ${body}
11138
11153
  </${label}>`;
11139
11154
  }
11140
11155
  function harnessEnv(timeoutMs, source = process.env) {
11141
- const home = source.HOME ?? homedir3();
11156
+ const home = source.HOME ?? homedir4();
11142
11157
  const env = {
11143
11158
  HOME: home,
11144
- 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(":"),
11145
11160
  MCP_TOOL_TIMEOUT: String(timeoutMs)
11146
11161
  };
11147
11162
  for (const key of ["USER", "LANG", "TMPDIR", "SHELL", "ANTHROPIC_API_KEY", "CLAUDE_CONFIG_DIR"]) {
@@ -11318,12 +11333,12 @@ async function chainAfter(auth, job, outcome, log) {
11318
11333
  const parent = job.parent_job_id ? await parentOf(auth, job.parent_job_id) : null;
11319
11334
  const own = job.params ?? {};
11320
11335
  const deep = own.depth === "deep" && job.trigger === "hook";
11321
- const next2 = daemonQueues(job.skill, 0, parent?.skill ?? null, deep);
11322
- if (!next2.length) return;
11336
+ const next = daemonQueues(job.skill, 0, parent?.skill ?? null, deep);
11337
+ if (!next.length) return;
11323
11338
  if (await chainDepth(auth, job) >= CHAIN_DEPTH_MAX) return;
11324
11339
  const from = deep ? own.repo_path : parent?.params?.repo_path;
11325
11340
  const repo = typeof from === "string" ? { repo_path: from } : null;
11326
- for (const skill of next2) {
11341
+ for (const skill of next) {
11327
11342
  const params = { ...repo ?? {}, ...deep ? { depth: "deep", background: true } : {} };
11328
11343
  const { data, error } = await auth.client.from("jobs").insert({
11329
11344
  project_id: job.project_id,
@@ -11406,7 +11421,7 @@ var heldInThisProcess = false;
11406
11421
  function acquireLock() {
11407
11422
  if (heldInThisProcess) throw new Error("a nanopm daemon is already running in this process");
11408
11423
  mkdirSync(nanopmHome(), { recursive: true, mode: 448 });
11409
- const path = join11(nanopmHome(), "daemon.pid");
11424
+ const path = join13(nanopmHome(), "daemon.pid");
11410
11425
  for (; ; ) {
11411
11426
  try {
11412
11427
  const fd = openSync(path, "wx");
@@ -11493,13 +11508,13 @@ async function reclaimOwnJobs(client, daemonId) {
11493
11508
  async function harnessPids(runDir) {
11494
11509
  const dirs = [runDir];
11495
11510
  try {
11496
- 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));
11497
11512
  } catch {
11498
11513
  }
11499
11514
  const pids = [];
11500
11515
  for (const dir of dirs) {
11501
11516
  try {
11502
- const pid = Number((await readFile7(join11(dir, "harness.pid"), "utf8")).trim());
11517
+ const pid = Number((await readFile9(join13(dir, "harness.pid"), "utf8")).trim());
11503
11518
  if (pid > 1) pids.push(pid);
11504
11519
  } catch {
11505
11520
  }
@@ -11507,20 +11522,20 @@ async function harnessPids(runDir) {
11507
11522
  return pids;
11508
11523
  }
11509
11524
  async function killOrphanedHarness(jobId) {
11510
- const runDir = join11(nanopmHome(), "run", jobId);
11525
+ const runDir = join13(nanopmHome(), "run", jobId);
11511
11526
  for (const pid of await harnessPids(runDir)) {
11512
11527
  try {
11513
11528
  process.kill(pid, "SIGTERM");
11514
11529
  } catch {
11515
11530
  }
11516
11531
  }
11517
- await rm4(runDir, { recursive: true, force: true });
11532
+ await rm5(runDir, { recursive: true, force: true });
11518
11533
  }
11519
11534
  async function registerDaemon(auth, version, opts = {}) {
11520
- const path = join11(nanopmHome(), "daemon.json");
11535
+ const path = join13(nanopmHome(), "daemon.json");
11521
11536
  let identity = null;
11522
11537
  try {
11523
- identity = JSON.parse(await readFile7(path, "utf8"));
11538
+ identity = JSON.parse(await readFile9(path, "utf8"));
11524
11539
  } catch {
11525
11540
  identity = null;
11526
11541
  }
@@ -11558,8 +11573,8 @@ async function registerDaemon(auth, version, opts = {}) {
11558
11573
  return data.id;
11559
11574
  }
11560
11575
  async function rememberDaemon(id, shared = false) {
11561
- await mkdir5(nanopmHome(), { recursive: true, mode: 448 });
11562
- 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 });
11563
11578
  }
11564
11579
  async function heartbeat(client, daemonId) {
11565
11580
  await client.from("daemons").update({ status: "online", last_seen_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", daemonId);
@@ -11910,48 +11925,6 @@ function refuseFetch(url, allowlist, open = false) {
11910
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.`;
11911
11926
  }
11912
11927
 
11913
- // ../memory-mcp/src/repo-rules.ts
11914
- import { lstat, unlink } from "fs/promises";
11915
- import { relative as relative3, resolve as resolve5, sep as sep2 } from "path";
11916
- var REMOVE_MAX = 50;
11917
- function refuseRemove(workspace, requested) {
11918
- const root2 = resolve5(workspace);
11919
- const target = resolve5(root2, requested);
11920
- const rel = relative3(root2, target);
11921
- if (!rel || rel.startsWith("..") || rel.startsWith(`..${sep2}`)) return "outside the workspace";
11922
- if (rel === ".git" || rel.startsWith(`.git${sep2}`)) return "inside .git";
11923
- if (looksLikeASecret(rel)) return "looks like a secret, which the agent may neither read nor remove";
11924
- return null;
11925
- }
11926
- async function removeFiles(workspace, paths) {
11927
- const out = [];
11928
- for (const requested of paths.slice(0, REMOVE_MAX)) {
11929
- const refusal = refuseRemove(workspace, requested);
11930
- if (refusal) {
11931
- out.push({ path: requested, removed: false, reason: refusal });
11932
- continue;
11933
- }
11934
- const target = resolve5(workspace, requested);
11935
- try {
11936
- const stat2 = await lstat(target);
11937
- if (stat2.isSymbolicLink()) {
11938
- out.push({ path: requested, removed: false, reason: "a symlink; what it points at is not touched" });
11939
- continue;
11940
- }
11941
- if (stat2.isDirectory()) {
11942
- out.push({ path: requested, removed: false, reason: "a directory; name the files in it" });
11943
- continue;
11944
- }
11945
- await unlink(target);
11946
- out.push({ path: requested, removed: true });
11947
- } catch (err) {
11948
- const code = err.code;
11949
- out.push({ path: requested, removed: false, reason: code === "ENOENT" ? "not there" : err instanceof Error ? err.message : String(err) });
11950
- }
11951
- }
11952
- return out;
11953
- }
11954
-
11955
11928
  // ../memory-mcp/src/problem-rules.ts
11956
11929
  import { z as z20 } from "zod";
11957
11930
  var STATEMENT_MAX = 300;
@@ -12326,7 +12299,7 @@ ${renderMap()}`,
12326
12299
  }
12327
12300
  },
12328
12301
  async ({ ref, outcome, note }) => {
12329
- return json(await finishSolution2(ctx, ref, outcome, note));
12302
+ return json(await finishSolution(ctx, ref, outcome, note));
12330
12303
  }
12331
12304
  );
12332
12305
  server.registerTool(
@@ -12426,20 +12399,20 @@ ${renderMap()}`,
12426
12399
  }
12427
12400
  );
12428
12401
  server.registerTool(
12429
- "remove_files",
12402
+ "build_plan",
12430
12403
  {
12431
- 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.",
12432
- 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) }
12433
12406
  },
12434
- async ({ paths }) => {
12435
- if (!ctx.workspace) return failure("this job has no repository to remove files from");
12436
- const outcomes = await removeFiles(ctx.workspace, paths);
12437
- const removed = outcomes.filter((o) => o.removed).map((o) => o.path);
12438
- await logActivity2(ctx, "repo.remove", `removed ${removed.length} of ${paths.length} files${removed.length ? `: ${trim2(removed.join(", "), 200)}` : ""}`, {
12439
- outcome: removed.length === paths.length ? "ok" : "error"
12440
- });
12441
- return json({ outcomes, note: removed.length ? "Removed. The commit at the end of this job will record the deletions." : "Nothing was removed." });
12442
- }
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")
12443
12416
  );
12444
12417
  server.registerTool(
12445
12418
  "web_search",
@@ -13226,7 +13199,7 @@ async function markFast(ctx, problemRef) {
13226
13199
  await logActivity2(ctx, "problems.fast", `${problem.ref} is one small change`);
13227
13200
  return { problem: problem.ref, fast: true };
13228
13201
  }
13229
- async function finishSolution2(ctx, ref, outcome, note) {
13202
+ async function finishSolution(ctx, ref, outcome, note) {
13230
13203
  if (ctx.actor === "mapping-review") throw new Error("A targeted collection cannot decide solution outcomes.");
13231
13204
  const upper = ref.toUpperCase();
13232
13205
  const { data: current } = await ctx.client.from("solutions").select("status").eq("project_id", ctx.projectId).eq("ref", upper).is("superseded_by", null).maybeSingle();
@@ -13524,7 +13497,8 @@ var MEMORY_TOOL_NAMES = [
13524
13497
  "play_store_search",
13525
13498
  "list_market",
13526
13499
  "write_market",
13527
- "remove_files",
13500
+ "build_plan",
13501
+ "build_step",
13528
13502
  // Talk to Nano (docs/✅nanopm-68-talk-to-nano.md): the map read, and changed as the founder asked.
13529
13503
  ...TALK_TOOLS
13530
13504
  ];
@@ -13535,7 +13509,7 @@ async function startMemoryServer(env = process.env) {
13535
13509
  }
13536
13510
 
13537
13511
  // src/commands/add.ts
13538
- import { readFile as readFile9 } from "fs/promises";
13512
+ import { readFile as readFile11 } from "fs/promises";
13539
13513
 
13540
13514
  // src/project.ts
13541
13515
  import { execFile as execFile7 } from "child_process";
@@ -13543,11 +13517,11 @@ import { basename } from "path";
13543
13517
  import { promisify as promisify7 } from "util";
13544
13518
 
13545
13519
  // src/local.ts
13546
- import { readFile as readFile8 } from "fs/promises";
13547
- import { join as join12 } from "path";
13520
+ import { readFile as readFile10 } from "fs/promises";
13521
+ import { join as join14 } from "path";
13548
13522
  async function localDaemonId() {
13549
13523
  try {
13550
- const parsed = JSON.parse(await readFile8(join12(nanopmHome(), "daemon.json"), "utf8"));
13524
+ const parsed = JSON.parse(await readFile10(join14(nanopmHome(), "daemon.json"), "utf8"));
13551
13525
  return parsed.id ?? null;
13552
13526
  } catch {
13553
13527
  return null;
@@ -13682,7 +13656,7 @@ async function add(file, opts) {
13682
13656
  text2 = await readStdin();
13683
13657
  } else {
13684
13658
  try {
13685
- text2 = await readFile9(file, "utf8");
13659
+ text2 = await readFile11(file, "utf8");
13686
13660
  } catch {
13687
13661
  throw new Error(`could not read ${file}`);
13688
13662
  }
@@ -13697,7 +13671,7 @@ async function add(file, opts) {
13697
13671
  }
13698
13672
 
13699
13673
  // src/commands/run.ts
13700
- import { readFile as readFile10 } from "fs/promises";
13674
+ import { readFile as readFile12 } from "fs/promises";
13701
13675
  import { createInterface as createInterface2 } from "readline/promises";
13702
13676
  import { setTimeout as sleep3 } from "timers/promises";
13703
13677
 
@@ -13864,7 +13838,7 @@ function phaseOf(tool) {
13864
13838
  if (name === "query") return "data";
13865
13839
  if (name === "web_search" || name === "web_fetch") return "web";
13866
13840
  if (REVIEW_WRITES.has(name)) return "review";
13867
- if (name === "remove_files") return "build";
13841
+ if (name === "build_plan" || name === "build_step") return "build";
13868
13842
  return null;
13869
13843
  }
13870
13844
  function touched(tool, input) {
@@ -14029,7 +14003,7 @@ function questionBlock(p) {
14029
14003
 
14030
14004
  // src/commands/run.ts
14031
14005
  async function answersFromFile(path) {
14032
- const raw = JSON.parse(await readFile10(path, "utf8"));
14006
+ const raw = JSON.parse(await readFile12(path, "utf8"));
14033
14007
  const entries = raw.map((a) => typeof a === "string" ? { answer: a } : a);
14034
14008
  return scriptedAnswerer(entries);
14035
14009
  }
@@ -14252,15 +14226,15 @@ async function askOne(auth, projectId, question, daemonId, opts, out) {
14252
14226
  // src/commands/daemon-service.ts
14253
14227
  import { execFile as execFile8 } from "child_process";
14254
14228
  import { existsSync as existsSync5 } from "fs";
14255
- import { mkdir as mkdir6, readFile as readFile11, rm as rm5, writeFile as writeFile5 } from "fs/promises";
14256
- import { homedir as homedir4 } from "os";
14257
- 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";
14258
14232
  import { promisify as promisify8 } from "util";
14259
14233
 
14260
14234
  // package.json
14261
14235
  var package_default = {
14262
14236
  name: "@nanopm/cli",
14263
- version: "0.1.10",
14237
+ version: "0.1.11",
14264
14238
  description: "An autonomous product manager for builders who run several small products. Runs on your machine, drives your own coding harness.",
14265
14239
  type: "module",
14266
14240
  license: "MIT",
@@ -14312,10 +14286,10 @@ var VERSION = package_default.version;
14312
14286
  var execFileAsync8 = promisify8(execFile8);
14313
14287
  var LAUNCHD_LABEL = "pm.nano.daemon";
14314
14288
  function plistPath() {
14315
- return join13(homedir4(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
14289
+ return join15(homedir5(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
14316
14290
  }
14317
14291
  function daemonLogPath() {
14318
- return join13(nanopmHome(), "daemon.log");
14292
+ return join15(nanopmHome(), "daemon.log");
14319
14293
  }
14320
14294
  function renderPlist(opts) {
14321
14295
  const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -14374,13 +14348,13 @@ async function installDaemon(out = (l) => console.log(l), opts = {}) {
14374
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.`);
14375
14349
  const bin = process.argv[1];
14376
14350
  if (!bin) throw new Error("cannot locate the nanopm binary");
14377
- const home = homedir4();
14378
- const path = [process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", join13(home, ".local", "bin")].join(":");
14379
- await mkdir6(join13(home, "Library", "LaunchAgents"), { recursive: true });
14380
- 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 });
14381
14355
  if (opts.shared !== void 0) await rememberShared(opts.shared);
14382
14356
  await found({ ...detection, authenticated: true });
14383
- 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 });
14384
14358
  const target = guiTarget();
14385
14359
  await execFileAsync8("launchctl", ["bootout", target, plistPath()]).catch(() => void 0);
14386
14360
  await execFileAsync8("launchctl", ["bootstrap", target, plistPath()]);
@@ -14401,7 +14375,7 @@ async function uninstallDaemon(out = (l) => console.log(l)) {
14401
14375
  requireMac();
14402
14376
  const target = guiTarget();
14403
14377
  await execFileAsync8("launchctl", ["bootout", target, plistPath()]).catch(() => void 0);
14404
- await rm5(plistPath(), { force: true });
14378
+ await rm6(plistPath(), { force: true });
14405
14379
  out(`Removed ${LAUNCHD_LABEL}.`);
14406
14380
  }
14407
14381
  async function daemonStatus(out = (l) => console.log(l)) {
@@ -14428,7 +14402,7 @@ async function daemonStatus(out = (l) => console.log(l)) {
14428
14402
  }
14429
14403
  async function daemonLogs(lines = 50) {
14430
14404
  try {
14431
- const text2 = await readFile11(daemonLogPath(), "utf8");
14405
+ const text2 = await readFile13(daemonLogPath(), "utf8");
14432
14406
  return text2.trimEnd().split("\n").slice(-lines).join("\n");
14433
14407
  } catch {
14434
14408
  return `no log at ${daemonLogPath()} yet`;
@@ -14436,41 +14410,41 @@ async function daemonLogs(lines = 50) {
14436
14410
  }
14437
14411
  async function localWebSearch() {
14438
14412
  try {
14439
- const parsed = JSON.parse(await readFile11(join13(nanopmHome(), "daemon.json"), "utf8"));
14413
+ const parsed = JSON.parse(await readFile13(join15(nanopmHome(), "daemon.json"), "utf8"));
14440
14414
  return parsed.webSearch === true;
14441
14415
  } catch {
14442
14416
  return false;
14443
14417
  }
14444
14418
  }
14445
14419
  async function rememberWebSearch(webSearch2) {
14446
- const path = join13(nanopmHome(), "daemon.json");
14420
+ const path = join15(nanopmHome(), "daemon.json");
14447
14421
  let parsed = {};
14448
14422
  try {
14449
- parsed = JSON.parse(await readFile11(path, "utf8"));
14423
+ parsed = JSON.parse(await readFile13(path, "utf8"));
14450
14424
  } catch {
14451
14425
  parsed = {};
14452
14426
  }
14453
- await mkdir6(nanopmHome(), { recursive: true, mode: 448 });
14454
- 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 });
14455
14429
  }
14456
14430
  async function localShared() {
14457
14431
  try {
14458
- const parsed = JSON.parse(await readFile11(join13(nanopmHome(), "daemon.json"), "utf8"));
14432
+ const parsed = JSON.parse(await readFile13(join15(nanopmHome(), "daemon.json"), "utf8"));
14459
14433
  return Boolean(parsed.shared);
14460
14434
  } catch {
14461
14435
  return false;
14462
14436
  }
14463
14437
  }
14464
14438
  async function rememberShared(shared) {
14465
- const path = join13(nanopmHome(), "daemon.json");
14439
+ const path = join15(nanopmHome(), "daemon.json");
14466
14440
  let parsed = {};
14467
14441
  try {
14468
- parsed = JSON.parse(await readFile11(path, "utf8"));
14442
+ parsed = JSON.parse(await readFile13(path, "utf8"));
14469
14443
  } catch {
14470
14444
  parsed = {};
14471
14445
  }
14472
- await mkdir6(nanopmHome(), { recursive: true, mode: 448 });
14473
- 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 });
14474
14448
  }
14475
14449
 
14476
14450
  // src/format/log.ts
@@ -14713,702 +14687,766 @@ The install line does the same: curl -fsSL https://nanopm.app/install | sh`);
14713
14687
  out(await restartDaemon() ? `Updated ${VERSION} \u2192 ${latest}; the daemon restarted on it.` : `Updated ${VERSION} \u2192 ${latest}.`);
14714
14688
  }
14715
14689
 
14716
- // src/format/roadmap.ts
14717
- var SHAPE = {
14718
- proposed: { mark: "\u25CB", colour: ANSI.amber, label: "proposed" },
14719
- accepted: { mark: "\u25D0", colour: ANSI.green, label: "accepted" },
14720
- building: { mark: "\u25D4", colour: ANSI.blue, label: "building" },
14721
- rejected: { mark: "\u2298", colour: ANSI.red, label: "rejected" },
14722
- done: { mark: "\u25CF", colour: ANSI.green, label: "done" },
14723
- dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
14724
- };
14725
- var WORK_KINDS = /* @__PURE__ */ new Set(["branch", "git", "url", "job"]);
14726
- function renderRoadmapItem(item, opts, problem, builtBy) {
14727
- const paint = painter(opts.color);
14728
- const shape2 = SHAPE[item.status] ?? { mark: "\xB7", colour: "", label: item.status };
14729
- const out = [`${paint(shape2.colour, shape2.mark)} ${paint(ANSI.bold, item.ref)} ${item.title}`];
14730
- if (problem) out.push(paint(ANSI.dim, ` answers ${problem.ref} ${clip(problem.statement, Math.max(30, columns() - 20))}`));
14731
- out.push("");
14732
- for (const line of wrap(item.reasoning, Math.max(40, columns() - 4))) out.push(` ${line}`);
14733
- out.push("");
14734
- const facts = [
14735
- ["status", shape2.label + (item.outcome ? ` \xB7 ${item.outcome}${item.outcome_note ? `: ${item.outcome_note}` : ""}` : "")],
14736
- ["rank", String(item.rank)],
14737
- ["who", item.actor === "founder" ? "you" : "NanoPM"],
14738
- ["bets on", item.assumption ?? "\u2014"],
14739
- ["we'll know", item.eval ?? "\u2014"]
14740
- ];
14741
- for (const [k, v] of facts) out.push(paint(ANSI.dim, ` ${k.padEnd(9)} ${v}`));
14742
- const sources2 = item.sources ?? [];
14743
- const restsOn = sources2.filter((s) => !WORK_KINDS.has(s.kind));
14744
- if (restsOn.length) {
14745
- out.push(paint(ANSI.dim, ` ${"rests on".padEnd(9)} ${restsOn.map((s) => `${s.kind}:${s.ref}`).join(", ")}`));
14746
- }
14747
- const where = renderWhere(sources2);
14748
- if (where) out.push(paint(ANSI.dim, ` ${"where".padEnd(9)} ${where}${builtBy ? ` \xB7 built by ${builtBy}` : ""}`));
14749
- return out.join("\n");
14750
- }
14751
- function renderWhere(sources2) {
14752
- const branch = sources2.find((s) => s.kind === "branch")?.ref;
14753
- const commit = sources2.find((s) => s.kind === "git")?.ref;
14754
- const url = sources2.find((s) => s.kind === "url")?.ref;
14755
- 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";
14756
14694
  }
14757
- var VERB = {
14758
- want: { text: "wanted", colour: ANSI.amber },
14759
- confirm: { text: "confirmed", colour: ANSI.green },
14760
- drop: { text: "dropped", colour: ANSI.red },
14761
- merge: { text: "merged into", colour: ANSI.blue },
14762
- separate: { text: "kept apart", colour: ANSI.blue },
14763
- weight: { text: "weighed", colour: ANSI.blue },
14764
- area: { text: "filed", colour: ANSI.blue },
14765
- accept: { text: "accepted", colour: ANSI.green },
14766
- reject: { text: "rejected", colour: ANSI.red },
14767
- first: { text: "put first", colour: ANSI.blue },
14768
- done: { text: "did", colour: ANSI.green },
14769
- 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."
14770
14699
  };
14771
- function renderDecisions(rows2, opts) {
14772
- const paint = painter(opts.color);
14773
- if (!rows2.length) return "No decisions yet.";
14774
- const out = [];
14775
- for (const row of rows2) {
14776
- const when = row.created_at.slice(0, 10);
14777
- const verb = VERB[row.kind] ?? { text: row.kind, colour: ANSI.blue };
14778
- 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() : "";
14779
- const by = row.by ? paint(ANSI.dim, ` \xB7 by ${row.by}`) : "";
14780
- out.push(`${paint(ANSI.dim, when)} ${paint(verb.colour, verb.text)} ${about}${by}`.trimEnd());
14781
- if (row.body) {
14782
- 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]}`);
14783
14708
  }
14709
+ lines.push("", " nanopm autonomy <level>");
14710
+ return lines.join("\n");
14784
14711
  }
14785
- return out.join("\n");
14786
- }
14787
- var HUE = ["\x1B[34m", "\x1B[35m", "\x1B[36m", "\x1B[32m", "\x1B[95m"];
14788
- function themesOf(tree) {
14789
- return new Map(tree.map((b) => [b.id, { name: themeName(b), colour: HUE[themeHue(tree, b) - 1] }]));
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");
14790
14727
  }
14791
- function sizeWord(size, paint) {
14792
- return size === "big" ? `${paint(ANSI.bold, "\u25AA")} ${SIZE_WORD.big}` : size === "small" ? paint(ANSI.dim, SIZE_WORD.small) : "";
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");
14743
+ }
14744
+ return [`${project.name} verifies with:`, "", ` ${current}`, "", ' nanopm verify "<command>" \xB7 nanopm verify --clear'].join("\n");
14745
+ }
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");
14793
14752
  }
14794
- function renderSolutionsBoard(board, opts) {
14753
+
14754
+ // src/format/opportunities.ts
14755
+ function renderOpportunities(rows2, lens, opts) {
14795
14756
  const paint = painter(opts.color);
14796
14757
  const width = Math.max(40, columns());
14797
- const themes = themesOf(board.tree);
14798
- const out = [`${paint(ANSI.bold, "Roadmap")}${board.objective ? ` ${paint(ANSI.dim, `${board.objective.lever} \u2014 ${board.objective.sentence}`)}` : ""}`];
14799
- const chips = board.tree.flatMap((b) => {
14800
- const n = board.cards.filter((c) => c.branch.id === b.id).length;
14801
- const t = themes.get(b.id);
14802
- return n ? [`${paint(t.colour, "\u25CF")} ${t.name} ${paint(ANSI.dim, String(n))}`] : [];
14803
- });
14804
- const legend = chips.reduce((lines, chip) => {
14805
- const last = lines.at(-1);
14806
- if (last !== void 0 && visibleLength(last) + 3 + visibleLength(chip) <= width) lines[lines.length - 1] = `${last} ${chip}`;
14807
- else lines.push(chip);
14808
- return lines;
14809
- }, []);
14810
- out.push("", ...legend);
14811
- 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."));
14812
- const refWidth = Math.max(5, ...board.cards.map((c) => c.pick.ref.length));
14813
- const gutter = " ".repeat(refWidth + 4);
14814
- const titleWidth = Math.min(Math.max(...board.cards.map((c) => c.pick.title.length)), width - gutter.length - 8);
14815
- for (const h of HORIZONS) {
14816
- const column = board.cards.filter((c) => c.horizon === h);
14817
- out.push("", `${paint(ANSI.bold, HORIZON_WORD[h])} ${paint(ANSI.dim, String(column.length))}`);
14818
- if (!column.length) out.push(paint(ANSI.dim, ` Move a card here: nanopm roadmap move <ref> ${h}`));
14819
- for (const c of column) {
14820
- const t = themes.get(c.branch.id);
14821
- out.push(` ${paint(ANSI.dim, c.pick.ref.padEnd(refWidth))} ${paint(t.colour, "\u25CF")} ${paint(ANSI.dim, t.name)}`);
14822
- out.push(`${gutter}${clip(c.pick.title, titleWidth).padEnd(titleWidth)} ${sizeWord(c.pick.size, paint)}`.trimEnd());
14823
- const depth = board.working.get(c.leaf.id);
14824
- if (depth) out.push(`${gutter}${paint(ANSI.blue, "\u25CF")} ${paint(ANSI.dim, WORK_WORD[depth])}`);
14825
- }
14758
+ const out = [];
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
- if (board.earlier) out.push("", paint(ANSI.bold, "On the problem list"), "", board.earlier);
14828
- out.push(
14829
- "",
14830
- paint(ANSI.dim, 'nanopm roadmap <ref> \xB7 nanopm roadmap move <ref> now|next|later [position] \xB7 nanopm reject <ref> ["why"]'),
14831
- paint(ANSI.dim, "nanopm opportunities brainstorm <ref> [--deep]: solutions to another problem, or deeper ones")
14832
- );
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}`) : ""}`);
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]`.'));
14833
14793
  return out.join("\n");
14834
14794
  }
14835
- function renderSolutionSheet(s, at, opts) {
14795
+
14796
+ // src/format/needs.ts
14797
+ var MARK = { observed: "\u25CF", stated: "\u2013", inferred: "\u25CB" };
14798
+ function renderNeeds(persona, tree, opts) {
14836
14799
  const paint = painter(opts.color);
14837
14800
  const width = Math.max(40, columns());
14838
- const theme = at.branch ? themesOf(at.tree).get(at.branch.id) : void 0;
14839
14801
  const out = [];
14840
- if (theme) out.push(`${paint(theme.colour, "\u25CF")} ${paint(ANSI.dim, theme.name)}`);
14841
- const size = sizeWord(s.size, paint);
14842
- out.push(`${paint(ANSI.dim, s.ref)} ${paint(ANSI.bold, s.title)}${size ? ` ${size}` : ""}`);
14843
- const pad = " ".repeat(12);
14844
- const heading = (text2) => out.push("", ` ${paint(ANSI.bold, text2)}`);
14845
- heading("Problem we are solving");
14846
- if (at.leaf) {
14847
- const solves = wrap(at.leaf.problem, width - 6 - at.leaf.ref.length - 2);
14848
- solves[solves.length - 1] += ` ${paint(ANSI.dim, at.leaf.ref)}`;
14849
- for (const line of solves) out.push(` ${line}`);
14850
- if (at.branch && at.branch.id !== at.leaf.id) {
14851
- for (const line of wrap(`Part of: ${at.branch.problem} ${at.branch.ref}`, width - 6)) out.push(` ${paint(ANSI.dim, line)}`);
14852
- }
14853
- } else {
14854
- out.push(paint(ANSI.dim, " The problem it answered is no longer on the map."));
14855
- }
14856
- const described = paragraphs(s.description);
14857
- if (described.length) {
14858
- heading("Proposed solution");
14859
- described.forEach((p, i) => {
14860
- if (i) out.push("");
14861
- for (const line of wrap(p, width - 6)) out.push(` ${line}`);
14862
- });
14863
- }
14864
- const changelog = parseChangelog(s.changelog);
14865
- if (changelog) {
14866
- heading("Future changelog");
14867
- for (const line of wrap(changelog.headline, width - 6)) out.push(` ${paint(ANSI.bold, line)}`);
14868
- for (const point of changelog.points) wrap(point, width - 8).forEach((line, i) => out.push(` ${i ? " " : "\u2022"} ${line}`));
14869
- }
14870
- if (s.steps.length) {
14871
- heading("Plan");
14872
- s.steps.forEach((step, i) => {
14873
- const who = STEP_WHO_WORD[step.who] ?? step.who;
14874
- wrap(step.do, width - pad.length - 8).forEach((line, j) => out.push(`${j ? pad : ` \u2610 ${who.padEnd(6)}`}${line}`));
14875
- if (step.checks) out.push(pad + paint(ANSI.amber, i === 0 ? "checks the problem first" : "checks the problem"));
14876
- });
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");
14877
14807
  }
14878
- const card = at.leaf ? at.cards.find((c) => c.leaf.id === at.leaf.id) : void 0;
14879
- const others = card ? card.others.filter((o) => o.id !== s.id) : [];
14880
- out.push("");
14881
- if (s.status === "rejected") out.push(paint(ANSI.dim, " Thrown out."));
14882
- else if (card?.pick.id === s.id) {
14883
- const column = at.cards.filter((c) => c.horizon === card.horizon);
14884
- const yours = s.placed_by === "founder" ? ", where you put it" : "";
14885
- out.push(paint(ANSI.dim, ` On the board: ${HORIZON_WORD[card.horizon]}, ${column.indexOf(card) + 1} of ${column.length}${yours}.`));
14886
- } else out.push(paint(ANSI.dim, card ? ` Not on the board: ${card.pick.ref} is this problem's card.` : " Not on the board."));
14887
- if (others.length) out.push(paint(ANSI.dim, ` Other solutions to this problem: ${others.map((o) => o.ref).join(", ")}.`));
14888
- if (s.status === "proposed") {
14889
- const move = card?.pick.id === s.id ? `nanopm roadmap move ${s.ref} now|next|later [position] \xB7 ` : "";
14890
- out.push(paint(ANSI.dim, ` ${move}nanopm reject ${s.ref} ["why"]`));
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) {
14820
+ out.push("");
14821
+ line(need, 0);
14891
14822
  }
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(", ")}.`));
14892
14825
  return out.join("\n");
14893
14826
  }
14894
14827
 
14895
- // src/format/problems.ts
14896
- var SHAPE2 = {
14897
- candidate: { mark: "\u25CB", colour: ANSI.dim, label: "candidate" },
14898
- confirmed: { mark: "\u25C9", colour: ANSI.amber, label: "confirmed" },
14899
- addressed: { mark: "\u25CF", colour: ANSI.green, label: "addressed" },
14900
- dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
14901
- };
14902
- function renderImportance(problem, opts) {
14903
- const paint = painter(opts.color);
14904
- if (problem.importance_basis === "evidence") {
14905
- return paint(ANSI.bold, String(Math.round(problem.importance)).padStart(3));
14906
- }
14907
- const value = problem.importance;
14908
- const word = value >= 67 ? "high" : value >= 34 ? "med" : value > 0 ? "low" : "\u2014";
14909
- return paint(ANSI.dim, `~${word}`.padStart(5));
14828
+ // src/commands/needs.ts
14829
+ async function people(client, projectId) {
14830
+ const { data, error } = await client.from("memory").select("key, title, content").eq("project_id", projectId).eq("topic", PERSONA_TOPIC).is("superseded_by", null);
14831
+ if (error) throw new Error(`could not read the personas: ${error.message}`);
14832
+ return (data ?? []).filter((r) => PERSONA_KEY.test(r.key)).map((r) => {
14833
+ const p = parsePersona(r.key, r.content);
14834
+ return { slug: p.slug, name: personaName(r.title, p.slug), primary: p.primary };
14835
+ }).sort((a, b) => Number(b.primary) - Number(a.primary));
14910
14836
  }
14911
- function renderProblems(problems2, duplicates, opts) {
14912
- const paint = painter(opts.color);
14913
- if (!problems2.length) {
14914
- return [
14915
- "No problems on the list yet.",
14916
- "",
14917
- paint(ANSI.dim, " nanopm review reads your data, then works out what they say about your users"),
14918
- paint(ANSI.dim, " nanopm opportunities what to work on, for your objective")
14919
- ].join("\n");
14920
- }
14921
- const refWidth = Math.max(...problems2.map((p) => p.ref.length));
14922
- const out = [];
14923
- const map = groupProblemMap(problems2.map((p) => ({ ...p, parentRef: p.parent_ref })));
14924
- const ordered = [...map.branches.flatMap((b) => [b.parent, ...b.children]), ...map.unplaced];
14925
- for (const problem of ordered) {
14926
- const shape2 = SHAPE2[problem.status] ?? { mark: "\xB7", colour: ANSI.dim, label: problem.status };
14927
- const score = renderImportance(problem, opts);
14928
- 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))}`;
14929
- out.push(head);
14930
- 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"}`));
14931
- const indent = ` ${" ".repeat(refWidth)} `;
14932
- 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";
14933
- out.push(paint(ANSI.dim, indent + clip(tail2, Math.max(40, columns() - indent.length))));
14934
- }
14935
- const unproven = problems2.filter((p) => p.proofs === 0).length;
14936
- if (unproven) {
14937
- out.push("");
14938
- out.push(
14939
- paint(
14940
- ANSI.dim,
14941
- `${unproven} of ${problems2.length} rest on judgement, not proof. A tilde marks those.`
14942
- )
14943
- );
14944
- }
14945
- if (duplicates.length) {
14946
- out.push("");
14947
- for (const pair of duplicates.slice(0, 3)) {
14948
- const why = pair.flagged ? `NanoPM is not sure ${pair.a} and ${pair.b} are different problems` : `${pair.a} and ${pair.b} read alike`;
14949
- out.push(paint(ANSI.amber, `? ${why}`));
14950
- }
14951
- out.push(paint(ANSI.dim, ` nanopm decisions why \xB7 nanopm problems merge ${duplicates[0].b} ${duplicates[0].a}`));
14952
- }
14953
- if (opts.manualMerges !== void 0) {
14954
- out.push("");
14955
- out.push(
14956
- paint(
14957
- ANSI.dim,
14958
- `${opts.manualMerges} merge${opts.manualMerges === 1 ? "" : "s"} you have had to make by hand.`
14959
- )
14960
- );
14961
- }
14962
- out.push("");
14963
- out.push(paint(ANSI.dim, " nanopm problems <ref> \xB7 nanopm problems merge <from> <into> \xB7 nanopm problems weight <ref> <0-100>"));
14964
- return out.join("\n");
14837
+ function pick2(list, named) {
14838
+ if (!list.length) throw new Error("there is no persona on the business card yet. `nanopm personas --refresh` works out who this is for.");
14839
+ if (!named) return list[0];
14840
+ const one = list.find((p) => p.slug === named.toLowerCase() || p.name.toLowerCase() === named.toLowerCase());
14841
+ if (!one) throw new Error(`no persona "${named}". The personas are: ${list.map((p) => p.slug).join(", ")}.`);
14842
+ return one;
14965
14843
  }
14966
- function renderProblem(problem, evidence2, addressedBy, opts) {
14967
- const paint = painter(opts.color);
14968
- const shape2 = SHAPE2[problem.status] ?? { mark: "\xB7", colour: ANSI.dim, label: problem.status };
14969
- const width = columns();
14970
- const out = [];
14971
- out.push(`${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, problem.ref)} ${paint(ANSI.bold, shape2.label)}`);
14972
- out.push("");
14973
- for (const line of wrap(problem.statement, width)) out.push(line);
14974
- out.push("");
14975
- const facts = [];
14976
- const m = problem.mapping ?? null;
14977
- if (m) {
14978
- const story = problemStory(m, { proofs: problem.proofs, kinds: problem.proof_kinds ?? [], provenance: problem.provenance ?? "inferred", authorKind: problem.author_kind ?? "nanopm", createdAt: problem.created_at ?? "" });
14979
- out.push(...wrap(story.story, width));
14980
- out.push("");
14981
- out.push(...wrap(story.doubt, width).map((l) => paint(ANSI.dim, l)));
14982
- out.push(...wrap(story.assumes ? `${story.test} ${story.assumes}` : story.test, width).map((l) => paint(ANSI.dim, l)));
14983
- out.push("");
14984
- out.push(paint(ANSI.dim, "how I got here"));
14985
- facts.push(["kind", `${m.level === 1 ? "a need" : "a difficulty"} \xB7 ${m.knowledge} \xB7 ${m.origin === "expert_inference" ? "expert inference" : "source analysis"}`]);
14986
- if (m.parent_reason) facts.push(["grouping", m.parent_reason]);
14987
- }
14988
- if (problem.segment) facts.push(["who", problem.segment]);
14989
- facts.push([
14990
- "weight",
14991
- 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}` : ""}`
14844
+ async function rows(client, projectId, persona) {
14845
+ let q = client.from("need_nodes").select(NEED_NODE_COLUMNS).eq("project_id", projectId);
14846
+ if (persona) q = q.eq("persona", persona);
14847
+ const { data, error } = await q.order("created_at");
14848
+ if (error) throw new Error(`could not read the needs map: ${error.message}`);
14849
+ return data ?? [];
14850
+ }
14851
+ async function needs(persona, opts) {
14852
+ const auth = await authenticate();
14853
+ const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
14854
+ const list = await people(auth.client, project.id);
14855
+ const who = pick2(list, persona);
14856
+ const tree = needTree(await rows(auth.client, project.id, who.slug));
14857
+ return renderNeeds(who, tree, { color: opts.color ?? useColor(), others: list.filter((p) => p.slug !== who.slug).map((p) => p.slug) });
14858
+ }
14859
+ async function mapNeeds(target, opts) {
14860
+ const auth = await authenticate();
14861
+ const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
14862
+ const list = await people(auth.client, project.id);
14863
+ let persona;
14864
+ let scope = "map";
14865
+ if (target.ref) {
14866
+ const ref = target.ref.toUpperCase();
14867
+ const node = (await rows(auth.client, project.id)).find((r) => r.ref === ref);
14868
+ if (!node) throw new Error(`no node ${target.ref} on any needs map. \`nanopm needs\` shows the refs.`);
14869
+ persona = pick2(list, node.persona);
14870
+ scope = ref;
14871
+ } else persona = pick2(list, target.persona);
14872
+ const [{ data: active }, tree] = await Promise.all([
14873
+ auth.client.from("jobs").select("params").eq("project_id", project.id).eq("skill", "needs").in("status", [...ACTIVE_STATUSES]),
14874
+ rows(auth.client, project.id, persona.slug).then(needTree)
14992
14875
  ]);
14993
- if (problem.parent_ref) facts.push(["part of", problem.parent_ref]);
14994
- for (const [label, value] of facts) {
14995
- out.push(`${paint(ANSI.dim, label.padEnd(9))}${clip(value, width - 9)}`);
14996
- }
14997
- if (evidence2.length) {
14998
- out.push("");
14999
- out.push(paint(ANSI.dim, "sources"));
15000
- for (const item of evidence2) {
15001
- const when = (item.occurred_at ?? item.created_at).slice(0, 10);
15002
- const where = item.metric_key ?? item.source_ref ?? item.kind;
15003
- for (const line of wrap(item.body, width - 2)) out.push(` ${line}`);
15004
- out.push(paint(ANSI.dim, ` \u2014 ${item.relation ?? "supports"} \xB7 ${item.kind} \xB7 ${where} \xB7 ${when}`));
15005
- }
15006
- } else {
15007
- out.push("");
15008
- out.push(paint(ANSI.dim, "No proof yet. This ranking is a judgement, and the next review may settle it."));
15009
- }
15010
- if (addressedBy.length) {
15011
- out.push("");
15012
- out.push(paint(ANSI.dim, "addressed by"));
15013
- for (const item of addressedBy) {
15014
- out.push(` ${paint(ANSI.dim, item.ref)} ${clip(item.title, width - 12)} ${paint(ANSI.dim, `(${item.status})`)}`);
15015
- }
14876
+ const lineage = (ref) => {
14877
+ const walk = (l) => {
14878
+ for (const n of l) {
14879
+ if (n.ref === ref.toUpperCase()) return n.path.map((p) => p.ref);
14880
+ const deeper = walk(n.children);
14881
+ if (deeper) return deeper;
14882
+ }
14883
+ return null;
14884
+ };
14885
+ return walk(tree) ?? [ref];
14886
+ };
14887
+ const clash = needsRunConflict((active ?? []).map((j) => runScopeOf(j.params)), { persona: persona.slug, scope }, { primary: list.find((p) => p.primary)?.slug ?? list[0]?.slug ?? null, lineage, name: (s) => list.find((p) => p.slug === s)?.name ?? s });
14888
+ if (clash) throw new Error(clash);
14889
+ const repo = await detectRepo(opts.cwd);
14890
+ if (repo) await rememberClone(auth.client, project.id, repo.root);
14891
+ const params = {
14892
+ persona: persona.slug,
14893
+ ...scope === "map" ? {} : { scope },
14894
+ depth: opts.short ? "short" : "deep",
14895
+ ...opts.note?.trim() ? { note: opts.note.trim().slice(0, 600) } : {},
14896
+ ...repo ? { repo_path: repo.root } : {}
14897
+ };
14898
+ return runSkill("needs", { project: project.id, params, cancelSignal: opts.cancelSignal });
14899
+ }
14900
+ async function confirmNeed(ref, opts) {
14901
+ const auth = await authenticate();
14902
+ const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
14903
+ const { data, error } = await auth.client.from("need_nodes").update({ status: "confirmed" }).eq("project_id", project.id).eq("ref", ref.toUpperCase()).eq("status", "proposed").select("ref, label");
14904
+ if (error) throw new Error(pgMessage(error));
14905
+ if (!data?.length) throw new Error(`${ref} is not a node waiting on your word. \`nanopm needs\` shows which are.`);
14906
+ return `Confirmed ${data[0].ref}: ${data[0].label}. A run proposes beside it from now on, never over it.`;
14907
+ }
14908
+ async function dropNeed(ref, why, opts) {
14909
+ const auth = await authenticate();
14910
+ const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
14911
+ const { data, error } = await auth.client.from("need_nodes").update({ status: "dropped", drop_reason: why?.trim().slice(0, 400) || "not true of them" }).eq("project_id", project.id).eq("ref", ref.toUpperCase()).in("status", ["proposed", "confirmed"]).select("ref, label");
14912
+ if (error) throw new Error(pgMessage(error));
14913
+ if (!data?.length) throw new Error(`${ref} is not on the map any more.`);
14914
+ return `Dropped ${data[0].ref}: ${data[0].label}. It stays on the record, and NanoPM will not propose it again in the same place.`;
14915
+ }
14916
+
14917
+ // src/commands/opportunities.ts
14918
+ async function lensOf2(client, projectId) {
14919
+ const { data } = await client.from("memory").select("id, key, content").eq("project_id", projectId).eq("topic", GOALS_TOPIC).in("key", [OBJECTIVE_KEY, DEFINITION_KEY]).is("superseded_by", null);
14920
+ const rows2 = data ?? [];
14921
+ const aim = rows2.find((r) => r.key === OBJECTIVE_KEY);
14922
+ const m = aim ? /^Lever:\s*([a-z]+)\.\s*(.*)$/s.exec(aim.content.trim()) : null;
14923
+ const lever = m?.[1];
14924
+ if (!aim || !lever || !OUTCOME_LEVERS.includes(lever)) return null;
14925
+ const def = rows2.find((r) => r.key === DEFINITION_KEY);
14926
+ const parsed = def ? parseDefinition(def.content) : null;
14927
+ const sentence2 = m[2].trim();
14928
+ const definition = parsed && parsed.lever === lever ? parsed.sentence : null;
14929
+ return { id: aim.id, lever, sentence: sentence2, definition, key: lensKey(lever, sentence2, definition) };
14930
+ }
14931
+ async function loadOpportunities(client, projectId) {
14932
+ const [standing, replaced] = await Promise.all([
14933
+ client.from("opportunities").select(OPPORTUNITY_COLUMNS).eq("project_id", projectId).neq("status", "replaced"),
14934
+ client.from("opportunities").select(REPLACED_COLUMNS).eq("project_id", projectId).eq("status", "replaced")
14935
+ ]);
14936
+ const error = standing.error ?? replaced.error;
14937
+ if (error) throw new Error(`could not read the opportunities: ${error.message}`);
14938
+ return [...standing.data ?? [], ...replaced.data ?? []];
14939
+ }
14940
+ async function workingOn(client, projectId) {
14941
+ const { data } = await client.from("jobs").select("params").eq("project_id", projectId).eq("skill", SOLUTIONS_SKILL).in("status", [...ACTIVE_STATUSES]);
14942
+ return leavesAtWork((data ?? []).map((j) => j.params));
14943
+ }
14944
+ async function opportunities(opts) {
14945
+ const auth = await authenticate();
14946
+ const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
14947
+ const [lens, all, personas2] = await Promise.all([lensOf2(auth.client, project.id), loadOpportunities(auth.client, project.id), people(auth.client, project.id)]);
14948
+ const { shown, drawnFor } = shownMap(all, lens?.key ?? null);
14949
+ const names = new Map(personas2.map((p) => [p.slug, p.name]));
14950
+ return renderOpportunities(shown, lens, { color: opts.color ?? useColor(), previous: drawnFor === "previous", names });
14951
+ }
14952
+ async function rankOpportunities(opts) {
14953
+ const auth = await authenticate();
14954
+ const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
14955
+ const list = (s) => s ? s.split(",").map((x) => x.trim()).filter(Boolean) : void 0;
14956
+ const sources2 = opts.use || opts.skip ? { ...opts.use ? { use: list(opts.use) } : {}, ...opts.skip ? { skip: list(opts.skip) } : {} } : void 0;
14957
+ const params = {
14958
+ depth: opts.deep ? "deep" : "short",
14959
+ ...opts.note?.trim() ? { note: opts.note.trim().slice(0, OPPORTUNITIES_NOTE_MAX) } : {},
14960
+ ...sources2 ? { sources: sources2 } : {}
14961
+ };
14962
+ const checked = opportunitiesAsk(params);
14963
+ if (!checked.ok) throw new Error(checked.why);
14964
+ const { data: active } = await auth.client.from("jobs").select("id").eq("project_id", project.id).eq("skill", "opportunities").in("status", [...ACTIVE_STATUSES]);
14965
+ const clash = opportunitiesRunConflict(active ?? []);
14966
+ if (clash) throw new Error(clash);
14967
+ const repo = await detectRepo(opts.cwd);
14968
+ if (repo) await rememberClone(auth.client, project.id, repo.root);
14969
+ return runSkill("opportunities", { project: project.id, params: { ...params, ...repo ? { repo_path: repo.root } : {} }, cancelSignal: opts.cancelSignal });
14970
+ }
14971
+ async function rpc2(name, args, opts) {
14972
+ const auth = await authenticate();
14973
+ const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
14974
+ const { error } = await auth.client.rpc(name, { p_project_id: project.id, ...args });
14975
+ if (error) throw new Error(pgMessage(error));
14976
+ }
14977
+ async function confirmOpportunity(ref, opts) {
14978
+ await rpc2("confirm_opportunity", { p_ref: ref }, opts);
14979
+ return `${ref.toUpperCase()} is yours. A later run ranks around it and never replaces it.`;
14980
+ }
14981
+ async function setAsideOpportunity(ref, why, opts) {
14982
+ await rpc2("set_aside_opportunity", { p_ref: ref, p_reason: why ?? "not now" }, opts);
14983
+ return `${ref.toUpperCase()} is set aside for this objective, with the narrower problems under it. It comes back if the objective changes.`;
14984
+ }
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}`);
15016
15020
  }
15017
- out.push("");
15018
- out.push(paint(ANSI.dim, " nanopm problems \xB7 nanopm roadmap"));
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`.");
15019
15029
  return out.join("\n");
15020
15030
  }
15021
15031
 
15022
- // src/format/tree.ts
15023
- var SOLUTION_SHAPE = {
15032
+ // src/format/roadmap.ts
15033
+ var SHAPE = {
15024
15034
  proposed: { mark: "\u25CB", colour: ANSI.amber, label: "proposed" },
15025
15035
  accepted: { mark: "\u25D0", colour: ANSI.green, label: "accepted" },
15026
15036
  building: { mark: "\u25D4", colour: ANSI.blue, label: "building" },
15027
- done: { mark: "\u25CF", colour: ANSI.green, label: "done" },
15028
15037
  rejected: { mark: "\u2298", colour: ANSI.red, label: "rejected" },
15038
+ done: { mark: "\u25CF", colour: ANSI.green, label: "done" },
15029
15039
  dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
15030
15040
  };
15031
- var PROBLEM_SHAPE = {
15032
- candidate: { mark: "\u25CB", colour: ANSI.dim },
15033
- confirmed: { mark: "\u25C9", colour: ANSI.amber },
15034
- addressed: { mark: "\u25CF", colour: ANSI.green },
15035
- dropped: { mark: "\u2298", colour: ANSI.dim }
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 }
15036
15086
  };
15037
- var NO_AREA = "no area yet";
15038
- var SHOWN = /* @__PURE__ */ new Set(["proposed", "accepted", "building", "done"]);
15039
- function renderTree(problems2, solutions, strategy, opts) {
15087
+ function renderDecisions(rows2, opts) {
15040
15088
  const paint = painter(opts.color);
15089
+ if (!rows2.length) return "No decisions yet.";
15041
15090
  const out = [];
15042
- const width = Math.max(40, columns());
15043
- if (strategy) {
15044
- out.push(paint(ANSI.bold, "Strategy"));
15045
- for (const line of wrap(strategy, width - 4)) out.push(` ${line}`);
15046
- out.push("");
15047
- }
15048
- const shown = solutions.filter((s) => SHOWN.has(s.status));
15049
- const open = problems2.filter((p) => p.status !== "dropped" && (!opts.alongside || shown.some((s) => s.problem_id === p.id)));
15050
- if (opts.alongside && !open.length) return "";
15051
- if (!open.length && !shown.length) {
15052
- out.push("No problems yet. `nanopm opportunities` shows what to work on.");
15053
- return out.join("\n");
15054
- }
15055
- const byProblem = /* @__PURE__ */ new Map();
15056
- for (const s of shown) byProblem.set(s.problem_id, [...byProblem.get(s.problem_id) ?? [], s]);
15057
- const refWidth = Math.max(...[...open.map((p) => p.ref), ...shown.map((s) => s.ref)].map((r) => r.length), 5);
15058
- for (const [area, rows2] of groupByArea(open)) {
15059
- out.push(paint(ANSI.bold, area));
15060
- for (const p of rows2) {
15061
- const shape2 = PROBLEM_SHAPE[p.status] ?? { mark: "\xB7", colour: "" };
15062
- const kind = p.kind === "founder" ? paint(ANSI.dim, " (founder)") : "";
15063
- const proof = p.proofs ? `${p.proofs} proof${p.proofs === 1 ? "" : "s"}` : "no proof";
15064
- const tail2 = `${renderImportance(p, opts)} ${paint(ANSI.dim, proof)}`;
15065
- const head = ` ${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, p.ref.padEnd(refWidth))} `;
15066
- out.push(head + clip(p.statement, Math.max(30, width - visibleLength(head) - visibleLength(tail2) - 2)) + kind + " " + tail2);
15067
- const under = byProblem.get(p.id) ?? [];
15068
- if (!under.length) {
15069
- out.push(paint(ANSI.dim, ` ${" ".repeat(refWidth + 2)} no solution yet`));
15070
- continue;
15071
- }
15072
- for (const s of under) out.push(...solutionLines(s, refWidth, width, paint));
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}`));
15073
15099
  }
15074
- out.push("");
15075
15100
  }
15076
- const waiting = shown.filter((s) => s.status === "proposed").length;
15077
- if (waiting) {
15078
- out.push(`${waiting} waiting on you.`);
15079
- out.push(paint(ANSI.dim, ' nanopm accept <ref> \xB7 nanopm reject <ref> "why" \xB7 nanopm roadmap <ref> \xB7 nanopm roadmap --board'));
15080
- } else if (!opts.alongside) {
15081
- out.push(paint(ANSI.dim, "Nothing waiting on you."));
15082
- }
15083
- return out.join("\n").trimEnd();
15101
+ return out.join("\n");
15084
15102
  }
15085
- function solutionLines(s, refWidth, width, paint, indent = 6) {
15086
- const shape2 = SOLUTION_SHAPE[s.status] ?? { mark: "\xB7", colour: "", label: s.status };
15087
- const pad = " ".repeat(indent);
15088
- const STATE = { accepted: "chosen", building: "building", done: "done" };
15089
- 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;
15090
- const badge = tail2 ? paint(ANSI.dim, ` ${tail2}`) : "";
15091
- const head = `${pad}${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, s.ref.padEnd(refWidth))} `;
15092
- const lines = [head + clip(s.title, Math.max(30, width - visibleLength(head) - visibleLength(badge))) + badge];
15093
- const reason2 = `${pad} ${" ".repeat(refWidth)} `;
15094
- lines.push(paint(ANSI.dim, reason2 + clip(firstSentence(s.reasoning), Math.max(30, width - reason2.length))));
15095
- return lines;
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] }]));
15096
15106
  }
15097
- function groupByArea(problems2) {
15098
- const groups = /* @__PURE__ */ new Map();
15099
- for (const p of problems2) {
15100
- const area = p.area ?? NO_AREA;
15101
- groups.set(area, [...groups.get(area) ?? [], p]);
15102
- }
15103
- const named = [...groups.entries()].filter(([a]) => a !== NO_AREA);
15104
- const rest = groups.get(NO_AREA);
15105
- return rest ? [...named, [NO_AREA, rest]] : named;
15107
+ function sizeWord(size, paint) {
15108
+ return size === "big" ? `${paint(ANSI.bold, "\u25AA")} ${SIZE_WORD.big}` : size === "small" ? paint(ANSI.dim, SIZE_WORD.small) : "";
15106
15109
  }
15107
- var BOARD_COLUMNS = [
15108
- { status: "proposed", label: "Proposed" },
15109
- { status: "accepted", label: "Chosen" },
15110
- { status: "building", label: "Building" },
15111
- { status: "done", label: "Done" }
15112
- ];
15113
- function renderBoard(problems2, solutions, opts) {
15110
+ function renderSolutionsBoard(board, opts) {
15114
15111
  const paint = painter(opts.color);
15115
- const out = [];
15116
- const byId = new Map(problems2.map((p) => [p.id, p]));
15117
15112
  const width = Math.max(40, columns());
15118
- const refWidth = Math.max(...solutions.map((s) => s.ref.length), 5);
15119
- for (const col of BOARD_COLUMNS) {
15120
- const rows2 = solutions.filter((s) => s.status === col.status);
15121
- out.push(`${paint(ANSI.bold, col.label)} ${paint(ANSI.dim, String(rows2.length))}`);
15122
- if (!rows2.length) out.push(paint(ANSI.dim, " \u2014"));
15123
- for (const s of rows2) {
15124
- const shape2 = SOLUTION_SHAPE[s.status];
15125
- const problem = byId.get(s.problem_id);
15126
- const head = ` ${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, s.ref.padEnd(refWidth))} `;
15127
- out.push(head + clip(s.title, Math.max(30, width - visibleLength(head))));
15128
- const under = [problem ? `${problem.ref} ${clip(problem.statement, 50)}` : "its problem was dropped", s.assumption ? clip(s.assumption, 40) : null].filter(Boolean).join(" \xB7 ");
15129
- out.push(paint(ANSI.dim, ` ${" ".repeat(refWidth + 2)} ${under}`));
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])}`);
15130
15141
  }
15131
- out.push("");
15132
15142
  }
15133
- return out.join("\n").trimEnd();
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");
15134
15150
  }
15135
-
15136
- // src/format/opportunities.ts
15137
- function renderOpportunities(rows2, lens, opts) {
15151
+ function renderSolutionSheet(s, at, opts) {
15138
15152
  const paint = painter(opts.color);
15139
15153
  const width = Math.max(40, columns());
15154
+ const theme = at.branch ? themesOf(at.tree).get(at.branch.id) : void 0;
15140
15155
  const out = [];
15141
- 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`.";
15142
- out.push(`${paint(ANSI.bold, "Opportunities")} ${paint(ANSI.dim, `${lens.lever} \u2014 ${lens.sentence}`)}`);
15143
- out.push(paint(ANSI.dim, lens.definition ? `Counts: ${lens.definition}` : "What it counts is not defined yet: drawn against the lever and your sentence."));
15144
- if (opts.previous) out.push(paint(ANSI.amber, "Drawn for your previous objective. `nanopm opportunities run` draws it for this one."));
15145
- if (!rows2.length) {
15146
- 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.");
15147
- return out.join("\n");
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."));
15148
15171
  }
15149
- const tree = liveTree(rows2);
15150
- const live = rows2.filter(isLive);
15151
- const evidenced = live.filter((r) => r.evidence_status === "evidence_backed").length;
15152
- 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.`));
15153
- const who = (r) => r.personas.map((p) => opts.names?.get(p) ?? p).join(", ");
15154
- const one = (r, place) => {
15155
- const indent = " ".repeat(r.depth - 1);
15156
- const yours = r.status === "confirmed" ? paint(ANSI.green, " yours") : "";
15157
- 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)}`);
15158
- const pad = `${indent} `;
15159
- const facts = [PRIORITY_WORD[r.priority], EVIDENCE_STATUS_WORD[r.evidence_status], NEXT_ACTION_WORD[r.next_action], who(r)].filter(Boolean).join(" \xB7 ");
15160
- out.push(`${pad}${paint(ANSI.dim, facts)}`);
15161
- const gist = r.description ? paragraphs(r.description)[0] : null;
15162
- if (gist) for (const l of wrap(gist, width - pad.length)) out.push(`${pad}${l}`);
15163
- if (r.learn) for (const l of wrap(`To learn more: ${r.learn}`, width - pad.length)) out.push(`${pad}${paint(ANSI.dim, l)}`);
15164
- 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}.`)}`);
15165
- if (r.reconsider) for (const l of wrap(`Worth reconsidering: ${r.reconsider}`, width - pad.length)) out.push(`${pad}${paint(ANSI.amber, l)}`);
15166
- for (const c of r.children) one(c, `${place}.${c.rank}`);
15167
- };
15168
- for (const r of tree) one(r, String(r.rank));
15169
- const aside = rows2.filter((r) => r.status === "set_aside");
15170
- if (aside.length) {
15171
- out.push("", paint(ANSI.bold, `Set aside (${aside.length})`));
15172
- 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}`) : ""}`);
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));
15173
15222
  }
15174
- 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]`.'));
15175
- return out.join("\n");
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));
15176
15226
  }
15177
-
15178
- // src/format/needs.ts
15179
- var MARK = { observed: "\u25CF", stated: "\u2013", inferred: "\u25CB" };
15180
- function renderNeeds(persona, tree, opts) {
15227
+ function renderProblems(problems2, duplicates, opts) {
15181
15228
  const paint = painter(opts.color);
15182
- const width = Math.max(40, columns());
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));
15183
15238
  const out = [];
15184
- const counts = needCounts(tree);
15185
- out.push(`${paint(ANSI.bold, `${persona.name}\u2019s needs`)} ${paint(ANSI.dim, `${counts.needs} needs \xB7 ${counts.difficulties + counts.precise} difficulties`)}`);
15186
- if (!tree.length) {
15187
- out.push("", `Nothing mapped yet. \`nanopm needs --refresh ${persona.slug}\` maps it in depth; add --short for a quick pass.`);
15188
- return out.join("\n");
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))));
15189
15250
  }
15190
- const line = (n, depth) => {
15191
- const indent = " ".repeat(depth);
15192
- const lead = n.level === 1 ? `${n.emoji ?? " "} ` : `${MARK[n.provenance]} `;
15193
- 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"}`) : "";
15194
- const state = n.status === "dropped" ? paint(ANSI.dim, " dropped") : n.status === "confirmed" || n.author_kind === "user" ? "" : paint(ANSI.amber, " to review");
15195
- const label = n.status === "dropped" ? paint(ANSI.dim, n.label) : n.level === 1 ? paint(ANSI.bold, n.label) : n.label;
15196
- const body = wrap(`${label}${where}${state}${n.proposal ? paint(ANSI.amber, " \xB7 NanoPM suggests a new wording") : ""}`, width - indent.length - 12);
15197
- out.push(`${indent}${lead}${paint(ANSI.dim, n.ref.padEnd(8))} ${body[0] ?? ""}`);
15198
- for (const more of body.slice(1)) out.push(`${indent} ${more}`);
15199
- for (const c of n.children) line(c, depth + 1);
15200
- };
15201
- for (const need of tree) {
15251
+ const unproven = problems2.filter((p) => p.proofs === 0).length;
15252
+ if (unproven) {
15202
15253
  out.push("");
15203
- line(need, 0);
15254
+ out.push(
15255
+ paint(
15256
+ ANSI.dim,
15257
+ `${unproven} of ${problems2.length} rest on judgement, not proof. A tilde marks those.`
15258
+ )
15259
+ );
15204
15260
  }
15205
- 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>\`.`));
15206
- if (opts.others?.length) out.push(paint(ANSI.dim, `Other personas: ${opts.others.join(", ")}.`));
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>"));
15207
15280
  return out.join("\n");
15208
15281
  }
15209
-
15210
- // src/commands/needs.ts
15211
- async function people(client, projectId) {
15212
- const { data, error } = await client.from("memory").select("key, title, content").eq("project_id", projectId).eq("topic", PERSONA_TOPIC).is("superseded_by", null);
15213
- if (error) throw new Error(`could not read the personas: ${error.message}`);
15214
- return (data ?? []).filter((r) => PERSONA_KEY.test(r.key)).map((r) => {
15215
- const p = parsePersona(r.key, r.content);
15216
- return { slug: p.slug, name: personaName(r.title, p.slug), primary: p.primary };
15217
- }).sort((a, b) => Number(b.primary) - Number(a.primary));
15218
- }
15219
- function pick2(list, named) {
15220
- if (!list.length) throw new Error("there is no persona on the business card yet. `nanopm personas --refresh` works out who this is for.");
15221
- if (!named) return list[0];
15222
- const one = list.find((p) => p.slug === named.toLowerCase() || p.name.toLowerCase() === named.toLowerCase());
15223
- if (!one) throw new Error(`no persona "${named}". The personas are: ${list.map((p) => p.slug).join(", ")}.`);
15224
- return one;
15225
- }
15226
- async function rows(client, projectId, persona) {
15227
- let q = client.from("need_nodes").select(NEED_NODE_COLUMNS).eq("project_id", projectId);
15228
- if (persona) q = q.eq("persona", persona);
15229
- const { data, error } = await q.order("created_at");
15230
- if (error) throw new Error(`could not read the needs map: ${error.message}`);
15231
- return data ?? [];
15232
- }
15233
- async function needs(persona, opts) {
15234
- const auth = await authenticate();
15235
- const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
15236
- const list = await people(auth.client, project.id);
15237
- const who = pick2(list, persona);
15238
- const tree = needTree(await rows(auth.client, project.id, who.slug));
15239
- return renderNeeds(who, tree, { color: opts.color ?? useColor(), others: list.filter((p) => p.slug !== who.slug).map((p) => p.slug) });
15240
- }
15241
- async function mapNeeds(target, opts) {
15242
- const auth = await authenticate();
15243
- const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
15244
- const list = await people(auth.client, project.id);
15245
- let persona;
15246
- let scope = "map";
15247
- if (target.ref) {
15248
- const ref = target.ref.toUpperCase();
15249
- const node = (await rows(auth.client, project.id)).find((r) => r.ref === ref);
15250
- if (!node) throw new Error(`no node ${target.ref} on any needs map. \`nanopm needs\` shows the refs.`);
15251
- persona = pick2(list, node.persona);
15252
- scope = ref;
15253
- } else persona = pick2(list, target.persona);
15254
- const [{ data: active }, tree] = await Promise.all([
15255
- auth.client.from("jobs").select("params").eq("project_id", project.id).eq("skill", "needs").in("status", [...ACTIVE_STATUSES]),
15256
- rows(auth.client, project.id, persona.slug).then(needTree)
15257
- ]);
15258
- const lineage = (ref) => {
15259
- const walk = (l) => {
15260
- for (const n of l) {
15261
- if (n.ref === ref.toUpperCase()) return n.path.map((p) => p.ref);
15262
- const deeper = walk(n.children);
15263
- if (deeper) return deeper;
15264
- }
15265
- return null;
15266
- };
15267
- return walk(tree) ?? [ref];
15268
- };
15269
- const clash = needsRunConflict((active ?? []).map((j) => runScopeOf(j.params)), { persona: persona.slug, scope }, { primary: list.find((p) => p.primary)?.slug ?? list[0]?.slug ?? null, lineage, name: (s) => list.find((p) => p.slug === s)?.name ?? s });
15270
- if (clash) throw new Error(clash);
15271
- const repo = await detectRepo(opts.cwd);
15272
- if (repo) await rememberClone(auth.client, project.id, repo.root);
15273
- const params = {
15274
- persona: persona.slug,
15275
- ...scope === "map" ? {} : { scope },
15276
- depth: opts.short ? "short" : "deep",
15277
- ...opts.note?.trim() ? { note: opts.note.trim().slice(0, 600) } : {},
15278
- ...repo ? { repo_path: repo.root } : {}
15279
- };
15280
- return runSkill("needs", { project: project.id, params, cancelSignal: opts.cancelSignal });
15281
- }
15282
- async function confirmNeed(ref, opts) {
15283
- const auth = await authenticate();
15284
- const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
15285
- const { data, error } = await auth.client.from("need_nodes").update({ status: "confirmed" }).eq("project_id", project.id).eq("ref", ref.toUpperCase()).eq("status", "proposed").select("ref, label");
15286
- if (error) throw new Error(pgMessage(error));
15287
- if (!data?.length) throw new Error(`${ref} is not a node waiting on your word. \`nanopm needs\` shows which are.`);
15288
- return `Confirmed ${data[0].ref}: ${data[0].label}. A run proposes beside it from now on, never over it.`;
15289
- }
15290
- async function dropNeed(ref, why, opts) {
15291
- const auth = await authenticate();
15292
- const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
15293
- const { data, error } = await auth.client.from("need_nodes").update({ status: "dropped", drop_reason: why?.trim().slice(0, 400) || "not true of them" }).eq("project_id", project.id).eq("ref", ref.toUpperCase()).in("status", ["proposed", "confirmed"]).select("ref, label");
15294
- if (error) throw new Error(pgMessage(error));
15295
- if (!data?.length) throw new Error(`${ref} is not on the map any more.`);
15296
- return `Dropped ${data[0].ref}: ${data[0].label}. It stays on the record, and NanoPM will not propose it again in the same place.`;
15297
- }
15298
-
15299
- // src/commands/opportunities.ts
15300
- async function lensOf2(client, projectId) {
15301
- const { data } = await client.from("memory").select("id, key, content").eq("project_id", projectId).eq("topic", GOALS_TOPIC).in("key", [OBJECTIVE_KEY, DEFINITION_KEY]).is("superseded_by", null);
15302
- const rows2 = data ?? [];
15303
- const aim = rows2.find((r) => r.key === OBJECTIVE_KEY);
15304
- const m = aim ? /^Lever:\s*([a-z]+)\.\s*(.*)$/s.exec(aim.content.trim()) : null;
15305
- const lever = m?.[1];
15306
- if (!aim || !lever || !OUTCOME_LEVERS.includes(lever)) return null;
15307
- const def = rows2.find((r) => r.key === DEFINITION_KEY);
15308
- const parsed = def ? parseDefinition(def.content) : null;
15309
- const sentence2 = m[2].trim();
15310
- const definition = parsed && parsed.lever === lever ? parsed.sentence : null;
15311
- return { id: aim.id, lever, sentence: sentence2, definition, key: lensKey(lever, sentence2, definition) };
15312
- }
15313
- async function loadOpportunities(client, projectId) {
15314
- const [standing, replaced] = await Promise.all([
15315
- client.from("opportunities").select(OPPORTUNITY_COLUMNS).eq("project_id", projectId).neq("status", "replaced"),
15316
- client.from("opportunities").select(REPLACED_COLUMNS).eq("project_id", projectId).eq("status", "replaced")
15317
- ]);
15318
- const error = standing.error ?? replaced.error;
15319
- if (error) throw new Error(`could not read the opportunities: ${error.message}`);
15320
- return [...standing.data ?? [], ...replaced.data ?? []];
15321
- }
15322
- async function workingOn(client, projectId) {
15323
- const { data } = await client.from("jobs").select("params").eq("project_id", projectId).eq("skill", SOLUTIONS_SKILL).in("status", [...ACTIVE_STATUSES]);
15324
- return leavesAtWork((data ?? []).map((j) => j.params));
15325
- }
15326
- async function opportunities(opts) {
15327
- const auth = await authenticate();
15328
- const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
15329
- const [lens, all, personas2] = await Promise.all([lensOf2(auth.client, project.id), loadOpportunities(auth.client, project.id), people(auth.client, project.id)]);
15330
- const { shown, drawnFor } = shownMap(all, lens?.key ?? null);
15331
- const names = new Map(personas2.map((p) => [p.slug, p.name]));
15332
- return renderOpportunities(shown, lens, { color: opts.color ?? useColor(), previous: drawnFor === "previous", names });
15333
- }
15334
- async function rankOpportunities(opts) {
15335
- const auth = await authenticate();
15336
- const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
15337
- const list = (s) => s ? s.split(",").map((x) => x.trim()).filter(Boolean) : void 0;
15338
- const sources2 = opts.use || opts.skip ? { ...opts.use ? { use: list(opts.use) } : {}, ...opts.skip ? { skip: list(opts.skip) } : {} } : void 0;
15339
- const params = {
15340
- depth: opts.deep ? "deep" : "short",
15341
- ...opts.note?.trim() ? { note: opts.note.trim().slice(0, OPPORTUNITIES_NOTE_MAX) } : {},
15342
- ...sources2 ? { sources: sources2 } : {}
15343
- };
15344
- const checked = opportunitiesAsk(params);
15345
- if (!checked.ok) throw new Error(checked.why);
15346
- const { data: active } = await auth.client.from("jobs").select("id").eq("project_id", project.id).eq("skill", "opportunities").in("status", [...ACTIVE_STATUSES]);
15347
- const clash = opportunitiesRunConflict(active ?? []);
15348
- if (clash) throw new Error(clash);
15349
- const repo = await detectRepo(opts.cwd);
15350
- if (repo) await rememberClone(auth.client, project.id, repo.root);
15351
- return runSkill("opportunities", { project: project.id, params: { ...params, ...repo ? { repo_path: repo.root } : {} }, cancelSignal: opts.cancelSignal });
15352
- }
15353
- async function rpc2(name, args, opts) {
15354
- const auth = await authenticate();
15355
- const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
15356
- const { error } = await auth.client.rpc(name, { p_project_id: project.id, ...args });
15357
- if (error) throw new Error(pgMessage(error));
15358
- }
15359
- async function confirmOpportunity(ref, opts) {
15360
- await rpc2("confirm_opportunity", { p_ref: ref }, opts);
15361
- return `${ref.toUpperCase()} is yours. A later run ranks around it and never replaces it.`;
15362
- }
15363
- async function setAsideOpportunity(ref, why, opts) {
15364
- await rpc2("set_aside_opportunity", { p_ref: ref, p_reason: why ?? "not now" }, opts);
15365
- return `${ref.toUpperCase()} is set aside for this objective, with the narrower problems under it. It comes back if the objective changes.`;
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");
15366
15336
  }
15367
- async function moveOpportunity(ref, place, opts) {
15368
- const rank2 = Number(place);
15369
- if (!Number.isInteger(rank2) || rank2 < 1) throw new Error("say where: nanopm opportunities move <ref> <place>, a place from 1");
15370
- await rpc2("move_opportunity", { p_ref: ref, p_rank: rank2 }, opts);
15371
- return `${ref.toUpperCase()} is at #${rank2} among its siblings, and the place is yours. Moved above a higher priority, it takes that priority.`;
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();
15372
15400
  }
15373
- async function rewordOpportunity(ref, problem, opts) {
15374
- const text2 = problem?.trim().replace(/\s+/g, " ") ?? "";
15375
- if (!text2) throw new Error('say it: nanopm opportunities reword <ref> "the problem, in your words"');
15376
- if (text2.length > PROBLEM_MAX) throw new Error(`one line, under ${PROBLEM_MAX} characters`);
15377
- await rpc2("restate_opportunity", { p_ref: ref, p_problem: text2 }, opts);
15378
- 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;
15379
15412
  }
15380
- var AWAKE_MS = 12e4;
15381
- async function awakeDaemon(client, userId) {
15382
- const since = new Date(Date.now() - AWAKE_MS).toISOString();
15383
- const [{ data }, local] = await Promise.all([
15384
- 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 }),
15385
- localDaemonId()
15386
- ]);
15387
- 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;
15388
15422
  }
15389
- async function brainstorm(ref, opts) {
15390
- const auth = await authenticate();
15391
- const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
15392
- const [lens, all, busy] = await Promise.all([lensOf2(auth.client, project.id), loadOpportunities(auth.client, project.id), workingOn(auth.client, project.id)]);
15393
- const deep = Boolean(opts.deep);
15394
- const plan = brainstormPlan(shownMap(all, lens?.key ?? null).shown, ref, deep, new Set(busy.keys()));
15395
- const daemon = await awakeDaemon(auth.client, auth.userId);
15396
- 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.");
15397
- const repo = deep ? await detectRepo(opts.cwd) : null;
15398
- if (repo) await rememberClone(auth.client, project.id, repo.root);
15399
- for (const params of plan.jobs) {
15400
- 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 });
15401
- 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("");
15402
15448
  }
15403
- const n = plan.leaves.length;
15404
- const minutes = deep ? DEEP_MINUTES * n : FLASH_MINUTES;
15405
- 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`;
15406
- const how = deep ? `${DEPTH_WORD.deep}${n > 1 ? ", one after the other" : ""}` : `${DEPTH_WORD.quick}, a first pass`;
15407
- const out = [`${deep ? "Going deeper on" : "Brainstorming"} ${what}: ${how}, about ${minutes} minutes on ${daemon.hostname}.`];
15408
- 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))}`);
15409
- if (plan.busy) out.push(`${plan.busy} left out: Nano is already finding solutions to ${plan.busy === 1 ? "it" : "them"}.`);
15410
- out.push("The solutions land on `nanopm roadmap`.");
15411
- return out.join("\n");
15449
+ return out.join("\n").trimEnd();
15412
15450
  }
15413
15451
 
15414
15452
  // src/commands/roadmap.ts
@@ -15568,8 +15606,8 @@ async function decide(kind, ref, body, opts) {
15568
15606
  const row = Array.isArray(data) ? data[0] : void 0;
15569
15607
  if (!row) throw new Error(`nothing changed for ${ref}`);
15570
15608
  if (onMap) {
15571
- const next2 = card?.others[0];
15572
- 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.`;
15573
15611
  return `Thrown out: ${row.ref}${body?.trim() ? ` \u2014 ${body.trim()}` : ""}.${after}`;
15574
15612
  }
15575
15613
  const verb = kind === "accept" ? "Accepted" : "Rejected";
@@ -15586,123 +15624,36 @@ function normalizeRef(input, items) {
15586
15624
  return given;
15587
15625
  }
15588
15626
 
15589
- // src/commands/next.ts
15590
- async function next(opts) {
15627
+ // src/commands/build.ts
15628
+ async function build(ref, opts) {
15591
15629
  const { renderer, out } = rendererFor(opts.out);
15592
15630
  const auth = await authenticate();
15593
15631
  const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
15594
- const autonomy2 = await readAutonomy(auth.client, project.id);
15595
- if (autonomy2 === "propose") {
15596
- throw new Error(
15597
- `${project.name} is on "propose", so NanoPM may look but not touch.
15598
- nanopm autonomy execute_after_validation to let it change files, asking you first`
15599
- );
15600
- }
15601
- 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");
15602
- if (error) throw new Error(`could not read the solutions: ${error.message}`);
15603
- const accepted = items ?? [];
15604
- if (!accepted.length) {
15605
- throw new Error("nothing is accepted yet. Run `nanopm roadmap`, then `nanopm accept <ref>`.");
15606
- }
15607
- let item = accepted[0];
15608
- if (opts.ref) {
15609
- const wanted = normalizeRef(opts.ref, accepted);
15610
- const found = accepted.find((i) => i.ref === wanted);
15611
- if (!found) {
15612
- throw new Error(`${opts.ref} is not an accepted solution NanoPM can build. Accepted: ${accepted.map((i) => i.ref).join(", ")}.`);
15613
- }
15614
- item = found;
15615
- }
15616
15632
  const repo = await detectRepo(opts.cwd);
15617
- 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.");
15618
15640
  await rememberClone(auth.client, project.id, repo.root);
15619
- const daemonId = await localDaemonId();
15620
- const { data: job, error: queueError } = await auth.client.from("jobs").insert({
15621
- project_id: project.id,
15622
- skill: "next",
15623
- params: { solution_id: item.id, repo_path: repo.root },
15624
- requested_by: auth.userId,
15625
- target_daemon_id: daemonId
15626
- }).select("id").single();
15627
- if (queueError || !job) throw new Error(`could not queue the work: ${queueError?.message}`);
15628
- 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}`);
15629
15650
  const answerer = opts.answersFile ? await answersFromFile(opts.answersFile) : answersFromTerminal(out);
15630
- return streamJob(auth.client, job.id, { pollMs: 700, out, renderer, cancelSignal: opts.cancelSignal, answerer, verbose: verboseWanted() });
15631
- }
15632
- async function readAutonomy(client, projectId) {
15633
- const { data } = await client.from("project_preferences").select("autonomy").eq("project_id", projectId).maybeSingle();
15634
- return data?.autonomy ?? "propose";
15635
- }
15636
- var EXPLAINED = {
15637
- propose: "NanoPM reads and proposes. It never changes a file.",
15638
- execute_after_validation: "NanoPM changes files, on its own branch, after showing you what it will do and waiting for a yes.",
15639
- execute_then_report: "NanoPM acts first and tells you after. Not available yet."
15640
- };
15641
- async function autonomy(level, opts) {
15642
- const auth = await authenticate();
15643
- const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
15644
- const current = await readAutonomy(auth.client, project.id);
15645
- if (!level) {
15646
- const lines = [`${project.name} is on "${current}".`, ""];
15647
- for (const l of AUTONOMY_LEVELS) {
15648
- lines.push(` ${l === current ? "\u25CF" : "\u25CB"} ${l.padEnd(26)} ${EXPLAINED[l]}`);
15649
- }
15650
- lines.push("", " nanopm autonomy <level>");
15651
- return lines.join("\n");
15652
- }
15653
- const wanted = level.trim();
15654
- if (!AUTONOMY_LEVELS.includes(wanted)) {
15655
- throw new Error(`"${level}" is not a level. Pick one of: ${AUTONOMY_LEVELS.join(", ")}.`);
15656
- }
15657
- if (wanted === "execute_then_report") {
15658
- throw new Error(
15659
- "execute_then_report is not available yet. NanoPM asks before it changes anything until the review loop has been used in anger."
15660
- );
15661
- }
15662
- if (wanted === current) return `${project.name} is already on "${current}".`;
15663
- const { error } = await auth.client.from("project_preferences").update({ autonomy: wanted }).eq("project_id", project.id);
15664
- if (error) {
15665
- throw new Error(`could not change the dial: ${error.message}`);
15666
- }
15667
- return [`${project.name} is now on "${wanted}".`, EXPLAINED[wanted]].join("\n");
15668
- }
15669
- async function verifyCommand(command, opts) {
15670
- const auth = await authenticate();
15671
- const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
15672
- const { data } = await auth.client.from("project_preferences").select("verify_command").eq("project_id", project.id).maybeSingle();
15673
- const current = data?.verify_command ?? null;
15674
- if (!command && !opts.clear) {
15675
- if (!current) {
15676
- return [
15677
- `${project.name} has no check, so nothing verifies what NanoPM writes.`,
15678
- "",
15679
- ' nanopm verify "npm run lint && npm test"',
15680
- "",
15681
- "The daemon runs it in the repository root after a change and before the solution is",
15682
- "marked done. A failure goes back to NanoPM once, to fix."
15683
- ].join("\n");
15684
- }
15685
- return [`${project.name} verifies with:`, "", ` ${current}`, "", ' nanopm verify "<command>" \xB7 nanopm verify --clear'].join("\n");
15686
- }
15687
- const next2 = opts.clear ? null : command.trim();
15688
- if (next2 !== null && !next2) throw new Error('a check needs a command: nanopm verify "npm test"');
15689
- const { error } = await auth.client.from("project_preferences").update({ verify_command: next2 }).eq("project_id", project.id);
15690
- if (error) throw new Error(`could not save the check: ${error.message}`);
15691
- if (next2 === null) return `${project.name} no longer runs a check. Nothing will verify what NanoPM writes.`;
15692
- return [
15693
- `${project.name} will verify with:`,
15694
- "",
15695
- ` ${next2}`,
15696
- "",
15697
- "It runs in the repository root. If your change touches what the command itself runs \u2014",
15698
- "package.json, a lockfile, the script it calls \u2014 the check is skipped and you are told,",
15699
- "because an agent that can rewrite its own examiner is not being examined."
15700
- ].join("\n");
15651
+ return streamJob(auth.client, jobId, { pollMs: 700, out, renderer, cancelSignal: opts.cancelSignal, answerer, verbose: verboseWanted() });
15701
15652
  }
15702
15653
 
15703
15654
  // src/detect-sources.ts
15704
- import { readFile as readFile12 } from "fs/promises";
15705
- import { join as join14 } from "path";
15655
+ import { readFile as readFile14 } from "fs/promises";
15656
+ import { join as join16 } from "path";
15706
15657
  var ENV_FILES = [".env", ".env.local", ".env.development.local", ".env.development", ".env.example"];
15707
15658
  var POSTGRES_KEYS = [
15708
15659
  "DATABASE_URL",
@@ -15752,7 +15703,7 @@ async function envFiles(repoRoot) {
15752
15703
  const out = [];
15753
15704
  for (const file of ENV_FILES) {
15754
15705
  try {
15755
- out.push({ file, env: parseEnv(await readFile12(join14(repoRoot, file), "utf8")) });
15706
+ out.push({ file, env: parseEnv(await readFile14(join16(repoRoot, file), "utf8")) });
15756
15707
  } catch {
15757
15708
  continue;
15758
15709
  }
@@ -15812,7 +15763,7 @@ function describeUrl(url) {
15812
15763
  // src/format/closing.ts
15813
15764
  function closingLines(project, exit, env = process.env, mode = "onboard") {
15814
15765
  if (exit !== 0) return [];
15815
- 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";
15816
15767
  const chain = chainedAfter(mode).map((skill) => skillSentence(skill));
15817
15768
  return [
15818
15769
  "",
@@ -15820,7 +15771,7 @@ function closingLines(project, exit, env = process.env, mode = "onboard") {
15820
15771
  ...chain.length ? [`NanoPM is now ${joinAnd(chain)}; the Roadmap fills in as it goes. Watch it at ${webUrl(env)}/p/${project.slug}/journal`] : [],
15821
15772
  // The map is no longer chained after onboarding, so the first three clauses are what
15822
15773
  // builds it: what users said, where they leave traces, then the map that reads both.
15823
- `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`,
15824
15775
  "With a partner: nanopm invite <email> prints a link; they open it, or in their clone: nanopm login, then nanopm join <code>"
15825
15776
  ];
15826
15777
  }
@@ -16521,7 +16472,7 @@ async function seedWatches(watches2, read6) {
16521
16472
  }
16522
16473
 
16523
16474
  // src/commands/watch.ts
16524
- import { readFile as readFile13 } from "fs/promises";
16475
+ import { readFile as readFile15 } from "fs/promises";
16525
16476
  function cursorFor(source, watchId) {
16526
16477
  const all = source.cursor ?? {};
16527
16478
  return all[watchId] ?? {};
@@ -16582,7 +16533,7 @@ async function addWatch(labelOrId, id, opts) {
16582
16533
  const paint = painter(color);
16583
16534
  if (!labelOrId || !id) throw new Error('say which feed and what to call it: nanopm watch add <source> <id> --query "\u2026"');
16584
16535
  if (opts.query && opts.queryFile) throw new Error("--query or --query-file, not both");
16585
- const query = opts.queryFile ? await readFile13(opts.queryFile, "utf8") : opts.query ?? "";
16536
+ const query = opts.queryFile ? await readFile15(opts.queryFile, "utf8") : opts.query ?? "";
16586
16537
  const watch = watchInput(id, query, opts);
16587
16538
  const auth = await authenticate();
16588
16539
  const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
@@ -16769,9 +16720,9 @@ async function review(opts) {
16769
16720
  }
16770
16721
  const usable = connections.filter((c) => c.status === "ok");
16771
16722
  if (!usable.length) {
16772
- const failed = connections.map((c) => c.label).join(", ");
16723
+ const failed2 = connections.map((c) => c.label).join(", ");
16773
16724
  throw new Error(
16774
- `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.
16775
16726
  nanopm connect --list see what is wrong`
16776
16727
  );
16777
16728
  }
@@ -17406,12 +17357,12 @@ function renderDirection(rows2) {
17406
17357
  const out = [];
17407
17358
  for (const line of DIRECTION_ROWS) {
17408
17359
  const row = rows2.find((r) => r.key === directionKey(line));
17409
- const next2 = rows2.find((r) => r.key === directionKey(line, true));
17360
+ const next = rows2.find((r) => r.key === directionKey(line, true));
17410
17361
  out.push(`${DIRECTION_ROW_TITLE[line]}${mark(row)}`);
17411
17362
  out.push(` ${row?.content ?? "\u2014"}`);
17412
- if (next2) {
17413
- const because = reasonOf2(next2.sources);
17414
- 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}` : ""}`);
17415
17366
  }
17416
17367
  }
17417
17368
  out.push("Objective");
@@ -17451,13 +17402,13 @@ function render2(rows2) {
17451
17402
  const out = [];
17452
17403
  for (const line of PITCH_LINE_KEYS) {
17453
17404
  const row = rows2.find((r) => r.key === pitchKey(line));
17454
- const next2 = rows2.find((r) => r.key === pitchKey(line, true));
17405
+ const next = rows2.find((r) => r.key === pitchKey(line, true));
17455
17406
  const mark2 = row && row.provenance !== "stated" ? ` \xB7 to review${row.confidence != null ? ` (${row.confidence}% sure)` : ""}` : "";
17456
17407
  out.push(`${PITCH_LINE_TITLE[line]}${mark2}`);
17457
17408
  out.push(` ${row?.content ?? "\u2014"}`);
17458
- if (next2) {
17459
- const because = reasonOf3(next2.sources);
17460
- 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}` : ""}`);
17461
17412
  }
17462
17413
  }
17463
17414
  return out.join("\n");
@@ -17515,7 +17466,7 @@ async function invite(email, opts) {
17515
17466
  const [, ...rest] = inviteLines({ name: project.name, code, email: email.trim().toLowerCase(), web: webUrl() });
17516
17467
  return [`Invite to ${paint(ANSI.bold, project.name)}: ${paint(ANSI.bold, code)}`, ...rest.map((l) => l ? paint(ANSI.dim, l) : l)].join("\n");
17517
17468
  }
17518
- async function join15(code, opts) {
17469
+ async function join17(code, opts) {
17519
17470
  const normalized2 = code.trim().toUpperCase();
17520
17471
  if (!INVITE_CODE.test(normalized2)) throw new Error(`that does not look like an invite code: ten letters and digits, like ${"K7PQ2XM9RT"}`);
17521
17472
  const repo = await detectRepo(opts.cwd);
@@ -18216,7 +18167,7 @@ function buildProgram() {
18216
18167
  console.log(await invite(email, { cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
18217
18168
  });
18218
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) => {
18219
- console.log(await join15(code, { cwd: process.cwd(), color: colorOf(opts) }));
18170
+ console.log(await join17(code, { cwd: process.cwd(), color: colorOf(opts) }));
18220
18171
  });
18221
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) => {
18222
18173
  console.log(await members({ cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
@@ -18238,16 +18189,10 @@ function buildProgram() {
18238
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) => {
18239
18190
  console.log(await status({ cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
18240
18191
  });
18241
- 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) => {
18242
18193
  const cancel = new AbortController();
18243
18194
  process.once("SIGINT", () => cancel.abort());
18244
- process.exitCode = await next({
18245
- cwd: process.cwd(),
18246
- projectId: opts.project,
18247
- ref: ref ?? opts.ref,
18248
- answersFile: opts.answers,
18249
- cancelSignal: cancel.signal
18250
- });
18195
+ process.exitCode = await build(ref, { cwd: process.cwd(), projectId: opts.project, anyway: opts.anyway, answersFile: opts.answers, cancelSignal: cancel.signal });
18251
18196
  });
18252
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) => {
18253
18198
  console.log(await autonomy(level, { cwd: process.cwd(), projectId: opts.project }));