@nanopm/cli 0.1.17 → 0.1.19

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
@@ -5335,6 +5335,8 @@ var BUILD_SKILL = "build";
5335
5335
  var BUILD_STEPS_MIN = 1;
5336
5336
  var BUILD_STEPS_MAX = 8;
5337
5337
  var BUILD_STEP_MAX = 120;
5338
+ var BUILD_UNDERSTOOD_MIN = 40;
5339
+ var BUILD_UNDERSTOOD_MAX = 600;
5338
5340
  var BUILD_RESUMES_MAX = 3;
5339
5341
  var BUILD_TRAILER = "Co-Authored-By: NanoPM <noreply@nanopm.app>";
5340
5342
  var BUILD_COLUMNS = "id, ref, title, status, actor, agent_spec, spec";
@@ -5369,9 +5371,10 @@ function stepsLeft(plan, done) {
5369
5371
  function buildDir(home, jobId) {
5370
5372
  return `${home.replace(/\/+$/, "")}/.cache/nanopm/builds/${jobId}`;
5371
5373
  }
5372
- function buildBranch(ref, title) {
5374
+ function buildBranch(ref, title, jobId) {
5373
5375
  const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
5374
- return `nano/${ref.toLowerCase()}${slug ? `-${slug}` : ""}`;
5376
+ const own = jobId.replace(/[^a-z0-9]/gi, "").slice(0, 6).toLowerCase();
5377
+ return `nano/${ref.toLowerCase()}${slug ? `-${slug}` : ""}-${own}`;
5375
5378
  }
5376
5379
  var REGISTRIES = {
5377
5380
  "package-lock.json": ["registry.npmjs.org"],
@@ -5420,7 +5423,7 @@ var isDecideForMe = (answer) => /^\s*decide(\s+for\s+me)?\s*\.?\s*$/i.test(answe
5420
5423
  function answerForAgent(answer) {
5421
5424
  return isDecideForMe(answer) ? DECIDE_FOR_ME : answer;
5422
5425
  }
5423
- var BUILD_HISTORY_KINDS = ["worktree", "plan", "step", "question", "answer", "paused", "pr", "handed", "throw", "thrown"];
5426
+ var BUILD_HISTORY_KINDS = ["worktree", "plan", "step", "question", "answer", "paused", "pr", "handed", "throw", "thrown", "closed"];
5424
5427
  function originOf(job) {
5425
5428
  const named = (job.params ?? {}).origin;
5426
5429
  return typeof named === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(named) ? named : job.id;
@@ -5662,11 +5665,12 @@ async function ghReady(cwd) {
5662
5665
  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";
5663
5666
  }
5664
5667
  }
5665
- async function pullRequestMerged(cwd, number) {
5668
+ async function pullRequestState(cwd, number) {
5666
5669
  try {
5667
- return await gh(cwd, ["pr", "view", String(number), "--json", "state", "--jq", ".state"]) === "MERGED";
5670
+ const { state, headRefName } = JSON.parse(await gh(cwd, ["pr", "view", String(number), "--json", "state,headRefName"]));
5671
+ return (state === "MERGED" || state === "CLOSED" || state === "OPEN") && typeof headRefName === "string" ? { state, head: headRefName } : null;
5668
5672
  } catch {
5669
- return false;
5673
+ return null;
5670
5674
  }
5671
5675
  }
5672
5676
  async function openPullRequest(cwd, pr) {
@@ -11226,7 +11230,7 @@ async function prepareBuild(client, job, clone, adapter) {
11226
11230
  await startSolution(client, job.project_id, solution.ref, job.id);
11227
11231
  return { ok: true, build: { solution, clone, ...place, worktree: worktree2, network: await network(place.dir), history } };
11228
11232
  }
11229
- const branch = buildBranch(solution.ref, solution.title);
11233
+ const branch = buildBranch(solution.ref, solution.title, job.id);
11230
11234
  await mkdir5(join13(dir, ".."), { recursive: true, mode: 448 });
11231
11235
  const { base, fork, worktree } = await addWorktree(clone, dir, branch);
11232
11236
  made = true;
@@ -11269,9 +11273,10 @@ async function runBuildJob(input) {
11269
11273
  let end = { stop: failed("the agent produced no result") };
11270
11274
  for await (const event of adapter.run({ ...spec2, prompt, resumeSessionId: resume3 ? spec2.sessionId : void 0 })) {
11271
11275
  await input.onEvent(event);
11272
- if (event.type === "tool_use" && event.tool === PLAN_TOOL && !plan) {
11276
+ if (event.type === "tool_result" && event.tool === PLAN_TOOL && !event.isError && !plan) {
11273
11277
  plan = stepsOf(event.input);
11274
- if (plan) await events.write("plan", { steps: plan });
11278
+ const understood = event.input?.understood;
11279
+ if (plan) await events.write("plan", { steps: plan, ...typeof understood === "string" && understood.trim() ? { understood: understood.trim().slice(0, BUILD_UNDERSTOOD_MAX) } : {} });
11275
11280
  } else if (event.type === "tool_result" && event.tool === STEP_TOOL && !event.isError) {
11276
11281
  const { step, summary } = event.input ?? {};
11277
11282
  const index = Number(step) - 1;
@@ -11513,26 +11518,40 @@ async function throwAwayBuilds(client, daemonId, userId) {
11513
11518
  }
11514
11519
  return thrown;
11515
11520
  }
11516
- async function shippedBuilds(client, daemonId, userId) {
11517
- const { data: building } = await client.from("solutions").select("id, ref, project_id").eq("status", "building");
11521
+ async function settledBuilds(client, daemonId, userId) {
11522
+ const { data: building } = await client.from("solutions").select("id, ref, project_id").eq("status", "building").is("superseded_by", null);
11518
11523
  if (!building?.length) return [];
11519
11524
  const [{ data: built }, { data: clones }] = await Promise.all([
11520
- client.from("jobs").select("id, params, pr:result->pr").eq("skill", BUILD_SKILL).eq("status", "succeeded").eq("assigned_daemon_id", daemonId).eq("requested_by", userId).in("params->>solution_id", building.map((s) => s.id)),
11525
+ client.from("jobs").select("id, project_id, params, created_at, pr:result->pr").eq("skill", BUILD_SKILL).eq("status", "succeeded").eq("assigned_daemon_id", daemonId).eq("requested_by", userId).in("params->>solution_id", building.map((s) => s.id)).order("created_at", { ascending: false }),
11521
11526
  client.from("clones").select("project_id, repo_path").eq("daemon_id", daemonId)
11522
11527
  ]);
11528
+ const newest = [...new Map((built ?? []).reverse().map((j) => [j.params?.solution_id, j])).values()];
11529
+ const { data: closed } = newest.length ? await client.from("job_events").select("job_id").in("job_id", newest.map((j) => j.id)).eq("kind", "closed") : { data: [] };
11523
11530
  const cloneOf = new Map((clones ?? []).map((c) => [c.project_id, c.repo_path]));
11524
- const shipped = [];
11525
- for (const j of built ?? []) {
11531
+ const settled2 = [];
11532
+ for (const j of newest.filter((n) => !closed?.some((c) => c.job_id === n.id))) {
11526
11533
  const solution = building.find((s) => s.id === j.params?.solution_id);
11527
11534
  const number = j.pr?.number;
11528
11535
  const clone = solution ? cloneOf.get(solution.project_id) : void 0;
11529
- if (!solution || typeof number !== "number" || number <= 0 || !clone || !existsSync5(clone) || !await pullRequestMerged(clone, number)) continue;
11530
- const { error } = await client.rpc("finish_solution", { p_project_id: solution.project_id, p_ref: solution.ref, p_job_id: j.id });
11531
- if (error) continue;
11532
- await logActivity({ client }, { id: j.id, project_id: solution.project_id }, "shipped", `Pull request #${number} merged: ${solution.ref} shipped`);
11533
- shipped.push(j.id);
11534
- }
11535
- return shipped;
11536
+ if (!solution || typeof number !== "number" || number <= 0 || !clone || !existsSync5(clone)) continue;
11537
+ const [{ place }, pr] = await Promise.all([loadHistory(client, j), pullRequestState(clone, number)]);
11538
+ if (!place || !pr || pr.head !== place.branch) continue;
11539
+ const at = { id: j.id, project_id: solution.project_id };
11540
+ if (pr.state === "MERGED") {
11541
+ const { error } = await client.rpc("finish_solution", { p_project_id: solution.project_id, p_ref: solution.ref, p_job_id: j.id });
11542
+ if (error) continue;
11543
+ await logActivity({ client }, at, "shipped", `Pull request #${number} merged: ${solution.ref} shipped`);
11544
+ settled2.push({ id: j.id, ref: solution.ref, merged: true });
11545
+ } else if (pr.state === "CLOSED") {
11546
+ const { error } = await client.rpc("unstart_solution", { p_project_id: solution.project_id, p_ref: solution.ref, p_to: wasOf(j.params) });
11547
+ if (error) continue;
11548
+ await deleteBranch(clone, place.branch);
11549
+ await appendJobEvent(client, j.id, "closed", { number }, "nanopm");
11550
+ await logActivity({ client }, at, "closed", `Pull request #${number} closed without merging: ${solution.ref} back on the roadmap`);
11551
+ settled2.push({ id: j.id, ref: solution.ref, merged: false });
11552
+ }
11553
+ }
11554
+ return settled2;
11536
11555
  }
11537
11556
  async function tellTheApp(accessToken, jobId, fetchImpl = fetch) {
11538
11557
  try {
@@ -11696,7 +11715,7 @@ async function runDaemon(options = {}) {
11696
11715
  }
11697
11716
  if (!options.once && Date.now() - lastShipCheck > SHIP_CHECK_MS) {
11698
11717
  lastShipCheck = Date.now();
11699
- for (const id of await shippedBuilds(auth.client, daemonId, auth.userId).catch(() => [])) log(`build ${id}: its pull request merged, shipped`);
11718
+ for (const b of await settledBuilds(auth.client, daemonId, auth.userId).catch(() => [])) log(`build ${b.id}: its pull request ${b.merged ? `merged, ${b.ref} shipped` : `closed without merging, ${b.ref} back on the roadmap`}`);
11700
11719
  }
11701
11720
  if (!options.once && Date.now() - lastDeployCheck > DEPLOY_CHECK_MS) {
11702
11721
  lastDeployCheck = Date.now();
@@ -12811,8 +12830,8 @@ ${renderMap()}`,
12811
12830
  server.registerTool(
12812
12831
  "build_plan",
12813
12832
  {
12814
- 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.`,
12815
- inputSchema: { steps: z21.array(z21.string().min(3).max(BUILD_STEP_MAX)).min(BUILD_STEPS_MIN).max(BUILD_STEPS_MAX) }
12833
+ description: `Say what you understood and your plan before you change anything. \`understood\`: in your own words and plain language, the problem, what you will change, and what you will not touch \u2014 a short paragraph the founder reads first, so a misunderstanding shows before any code (at most ${BUILD_UNDERSTOOD_MAX} characters). \`steps\`: ${BUILD_STEPS_MIN} to ${BUILD_STEPS_MAX}, in order, each one change the founder can recognise, in their words (at most ${BUILD_STEP_MAX} characters); the last is your own check of the whole change. Call it once; the founder sees the paragraph, then the steps as a checklist.`,
12834
+ inputSchema: { understood: z21.string().min(BUILD_UNDERSTOOD_MIN).max(BUILD_UNDERSTOOD_MAX), steps: z21.array(z21.string().min(3).max(BUILD_STEP_MAX)).min(BUILD_STEPS_MIN).max(BUILD_STEPS_MAX) }
12816
12835
  },
12817
12836
  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")
12818
12837
  );
@@ -14651,7 +14670,7 @@ import { promisify as promisify8 } from "util";
14651
14670
  // package.json
14652
14671
  var package_default = {
14653
14672
  name: "@nanopm/cli",
14654
- version: "0.1.17",
14673
+ version: "0.1.19",
14655
14674
  description: "An autonomous product manager for builders who run several small products. Runs on your machine, drives your own coding harness.",
14656
14675
  type: "module",
14657
14676
  license: "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanopm/cli",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "An autonomous product manager for builders who run several small products. Runs on your machine, drives your own coding harness.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,8 +24,11 @@ NanoPM commits each step for you and opens the pull request when you are done.
24
24
  2. If the code contradicts the spec — what it asks already exists, a step it assumes is not
25
25
  there, the size does not hold — `ask_user` before you plan: what the spec says, what the code
26
26
  has, what it changes, and the options. Do not guess what the founder decided.
27
- 3. `build_plan`: your plan, 1 to 8 steps, in order, each a change the founder can recognise, in
28
- their words. The last step is your check of the whole change.
27
+ 3. `build_plan`: first, in `understood`, what you understood, in your own words and plain
28
+ language — the problem, what you will change, what you will not touch. The founder reads it
29
+ before anything else; a misunderstanding must show there, not in the pull request. Then your
30
+ plan, 1 to 8 steps, in order, each a change the founder can recognise, in their words. The last
31
+ step is your check of the whole change.
29
32
  4. Build one step at a time. After each: run its tests and the project's own checks (the ones
30
33
  CLAUDE.md, the README or `package.json` name), fix what they report, then `build_step` with the
31
34
  step's number and one line on what you did. NanoPM commits right then, so a step is committed