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