@nanopm/cli 0.1.11 → 0.1.13
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 +611 -317
- package/package.json +1 -1
- package/skills/build/skill.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"],
|
|
@@ -391,7 +391,7 @@ var SkillManifestSchema = z2.object({
|
|
|
391
391
|
/**
|
|
392
392
|
* Run only when the founder asks for it on one item, by a click or a command, never by Nano on
|
|
393
393
|
* its own: the autonomy dial, which governs what Nano does unasked, does not cap it
|
|
394
|
-
* (`capCapabilities`). A build is the one today (docs/⏳nanopm-
|
|
394
|
+
* (`capCapabilities`). A build is the one today (docs/⏳nanopm-76-build-it.md, settled 2).
|
|
395
395
|
*/
|
|
396
396
|
asked: z2.boolean().default(false),
|
|
397
397
|
/** The first user message sent to the harness, unless the job params provide one. */
|
|
@@ -673,6 +673,18 @@ function formatStreamEvent(event) {
|
|
|
673
673
|
return "\u2298 cancel requested";
|
|
674
674
|
case "error":
|
|
675
675
|
return `\u2717 ${String(p.code ?? "error")}: ${String(p.message ?? "")}`;
|
|
676
|
+
// A build (docs/⏳nanopm-76-build-it.md): its plan, each step with its commit, a pause, the pull request.
|
|
677
|
+
case "plan":
|
|
678
|
+
return ["The plan:", ...(Array.isArray(p.steps) ? p.steps : []).map((s, i) => ` ${i + 1}. ${String(s)}`)].join("\n");
|
|
679
|
+
case "step":
|
|
680
|
+
return `\u2713 ${String(p.step ?? "")}. ${String(p.says ?? "")}${p.commit ? ` \xB7 ${String(p.commit)}` : ""}`;
|
|
681
|
+
case "paused": {
|
|
682
|
+
const at = new Date(String(p.resets_at ?? ""));
|
|
683
|
+
const when = Number.isNaN(at.getTime()) ? "when it resets" : `at ${at.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`;
|
|
684
|
+
return `\u25CC paused: your Claude limit is reached. Nano carries on by itself ${when}.`;
|
|
685
|
+
}
|
|
686
|
+
case "pr":
|
|
687
|
+
return `\u2387 draft pull request #${String(p.number ?? "")} \xB7 ${String(p.url ?? "")}`;
|
|
676
688
|
default:
|
|
677
689
|
return null;
|
|
678
690
|
}
|
|
@@ -2177,12 +2189,14 @@ function boardOf(all, solutions) {
|
|
|
2177
2189
|
walk(tree);
|
|
2178
2190
|
const cards2 = [];
|
|
2179
2191
|
for (const [leafId, list] of byLeaf) {
|
|
2180
|
-
const standing = list.filter((s) => s.status === "proposed");
|
|
2181
|
-
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;
|
|
2182
2196
|
const leaf = nodes.get(leafId);
|
|
2183
2197
|
const branch = leaf ? branchOf(tree, leaf.id) : null;
|
|
2184
|
-
if (!pick3 || !leaf || !branch) continue;
|
|
2185
|
-
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 });
|
|
2186
2200
|
}
|
|
2187
2201
|
const col = (h) => HORIZONS.indexOf(h);
|
|
2188
2202
|
cards2.sort((a, b) => col(a.horizon) - col(b.horizon) || a.place - b.place || a.pick.created_at.localeCompare(b.pick.created_at));
|
|
@@ -3673,7 +3687,7 @@ function buildSandboxSettings(home) {
|
|
|
3673
3687
|
}
|
|
3674
3688
|
return { permissions: { deny, defaultMode: "default" } };
|
|
3675
3689
|
}
|
|
3676
|
-
function sandboxSettingsFor(home, grantedTools, network = []) {
|
|
3690
|
+
function sandboxSettingsFor(home, grantedTools, network = [], answerFile) {
|
|
3677
3691
|
const settings = buildSandboxSettings(home);
|
|
3678
3692
|
const granted = new Set(grantedTools);
|
|
3679
3693
|
settings.permissions.deny = settings.permissions.deny.filter((rule) => !granted.has(rule));
|
|
@@ -3685,8 +3699,14 @@ function sandboxSettingsFor(home, grantedTools, network = []) {
|
|
|
3685
3699
|
network: { allowedDomains: network.filter((h) => !/(^|\.)github(usercontent)?\.com$/.test(h)) }
|
|
3686
3700
|
};
|
|
3687
3701
|
}
|
|
3702
|
+
if (answerFile) settings.hooks = { PreToolUse: [{ matcher: "mcp__nanopm__ask_user", hooks: [{ type: "command", command: askHook(answerFile) }] }] };
|
|
3688
3703
|
return settings;
|
|
3689
3704
|
}
|
|
3705
|
+
function askHook(answerFile) {
|
|
3706
|
+
const quote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
3707
|
+
const decision = (d) => quote(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: d } }));
|
|
3708
|
+
return `if [ -f ${quote(answerFile)} ]; then mv ${quote(answerFile)} ${quote(`${answerFile}.taken`)}; printf '%s' ${decision("allow")}; else printf '%s' ${decision("defer")}; fi`;
|
|
3709
|
+
}
|
|
3690
3710
|
|
|
3691
3711
|
// ../daemon/src/adapters/claude-code/stream.ts
|
|
3692
3712
|
var RESULT_FAILURES = {
|
|
@@ -3704,6 +3724,8 @@ var StreamParser = class {
|
|
|
3704
3724
|
model = "unknown";
|
|
3705
3725
|
toolCalls = /* @__PURE__ */ new Map();
|
|
3706
3726
|
sawResult = false;
|
|
3727
|
+
/** When the Claude window opens again, from the last rejected `rate_limit_event`. */
|
|
3728
|
+
resetsAt;
|
|
3707
3729
|
// The read size, counted here because the stream is the one place every call passes.
|
|
3708
3730
|
contextPeak = 0;
|
|
3709
3731
|
readCalls = {};
|
|
@@ -3714,6 +3736,9 @@ var StreamParser = class {
|
|
|
3714
3736
|
get currentSessionId() {
|
|
3715
3737
|
return this.sessionId;
|
|
3716
3738
|
}
|
|
3739
|
+
get limitResetsAt() {
|
|
3740
|
+
return this.resetsAt;
|
|
3741
|
+
}
|
|
3717
3742
|
/** Feed one NDJSON line; get zero or more events. Non-JSON lines are ignored. */
|
|
3718
3743
|
parseLine(line) {
|
|
3719
3744
|
const trimmed = line.trim();
|
|
@@ -3765,10 +3790,18 @@ var StreamParser = class {
|
|
|
3765
3790
|
}
|
|
3766
3791
|
return events;
|
|
3767
3792
|
}
|
|
3793
|
+
case "rate_limit_event": {
|
|
3794
|
+
const info = obj.rate_limit_info;
|
|
3795
|
+
if (info?.status === "rejected" && typeof info.resetsAt === "number") this.resetsAt = info.resetsAt;
|
|
3796
|
+
return [];
|
|
3797
|
+
}
|
|
3768
3798
|
case "result": {
|
|
3769
3799
|
this.sawResult = true;
|
|
3770
3800
|
this.sessionId = obj.session_id ?? this.sessionId;
|
|
3771
3801
|
const usage = this.usageFrom(obj);
|
|
3802
|
+
if (obj.stop_reason === "tool_deferred" && obj.deferred_tool_use?.name) {
|
|
3803
|
+
return [{ type: "deferred", sessionId: this.sessionId ?? "", tool: obj.deferred_tool_use.name, input: obj.deferred_tool_use.input, usage }];
|
|
3804
|
+
}
|
|
3772
3805
|
if (obj.subtype === "success" && !obj.is_error) {
|
|
3773
3806
|
if (this.structuredOutput && obj.structured_output === void 0) {
|
|
3774
3807
|
return [{ type: "failed", sessionId: this.sessionId, code: "harness_error", message: "Claude omitted the required structured output." }];
|
|
@@ -3777,7 +3810,7 @@ var StreamParser = class {
|
|
|
3777
3810
|
}
|
|
3778
3811
|
const message = obj.result || `claude ended with ${obj.subtype ?? "an unknown result"}`;
|
|
3779
3812
|
const code = isRateLimit(message) ? "rate_limit" : RESULT_FAILURES[obj.subtype ?? ""] ?? "harness_error";
|
|
3780
|
-
return [{ type: "failed", sessionId: this.sessionId, code, message }];
|
|
3813
|
+
return [{ type: "failed", sessionId: this.sessionId, code, message, ...code === "rate_limit" && this.resetsAt ? { resetsAt: this.resetsAt } : {} }];
|
|
3781
3814
|
}
|
|
3782
3815
|
default:
|
|
3783
3816
|
return [];
|
|
@@ -3993,7 +4026,7 @@ var ClaudeCodeAdapter = class {
|
|
|
3993
4026
|
await writeFile2(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 384 });
|
|
3994
4027
|
const settingsPath = join3(spec2.runDir, "settings.json");
|
|
3995
4028
|
const flags = claudeCodeToolFlags(spec2.capabilities, { workspace: spec2.cwd, harnessWeb: spec2.harnessWeb, without: spec2.withoutTools });
|
|
3996
|
-
await writeFile2(settingsPath, JSON.stringify(sandboxSettingsFor(spec2.env.HOME ?? "", flags.tools, spec2.network)), { mode: 384 });
|
|
4029
|
+
await writeFile2(settingsPath, JSON.stringify(sandboxSettingsFor(spec2.env.HOME ?? "", flags.tools, spec2.network, spec2.answerFile)), { mode: 384 });
|
|
3997
4030
|
const child = spawn(this.claudePath, this.buildArgs(spec2, mcpConfigPath, settingsPath), {
|
|
3998
4031
|
cwd: spec2.cwd,
|
|
3999
4032
|
env: spec2.env,
|
|
@@ -4024,7 +4057,8 @@ var ClaudeCodeAdapter = class {
|
|
|
4024
4057
|
const { code } = await exit;
|
|
4025
4058
|
if (!parser.finished) {
|
|
4026
4059
|
const failure2 = classifyExit(stderr, aborted);
|
|
4027
|
-
|
|
4060
|
+
const resetsAt = failure2.code === "rate_limit" ? parser.limitResetsAt : void 0;
|
|
4061
|
+
yield { type: "failed", sessionId: parser.currentSessionId, ...failure2, message: appendExit(failure2.message, code), ...resetsAt ? { resetsAt } : {} };
|
|
4028
4062
|
}
|
|
4029
4063
|
} finally {
|
|
4030
4064
|
spec2.signal.removeEventListener("abort", onAbort);
|
|
@@ -4052,7 +4086,7 @@ function parsePrintResult(stdout) {
|
|
|
4052
4086
|
import { mkdir as mkdir6, readdir as readdir3, readFile as readFile9, rm as rm5, writeFile as writeFile6 } from "fs/promises";
|
|
4053
4087
|
import { openSync, closeSync, unlinkSync, writeFileSync, readFileSync as readFileSyncNode, mkdirSync } from "fs";
|
|
4054
4088
|
import { hostname as hostname2 } from "os";
|
|
4055
|
-
import { join as
|
|
4089
|
+
import { join as join14 } from "path";
|
|
4056
4090
|
import { setTimeout as sleep } from "timers/promises";
|
|
4057
4091
|
|
|
4058
4092
|
// ../daemon/src/awake.ts
|
|
@@ -4111,11 +4145,14 @@ async function latestVersion(fetchImpl = fetch) {
|
|
|
4111
4145
|
return null;
|
|
4112
4146
|
}
|
|
4113
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
|
+
}
|
|
4114
4151
|
async function installVersion(version, home = homedir2()) {
|
|
4115
4152
|
if (!RELEASE.test(version)) return { ok: false, why: `not a version: ${version}` };
|
|
4116
4153
|
const runtime = join4(root(home), "runtime", "bin");
|
|
4117
4154
|
try {
|
|
4118
|
-
await execFileAsync2(join4(runtime, "npm"),
|
|
4155
|
+
await execFileAsync2(join4(runtime, "npm"), installArgs(version, home), {
|
|
4119
4156
|
timeout: 5 * 6e4,
|
|
4120
4157
|
env: { ...process.env, PATH: `${runtime}:${process.env.PATH ?? ""}`, NPM_CONFIG_UPDATE_NOTIFIER: "false" }
|
|
4121
4158
|
});
|
|
@@ -4137,6 +4174,7 @@ function shouldUpdate(s) {
|
|
|
4137
4174
|
}
|
|
4138
4175
|
|
|
4139
4176
|
// ../daemon/src/mcp.ts
|
|
4177
|
+
import { join as join5 } from "path";
|
|
4140
4178
|
function memoryMcpCommand(env = process.env) {
|
|
4141
4179
|
const override = env.NANOPM_MCP_COMMAND;
|
|
4142
4180
|
if (override) {
|
|
@@ -4163,13 +4201,17 @@ function memoryMcpServer(job, auth, askTimeoutMs, workspace) {
|
|
|
4163
4201
|
NANOPM_PROJECT_ID: job.project_id,
|
|
4164
4202
|
NANOPM_USER_ID: auth.userId,
|
|
4165
4203
|
NANOPM_ACTOR: job.skill,
|
|
4166
|
-
|
|
4204
|
+
// A build: its questions come back through the answer file the hook hands over (buildAnswerFile).
|
|
4205
|
+
...workspace ? { NANOPM_WORKSPACE: workspace, NANOPM_ANSWER_FILE: buildAnswerFile(job.id) } : {},
|
|
4167
4206
|
NANOPM_ASK_TIMEOUT_MS: String(askTimeoutMs),
|
|
4168
4207
|
PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
4169
4208
|
HOME: process.env.HOME ?? ""
|
|
4170
4209
|
}
|
|
4171
4210
|
};
|
|
4172
4211
|
}
|
|
4212
|
+
function buildAnswerFile(jobId) {
|
|
4213
|
+
return join5(nanopmHome(), "run", jobId, "answer");
|
|
4214
|
+
}
|
|
4173
4215
|
|
|
4174
4216
|
// ../connectors/src/postgres.ts
|
|
4175
4217
|
import postgres from "postgres";
|
|
@@ -5286,7 +5328,7 @@ import { promisify as promisify4 } from "util";
|
|
|
5286
5328
|
// ../daemon/src/repo.ts
|
|
5287
5329
|
import { execFile as execFile3 } from "child_process";
|
|
5288
5330
|
import { readFile as readFile2, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
5289
|
-
import { join as
|
|
5331
|
+
import { join as join6 } from "path";
|
|
5290
5332
|
|
|
5291
5333
|
// ../shared/src/build.ts
|
|
5292
5334
|
var BUILD_SKILL = "build";
|
|
@@ -5296,11 +5338,26 @@ var BUILD_STEP_MAX = 120;
|
|
|
5296
5338
|
var BUILD_RESUMES_MAX = 3;
|
|
5297
5339
|
var BUILD_TRAILER = "Co-Authored-By: NanoPM <noreply@nanopm.app>";
|
|
5298
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
|
+
}
|
|
5299
5356
|
function buildRefusal(solution, opts = {}) {
|
|
5300
5357
|
if (solution.actor === "founder") return `${solution.ref} is yours to do, not Nano's to build.`;
|
|
5301
5358
|
if (!["proposed", "accepted", "building"].includes(solution.status)) return `${solution.ref} is ${solution.status}: only a solution on the roadmap can be built.`;
|
|
5302
5359
|
if (!solution.agent_spec) return `${solution.ref} has no agent spec yet. Write its specs first: nanopm spec ${solution.ref} --write`;
|
|
5303
|
-
const open = solution.spec
|
|
5360
|
+
const open = openQuestions(solution.spec);
|
|
5304
5361
|
if (open && !opts.anyway) {
|
|
5305
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`;
|
|
5306
5363
|
}
|
|
@@ -5340,7 +5397,7 @@ async function queueBuild(client, ask7) {
|
|
|
5340
5397
|
if (readError) throw new Error(`could not read the solution: ${pgMessage(readError)}`);
|
|
5341
5398
|
if (!data) throw new Error(`No solution ${ask7.ref} on the Roadmap.`);
|
|
5342
5399
|
const solution = data;
|
|
5343
|
-
const refused = buildRefusal(solution, { anyway: ask7.anyway });
|
|
5400
|
+
const refused = buildRefusal(solution, { anyway: ask7.anyway || Boolean(ask7.resumeOf) });
|
|
5344
5401
|
if (refused) throw new Error(refused);
|
|
5345
5402
|
const { data: job, error } = await client.from("jobs").insert({
|
|
5346
5403
|
project_id: ask7.projectId,
|
|
@@ -5348,12 +5405,55 @@ async function queueBuild(client, ask7) {
|
|
|
5348
5405
|
requested_by: ask7.userId,
|
|
5349
5406
|
trigger: "user",
|
|
5350
5407
|
target_daemon_id: ask7.daemonId,
|
|
5351
|
-
|
|
5408
|
+
// `was`: where the solution goes back on the board if the build stops (`unstart_solution`).
|
|
5409
|
+
// `origin`: the run that made the copy of the code, which every continuation works in.
|
|
5410
|
+
params: { solution_id: solution.id, repo_path: ask7.repoPath, was: ask7.resumeOf?.was ?? solution.status, ...ask7.resumeOf ? { origin: ask7.resumeOf.origin } : {}, ...ask7.anyway ? { anyway: true } : {} },
|
|
5411
|
+
// Continuing a stopped build takes up its worktree, its history and its session.
|
|
5412
|
+
...ask7.resumeOf ? { resume_of: ask7.resumeOf.id } : {}
|
|
5352
5413
|
}).select("id").single();
|
|
5353
5414
|
if (error?.code === "23505") throw new Error("A build of this product is already running. One at a time: `nanopm status` says where it is.");
|
|
5354
5415
|
if (error || !job) throw new Error(`could not queue the build: ${error ? pgMessage(error) : "no row"}`);
|
|
5355
5416
|
return { jobId: job.id, solution };
|
|
5356
5417
|
}
|
|
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);
|
|
5420
|
+
function answerForAgent(answer) {
|
|
5421
|
+
return isDecideForMe(answer) ? DECIDE_FOR_ME : answer;
|
|
5422
|
+
}
|
|
5423
|
+
var BUILD_HISTORY_KINDS = ["worktree", "plan", "step", "question", "answer", "paused", "pr"];
|
|
5424
|
+
function trustedPlace(p, at) {
|
|
5425
|
+
const [branch, base, fork] = [p.branch, p.base, p.fork].map((v) => typeof v === "string" ? v : "");
|
|
5426
|
+
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;
|
|
5427
|
+
return { dir: buildDir(at.home, at.jobId), branch, base, fork };
|
|
5428
|
+
}
|
|
5429
|
+
function buildHistory(events, at) {
|
|
5430
|
+
let place = null;
|
|
5431
|
+
let plan = null;
|
|
5432
|
+
const done = /* @__PURE__ */ new Set();
|
|
5433
|
+
let last = null;
|
|
5434
|
+
const answers = /* @__PURE__ */ new Map();
|
|
5435
|
+
for (const e of events) {
|
|
5436
|
+
if (e.kind === "worktree") place = trustedPlace(e.payload, at);
|
|
5437
|
+
else if (e.kind === "plan" && Array.isArray(e.payload.steps)) plan = e.payload.steps;
|
|
5438
|
+
else if (e.kind === "step" && typeof e.payload.step === "number") done.add(e.payload.step - 1);
|
|
5439
|
+
else if (e.kind === "answer") answers.set(e.seq, String(e.payload.answer ?? ""));
|
|
5440
|
+
if (e.kind === "question" || e.kind === "paused") last = e;
|
|
5441
|
+
else if (e.kind === "plan" || e.kind === "step") last = null;
|
|
5442
|
+
}
|
|
5443
|
+
if (!place) return { place, plan, done, resume: null };
|
|
5444
|
+
if (last?.kind === "question" && answers.has(last.seq) && last.payload.about === "usage") return { place, plan, done, resume: { why: "limit" } };
|
|
5445
|
+
if (last?.kind === "question" && answers.has(last.seq)) return { place, plan, done, resume: { why: "answer", answer: answers.get(last.seq) } };
|
|
5446
|
+
if (last?.kind === "paused") return { place, plan, done, resume: { why: "limit" } };
|
|
5447
|
+
return { place, plan, done, resume: { why: "lost" } };
|
|
5448
|
+
}
|
|
5449
|
+
function wasOf(params) {
|
|
5450
|
+
return params?.was === "proposed" ? "proposed" : "accepted";
|
|
5451
|
+
}
|
|
5452
|
+
async function loadBuildHistory(client, jobIds, at) {
|
|
5453
|
+
const { data, error } = await client.from("job_events").select("kind, seq, payload").in("job_id", [...jobIds]).in("kind", [...BUILD_HISTORY_KINDS]).order("id");
|
|
5454
|
+
if (error) throw new Error(`could not read the build's history: ${error.message}`);
|
|
5455
|
+
return buildHistory((data ?? []).map((e) => ({ kind: e.kind, seq: e.seq, payload: e.payload ?? {} })), at);
|
|
5456
|
+
}
|
|
5357
5457
|
|
|
5358
5458
|
// ../daemon/src/repo.ts
|
|
5359
5459
|
import { promisify as promisify3 } from "util";
|
|
@@ -5369,10 +5469,10 @@ var LOCKED = {
|
|
|
5369
5469
|
};
|
|
5370
5470
|
async function exec(cmd, repo, args, timeoutMs = TIMEOUT_MS) {
|
|
5371
5471
|
if (typeof repo === "string") return execFileAsync3(cmd, cmd === "git" ? ["-C", repo, ...args] : args, { cwd: repo, maxBuffer: MAX_BUFFER, timeout: timeoutMs });
|
|
5372
|
-
await writeFile4(
|
|
5472
|
+
await writeFile4(join6(repo.dir, ".git"), `gitdir: ${repo.admin}
|
|
5373
5473
|
`);
|
|
5374
|
-
await writeFile4(
|
|
5375
|
-
await rm3(
|
|
5474
|
+
await writeFile4(join6(repo.admin, "commondir"), repo.commondir);
|
|
5475
|
+
await rm3(join6(repo.admin, "config.worktree"), { force: true });
|
|
5376
5476
|
return execFileAsync3(cmd, args, { cwd: repo.dir, env: { ...process.env, GIT_DIR: repo.admin, GIT_WORK_TREE: repo.dir, ...LOCKED }, maxBuffer: MAX_BUFFER, timeout: timeoutMs });
|
|
5377
5477
|
}
|
|
5378
5478
|
async function git(repo, args, timeoutMs = TIMEOUT_MS) {
|
|
@@ -5441,7 +5541,7 @@ async function addWorktree(clone, dir, branch) {
|
|
|
5441
5541
|
const onto = await baseRef(clone, base);
|
|
5442
5542
|
await git(clone, ["worktree", "add", "-b", branch, dir, onto]);
|
|
5443
5543
|
const admin = await git(dir, ["rev-parse", "--absolute-git-dir"]);
|
|
5444
|
-
const worktree = { dir, admin, commondir: await readFile2(
|
|
5544
|
+
const worktree = { dir, admin, commondir: await readFile2(join6(admin, "commondir"), "utf8") };
|
|
5445
5545
|
return { base, fork: await git(worktree, ["rev-parse", "HEAD"]), worktree };
|
|
5446
5546
|
}
|
|
5447
5547
|
async function removeWorktree(clone, dir) {
|
|
@@ -5476,6 +5576,13 @@ async function ghReady(cwd) {
|
|
|
5476
5576
|
return /ENOENT/.test(errorMessage(err)) ? "the GitHub CLI is not installed on this machine: brew install gh, then gh auth login" : "the GitHub CLI is not signed in on this machine: run gh auth login in your terminal";
|
|
5477
5577
|
}
|
|
5478
5578
|
}
|
|
5579
|
+
async function pullRequestMerged(cwd, number) {
|
|
5580
|
+
try {
|
|
5581
|
+
return await gh(cwd, ["pr", "view", String(number), "--json", "state", "--jq", ".state"]) === "MERGED";
|
|
5582
|
+
} catch {
|
|
5583
|
+
return false;
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
5479
5586
|
async function openPullRequest(cwd, pr) {
|
|
5480
5587
|
try {
|
|
5481
5588
|
const out = await gh(cwd, ["pr", "create", "--draft", "--head", pr.branch, "--base", pr.base, "--title", pr.title, "--body", pr.body]);
|
|
@@ -5550,46 +5657,27 @@ async function reviewDeploys(client, daemonId, now = /* @__PURE__ */ new Date())
|
|
|
5550
5657
|
return results;
|
|
5551
5658
|
}
|
|
5552
5659
|
|
|
5553
|
-
// ../daemon/src/
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
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.";
|
|
5562
|
-
const { error } = await client.from("reviews").insert({
|
|
5563
|
-
project_id: job.project_id,
|
|
5564
|
-
summary,
|
|
5565
|
-
confirms: [],
|
|
5566
|
-
contradicts: [],
|
|
5567
|
-
job_id: job.id
|
|
5568
|
-
});
|
|
5569
|
-
if (error) return { status: "failed", error: { code: "harness_error", message: `could not record that there was nothing to read: ${error.message}` } };
|
|
5570
|
-
await client.from("activity_log").insert({
|
|
5571
|
-
project_id: job.project_id,
|
|
5572
|
-
job_id: job.id,
|
|
5573
|
-
actor: "daemon",
|
|
5574
|
-
step: "review.skipped",
|
|
5575
|
-
summary: attached ? "no working data source" : "nothing connected",
|
|
5576
|
-
outcome: "ok"
|
|
5577
|
-
});
|
|
5578
|
-
return { status: "succeeded", result: summary };
|
|
5579
|
-
}
|
|
5660
|
+
// ../daemon/src/build-job.ts
|
|
5661
|
+
import { existsSync as existsSync5 } from "fs";
|
|
5662
|
+
import { mkdir as mkdir5, readdir as readdir2, readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
|
|
5663
|
+
import { homedir as homedir4 } from "os";
|
|
5664
|
+
import { basename, join as join13 } from "path";
|
|
5665
|
+
|
|
5666
|
+
// ../daemon/src/problem-mapping-harness.ts
|
|
5667
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
5580
5668
|
|
|
5581
5669
|
// ../daemon/src/skills.ts
|
|
5582
5670
|
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
5583
5671
|
import { readFile as readFile3 } from "fs/promises";
|
|
5584
|
-
import { dirname as dirname2, join as
|
|
5672
|
+
import { dirname as dirname2, join as join7, resolve as resolve2 } from "path";
|
|
5585
5673
|
function findSkillsDir(env = process.env) {
|
|
5586
5674
|
if (env.NANOPM_SKILLS_DIR) return env.NANOPM_SKILLS_DIR;
|
|
5587
5675
|
const candidates = [
|
|
5588
|
-
|
|
5676
|
+
join7(process.cwd(), "skills"),
|
|
5589
5677
|
...process.argv[1] ? [resolve2(dirname2(process.argv[1]), "..", "skills")] : []
|
|
5590
5678
|
];
|
|
5591
5679
|
for (const dir of candidates) {
|
|
5592
|
-
if (existsSync2(
|
|
5680
|
+
if (existsSync2(join7(dir, "onboard", "skill.json"))) return dir;
|
|
5593
5681
|
}
|
|
5594
5682
|
throw new Error(`Cannot find the skills directory (tried ${candidates.join(", ")}). Set NANOPM_SKILLS_DIR.`);
|
|
5595
5683
|
}
|
|
@@ -5599,9 +5687,9 @@ function skillDirs(skillsDir, env = process.env) {
|
|
|
5599
5687
|
}
|
|
5600
5688
|
async function loadSkill(name, skillsDir = findSkillsDir()) {
|
|
5601
5689
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new Error(`Invalid skill name "${name}"`);
|
|
5602
|
-
const dir = skillDirs(skillsDir).map((d) =>
|
|
5690
|
+
const dir = skillDirs(skillsDir).map((d) => join7(d, name)).find((d) => existsSync2(join7(d, "skill.json")));
|
|
5603
5691
|
if (!dir) throw new Error(`Unknown skill "${name}"`);
|
|
5604
|
-
const raw = JSON.parse(await readFile3(
|
|
5692
|
+
const raw = JSON.parse(await readFile3(join7(dir, "skill.json"), "utf8"));
|
|
5605
5693
|
let manifest;
|
|
5606
5694
|
try {
|
|
5607
5695
|
manifest = parseSkillManifest(raw);
|
|
@@ -5610,7 +5698,7 @@ async function loadSkill(name, skillsDir = findSkillsDir()) {
|
|
|
5610
5698
|
}
|
|
5611
5699
|
const tainted = taintedByTheWeb(manifest);
|
|
5612
5700
|
if (tainted) throw new Error(`the "${name}" skill cannot be loaded: ${tainted}`);
|
|
5613
|
-
const body = await readFile3(
|
|
5701
|
+
const body = await readFile3(join7(dir, "SKILL.md"), "utf8");
|
|
5614
5702
|
return { manifest, body, dir };
|
|
5615
5703
|
}
|
|
5616
5704
|
var NOT_WITH_READ_WEB = ["write_memory", "problems", "propose", "review", "write_repo", "run_commands"];
|
|
@@ -5645,7 +5733,7 @@ function renderPrompt(template, params) {
|
|
|
5645
5733
|
var NEEDS_THE_REQUESTERS_CLONE = ["write_repo", "run_commands"];
|
|
5646
5734
|
var NEEDS_THE_REQUESTERS_IDENTITY = /* @__PURE__ */ new Set(["want", "start"]);
|
|
5647
5735
|
async function clonelessSkills(skillsDir = findSkillsDir()) {
|
|
5648
|
-
const names = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync2(
|
|
5736
|
+
const names = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync2(join7(skillsDir, d.name, "skill.json"))).map((d) => d.name);
|
|
5649
5737
|
const out = [];
|
|
5650
5738
|
for (const name of names) {
|
|
5651
5739
|
try {
|
|
@@ -5659,7 +5747,6 @@ async function clonelessSkills(skillsDir = findSkillsDir()) {
|
|
|
5659
5747
|
}
|
|
5660
5748
|
|
|
5661
5749
|
// ../daemon/src/problem-mapping-harness.ts
|
|
5662
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
5663
5750
|
function mappingHarnessRunner(options) {
|
|
5664
5751
|
return async (input) => {
|
|
5665
5752
|
const skill = await loadSkill(`problem-${input.role}`);
|
|
@@ -5975,7 +6062,7 @@ var UNANSWERED = /structured[ _]output|returned no result/i;
|
|
|
5975
6062
|
|
|
5976
6063
|
// ../daemon/src/site.ts
|
|
5977
6064
|
import { readFile as readFile4 } from "fs/promises";
|
|
5978
|
-
import { join as
|
|
6065
|
+
import { join as join8 } from "path";
|
|
5979
6066
|
var SITE_PATHS = ["/", "/about", "/pricing"];
|
|
5980
6067
|
var PAGE_CHARS = 6e3;
|
|
5981
6068
|
var SITE_TIMEOUT_MS = 2e4;
|
|
@@ -6098,7 +6185,7 @@ function siteFromSearch(name, hits) {
|
|
|
6098
6185
|
}
|
|
6099
6186
|
async function read(repoRoot, name) {
|
|
6100
6187
|
try {
|
|
6101
|
-
const text2 = await readFile4(
|
|
6188
|
+
const text2 = await readFile4(join8(repoRoot, name), "utf8");
|
|
6102
6189
|
return text2.length > 2e5 ? null : text2;
|
|
6103
6190
|
} catch {
|
|
6104
6191
|
return null;
|
|
@@ -6153,7 +6240,7 @@ async function fromReadme(repoRoot) {
|
|
|
6153
6240
|
|
|
6154
6241
|
// ../daemon/src/repo-docs.ts
|
|
6155
6242
|
import { readFile as readFile5, readdir, stat } from "fs/promises";
|
|
6156
|
-
import { join as
|
|
6243
|
+
import { join as join9, relative } from "path";
|
|
6157
6244
|
var WRITTEN = /(persona|audience|customer|user-research|research\/|icp|segment)/i;
|
|
6158
6245
|
var ABOUT_PEOPLE = /(persona|audience|customer|user-research|research\/|interview|feedback|survey|verbatim|voice|icp|segment)/i;
|
|
6159
6246
|
var DOC = /\.(md|mdx|txt)$/i;
|
|
@@ -6171,7 +6258,7 @@ async function readDocs(cwd, opts = {}) {
|
|
|
6171
6258
|
return;
|
|
6172
6259
|
}
|
|
6173
6260
|
for (const entry of entries) {
|
|
6174
|
-
const full =
|
|
6261
|
+
const full = join9(dir, entry.name);
|
|
6175
6262
|
if (SKIP.test(`/${relative(cwd, full)}`)) continue;
|
|
6176
6263
|
if (entry.isDirectory()) await walk(full, depth + 1);
|
|
6177
6264
|
else if (DOC.test(entry.name) && (!opts.onlyAboutPeople || ABOUT_PEOPLE.test(relative(cwd, full)))) found.push(full);
|
|
@@ -6658,8 +6745,8 @@ function rank(candidates, readings, verdict, faults) {
|
|
|
6658
6745
|
const failed2 = v?.failed ?? [];
|
|
6659
6746
|
const fatal = failed2.filter((t) => MUST_PASS_TESTS.includes(t.test));
|
|
6660
6747
|
const gates = faults.get(candidate.lands) ?? [];
|
|
6661
|
-
const
|
|
6662
|
-
const unanswered = MUST_PASS_TESTS.filter((t) => !
|
|
6748
|
+
const answered2 = /* @__PURE__ */ new Set([...v?.passed ?? [], ...failed2.map((t) => t.test)]);
|
|
6749
|
+
const unanswered = MUST_PASS_TESTS.filter((t) => !answered2.has(t));
|
|
6663
6750
|
const reservations = [
|
|
6664
6751
|
...fatal.map((t) => `${t.test} \u2014 ${t.because}`),
|
|
6665
6752
|
...gates,
|
|
@@ -7599,7 +7686,7 @@ function gateProposal(input) {
|
|
|
7599
7686
|
for (let hops = 0; n && n.status === "replaced" && n.replaced_by && hops < 50; hops += 1) n = byId.get(n.replaced_by);
|
|
7600
7687
|
return n && n.status !== "replaced" ? n : void 0;
|
|
7601
7688
|
};
|
|
7602
|
-
const
|
|
7689
|
+
const answered2 = (r) => r.status === "confirmed" || r.author_kind === "user";
|
|
7603
7690
|
const under = scopeRow ? new Set(descendants(rows2, scopeRow).map((r) => r.id)) : null;
|
|
7604
7691
|
const needOf = (r) => {
|
|
7605
7692
|
let n = r;
|
|
@@ -7718,13 +7805,13 @@ function gateProposal(input) {
|
|
|
7718
7805
|
keptIds.add(target.id);
|
|
7719
7806
|
continue;
|
|
7720
7807
|
}
|
|
7721
|
-
if (input.hook && !
|
|
7808
|
+
if (input.hook && !answered2(target) && target.provenance === "observed" && draft.provenance === "inferred") {
|
|
7722
7809
|
placed.set(node.key, { kind: "existing", row: target, branch: home });
|
|
7723
7810
|
keptIds.add(target.id);
|
|
7724
7811
|
kept.push(`"${target.label}"`);
|
|
7725
7812
|
continue;
|
|
7726
7813
|
}
|
|
7727
|
-
if (
|
|
7814
|
+
if (answered2(target)) {
|
|
7728
7815
|
home.nodes.push(outgoing(node.key, draft, outside, parent, nextRank(parent), { next_of: target.id }));
|
|
7729
7816
|
proposals += 1;
|
|
7730
7817
|
placed.set(node.key, { kind: "existing", row: target, branch: home });
|
|
@@ -7751,7 +7838,7 @@ function gateProposal(input) {
|
|
|
7751
7838
|
if (input.resumeOf && r.job_id === input.resumeOf) return false;
|
|
7752
7839
|
if (within && needOf(r).id !== within) return false;
|
|
7753
7840
|
if (input.hook && r.provenance === "observed") return false;
|
|
7754
|
-
return !descendants(rows2, r).some((d) => d.id !== r.id && d.status !== "dropped" && (
|
|
7841
|
+
return !descendants(rows2, r).some((d) => d.id !== r.id && d.status !== "dropped" && (answered2(d) || keptIds.has(d.id) || input.hook && d.provenance === "observed"));
|
|
7755
7842
|
});
|
|
7756
7843
|
}
|
|
7757
7844
|
return { branches: [...branches.values()].filter((b) => b.nodes.length), cut, kept, proposals, sweepable };
|
|
@@ -7986,7 +8073,7 @@ ${body}
|
|
|
7986
8073
|
// ../daemon/src/crew.ts
|
|
7987
8074
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
7988
8075
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
7989
|
-
import { join as
|
|
8076
|
+
import { join as join10 } from "path";
|
|
7990
8077
|
var READ_ONLY = {
|
|
7991
8078
|
read_repo: /^(Read|Glob|Grep)$/,
|
|
7992
8079
|
read_data: /(^|__)(list_connections|describe_source|query)$/
|
|
@@ -8001,7 +8088,7 @@ async function attempt6({ adapter, spec: spec2, onEvent, skill: skillName, name,
|
|
|
8001
8088
|
if (skill.manifest.capabilities.some((c) => !holds.includes(c))) throw new Error(`The ${name} may not hold tools.`);
|
|
8002
8089
|
const refused = grant.find((c) => !READ_ONLY[c] || !skill.manifest.capabilities.includes(c));
|
|
8003
8090
|
if (refused) throw new Error(`The ${name} may not hold ${refused}.`);
|
|
8004
|
-
const runDir = dir ?
|
|
8091
|
+
const runDir = dir ? join10(spec2.runDir, dir) : spec2.runDir;
|
|
8005
8092
|
if (dir) await mkdir2(runDir, { recursive: true, mode: 448 });
|
|
8006
8093
|
let result;
|
|
8007
8094
|
let usage = null;
|
|
@@ -9281,8 +9368,8 @@ async function flashBranch(ctx, w, n, places) {
|
|
|
9281
9368
|
}
|
|
9282
9369
|
return out;
|
|
9283
9370
|
}
|
|
9284
|
-
function answersFor(
|
|
9285
|
-
return
|
|
9371
|
+
function answersFor(answered2, leaf) {
|
|
9372
|
+
return answered2.find((a) => a.ref.trim().toUpperCase() === leaf.ref.toUpperCase())?.solutions ?? [];
|
|
9286
9373
|
}
|
|
9287
9374
|
async function deepDive(ctx, leaf, place, reads) {
|
|
9288
9375
|
const whole = deepMaterial(ctx.known, leaf);
|
|
@@ -10061,7 +10148,7 @@ import { execFile as execFile6 } from "child_process";
|
|
|
10061
10148
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
10062
10149
|
import { existsSync as existsSync3 } from "fs";
|
|
10063
10150
|
import { mkdir as mkdir3, readFile as readFile6 } from "fs/promises";
|
|
10064
|
-
import { join as
|
|
10151
|
+
import { join as join11 } from "path";
|
|
10065
10152
|
import { promisify as promisify6 } from "util";
|
|
10066
10153
|
var execFileAsync6 = promisify6(execFile6);
|
|
10067
10154
|
var DEFINITIONS_TIMEOUT_MS = 9e4;
|
|
@@ -10097,7 +10184,7 @@ function pickLines(raw) {
|
|
|
10097
10184
|
}
|
|
10098
10185
|
async function killRecorded(runDir) {
|
|
10099
10186
|
try {
|
|
10100
|
-
const pid = Number((await readFile6(
|
|
10187
|
+
const pid = Number((await readFile6(join11(runDir, "harness.pid"), "utf8")).trim());
|
|
10101
10188
|
if (pid > 1) process.kill(pid, "SIGKILL");
|
|
10102
10189
|
} catch {
|
|
10103
10190
|
}
|
|
@@ -10117,7 +10204,7 @@ async function readDefinitions({ adapter, spec: spec2, publish: publish5, journa
|
|
|
10117
10204
|
if (!hasRepo) return give({}, "No repository to read: the card offers the common definitions.", "ok");
|
|
10118
10205
|
try {
|
|
10119
10206
|
const skill = await loadSkill("define-objective");
|
|
10120
|
-
const runDir =
|
|
10207
|
+
const runDir = join11(spec2.runDir, "definitions");
|
|
10121
10208
|
await mkdir3(runDir, { recursive: true, mode: 448 });
|
|
10122
10209
|
const schema = definitionsOutputSchema();
|
|
10123
10210
|
const [lines, listed] = await Promise.all([countingLines(spec2.cwd), listTrackedFiles(spec2.cwd)]);
|
|
@@ -10179,7 +10266,7 @@ ${renderTrackedFiles(listed.files, void 0, listed.wiki)}
|
|
|
10179
10266
|
if (!result) throw new Error("no answer");
|
|
10180
10267
|
const parsed = DefinitionsAnswer.safeParse(JSON.parse(result));
|
|
10181
10268
|
if (!parsed.success) throw new Error("an answer in the wrong shape");
|
|
10182
|
-
const checked = checkProposals(parsed.data, (file) => existsSync3(
|
|
10269
|
+
const checked = checkProposals(parsed.data, (file) => existsSync3(join11(spec2.cwd, file)));
|
|
10183
10270
|
const definitions2 = lever ? checked[lever] ? { [lever]: checked[lever] } : {} : checked;
|
|
10184
10271
|
const kept = Object.values(definitions2).reduce((n, list) => n + (list?.length ?? 0), 0);
|
|
10185
10272
|
const read6 = Object.values(definitions2).flat().filter((d) => d?.found).length;
|
|
@@ -10453,8 +10540,8 @@ function localDate(timezone, at) {
|
|
|
10453
10540
|
// ../daemon/src/run-job.ts
|
|
10454
10541
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
10455
10542
|
import { existsSync as existsSync4 } from "fs";
|
|
10456
|
-
import { mkdir as
|
|
10457
|
-
import { homedir as
|
|
10543
|
+
import { mkdir as mkdir4, readFile as readFile7, rm as rm4 } from "fs/promises";
|
|
10544
|
+
import { homedir as homedir3, tmpdir } from "os";
|
|
10458
10545
|
import { join as join12, resolve as resolve4 } from "path";
|
|
10459
10546
|
|
|
10460
10547
|
// ../daemon/src/events.ts
|
|
@@ -10535,168 +10622,6 @@ function lighten(value) {
|
|
|
10535
10622
|
return value ?? null;
|
|
10536
10623
|
}
|
|
10537
10624
|
|
|
10538
|
-
// ../daemon/src/build-job.ts
|
|
10539
|
-
import { mkdir as mkdir4, readdir as readdir2, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
|
|
10540
|
-
import { homedir as homedir3 } from "os";
|
|
10541
|
-
import { join as join11 } from "path";
|
|
10542
|
-
var PLAN_TOOL = "mcp__nanopm__build_plan";
|
|
10543
|
-
var STEP_TOOL = "mcp__nanopm__build_step";
|
|
10544
|
-
var CLAUDE_CODE_MIN = "2.1.283";
|
|
10545
|
-
async function prepareBuild(client, job, clone, adapter) {
|
|
10546
|
-
const params = job.params ?? {};
|
|
10547
|
-
const dir = buildDir(homedir3(), job.id);
|
|
10548
|
-
let made = false;
|
|
10549
|
-
try {
|
|
10550
|
-
const [solution, machine] = await Promise.all([loadSolution(client, job.project_id, String(params.solution_id ?? "")), machineRefusal(adapter)]);
|
|
10551
|
-
const refused = buildRefusal(solution, { anyway: params.anyway === true }) ?? machine;
|
|
10552
|
-
if (refused) return { ok: false, outcome: failed(refused) };
|
|
10553
|
-
const branch = buildBranch(solution.ref, solution.title);
|
|
10554
|
-
await mkdir4(join11(dir, ".."), { recursive: true, mode: 448 });
|
|
10555
|
-
const { base, fork, worktree } = await addWorktree(clone, dir, branch);
|
|
10556
|
-
made = true;
|
|
10557
|
-
await handOverTheSpec(worktree, solution.agent_spec);
|
|
10558
|
-
await startSolution(client, job.project_id, solution.ref, job.id);
|
|
10559
|
-
return { ok: true, build: { solution, clone, dir, worktree, branch, base, fork, network: buildNetwork(await readdir2(dir)) } };
|
|
10560
|
-
} catch (err) {
|
|
10561
|
-
if (made) await removeWorktree(clone, dir);
|
|
10562
|
-
return { ok: false, outcome: failed(errorMessage(err)) };
|
|
10563
|
-
}
|
|
10564
|
-
}
|
|
10565
|
-
async function runBuildJob(input) {
|
|
10566
|
-
const { client, job, adapter, spec: spec2, events, log, verifyCommand: verifyCommand2 } = input;
|
|
10567
|
-
const { solution, clone, dir, worktree, branch, base, fork } = input.build;
|
|
10568
|
-
await log("build.start", `building ${solution.ref} on ${branch}, in a copy of your code`);
|
|
10569
|
-
await events.write("text", { text: `Working on ${branch}, in a copy of your code. Your own checkout is not touched.` });
|
|
10570
|
-
let plan = null;
|
|
10571
|
-
const done = /* @__PURE__ */ new Set();
|
|
10572
|
-
let usage;
|
|
10573
|
-
let said = "";
|
|
10574
|
-
const stop2 = async (message, opts = { keep: true }) => {
|
|
10575
|
-
await unstartSolution(client, job.project_id, solution.ref, solution.status).catch(() => void 0);
|
|
10576
|
-
if (opts.keep) await commitAll(worktree, `${solution.ref}: unfinished \u2014 ${solution.title}`).catch(() => null);
|
|
10577
|
-
else await removeWorktree(clone, dir);
|
|
10578
|
-
return { ...failed(opts.keep ? `${message}. What was done is on ${branch}, in ${dir}.` : message), usage };
|
|
10579
|
-
};
|
|
10580
|
-
const session2 = async (prompt, resume2) => {
|
|
10581
|
-
let end;
|
|
10582
|
-
for await (const event of adapter.run({ ...spec2, prompt, resumeSessionId: resume2 ? spec2.sessionId : void 0 })) {
|
|
10583
|
-
await input.onEvent(event);
|
|
10584
|
-
if (event.type === "tool_use" && event.tool === PLAN_TOOL && !plan) {
|
|
10585
|
-
plan = stepsOf(event.input);
|
|
10586
|
-
if (plan) await events.write("plan", { steps: plan });
|
|
10587
|
-
} else if (event.type === "tool_result" && event.tool === STEP_TOOL && !event.isError) {
|
|
10588
|
-
const { step, summary } = event.input ?? {};
|
|
10589
|
-
const index = Number(step) - 1;
|
|
10590
|
-
if (Number.isInteger(index) && index >= 0 && !done.has(index)) {
|
|
10591
|
-
const says = plan?.[index] ?? `step ${index + 1}`;
|
|
10592
|
-
const commit = await commitAll(worktree, `${solution.ref}: ${says}`, summary);
|
|
10593
|
-
done.add(index);
|
|
10594
|
-
await events.write("step", { step: index + 1, says, commit: commit?.sha ?? null, files: commit?.files ?? 0 });
|
|
10595
|
-
}
|
|
10596
|
-
} else if (event.type === "finished") {
|
|
10597
|
-
usage = sumUsage(usage, event.usage);
|
|
10598
|
-
said = event.result;
|
|
10599
|
-
end = { status: "succeeded" };
|
|
10600
|
-
} else if (event.type === "failed") {
|
|
10601
|
-
end = { status: "failed", error: { code: event.code, message: event.message } };
|
|
10602
|
-
}
|
|
10603
|
-
}
|
|
10604
|
-
return end?.status === "succeeded" ? null : end ?? failed("the agent produced no result");
|
|
10605
|
-
};
|
|
10606
|
-
const check = verifyCommand2 ? `
|
|
10607
|
-
|
|
10608
|
-
The project's own check is \`${verifyCommand2}\`. Run it before your last build_step, fix what it reports, and give its result under *How I checked it*.` : "";
|
|
10609
|
-
let ended = await session2(spec2.prompt + check, false);
|
|
10610
|
-
for (let resumes = 0; !ended && !(plan && !stepsLeft(plan, done).length); resumes += 1) {
|
|
10611
|
-
if (resumes >= BUILD_RESUMES_MAX) {
|
|
10612
|
-
const left = stepsLeft(plan, done).length;
|
|
10613
|
-
return stop2(plan ? `Nano stopped with ${left} step${left === 1 ? "" : "s"} of its plan left` : "Nano stopped without saying its plan");
|
|
10614
|
-
}
|
|
10615
|
-
ended = await session2(carryOn(plan, done), true);
|
|
10616
|
-
}
|
|
10617
|
-
if (ended) return stop2(ended.error?.message ?? "the build stopped");
|
|
10618
|
-
await commitAll(worktree, `${solution.ref}: ${solution.title}`);
|
|
10619
|
-
if (!await commitsSinceFork(worktree, fork)) return stop2("the build finished without changing anything", { keep: false });
|
|
10620
|
-
const rebased = await rebaseOnto(worktree, base);
|
|
10621
|
-
if (!rebased.ok) return stop2(`${base} moved while Nano was building, and the same files changed on both sides: ${rebased.files.join(", ") || "see git status"}`);
|
|
10622
|
-
const [pushed, noPr, page] = await Promise.all([pushBranch(worktree, branch), ghReady(worktree), solutionPage(client, job.project_id, solution.ref)]);
|
|
10623
|
-
if (!pushed.pushed) return stop2(`the push failed: ${pushed.reason}`);
|
|
10624
|
-
const pr = noPr ? null : await openPullRequest(worktree, { branch, base, title: `${solution.ref}: ${solution.title}`, body: prBody(said, page) });
|
|
10625
|
-
if (pr && !pr.ok) return { ...failed(`the branch ${branch} is pushed, but GitHub refused the pull request: ${pr.reason}${pushed.reviewUrl ? `. Open it yourself: ${pushed.reviewUrl}` : ""}`), usage };
|
|
10626
|
-
if (pr) {
|
|
10627
|
-
await events.write("pr", { number: pr.number, url: pr.url });
|
|
10628
|
-
await log("build.pr", `draft pull request #${pr.number} for ${solution.ref}`);
|
|
10629
|
-
}
|
|
10630
|
-
await removeWorktree(clone, dir);
|
|
10631
|
-
return {
|
|
10632
|
-
status: "succeeded",
|
|
10633
|
-
result: [
|
|
10634
|
-
said.trim(),
|
|
10635
|
-
"",
|
|
10636
|
-
pr ? `Draft pull request #${pr.number} \xB7 ${pr.url}` : `Pushed ${branch}. No pull request: ${noPr}.${pushed.reviewUrl ? ` ${pushed.reviewUrl}` : ""}`
|
|
10637
|
-
].filter(Boolean).join("\n"),
|
|
10638
|
-
data: { branch, session: spec2.sessionId, ...pr ? { pr: { number: pr.number, url: pr.url } } : { review_url: pushed.reviewUrl ?? null } },
|
|
10639
|
-
usage
|
|
10640
|
-
};
|
|
10641
|
-
}
|
|
10642
|
-
var failed = (message) => ({ status: "failed", error: { code: "harness_error", message } });
|
|
10643
|
-
async function loadSolution(client, projectId, id) {
|
|
10644
|
-
const { data, error } = await client.from("solutions").select(BUILD_COLUMNS).eq("id", id).eq("project_id", projectId).is("superseded_by", null).maybeSingle();
|
|
10645
|
-
if (error) throw new Error(`could not read the solution: ${error.message}`);
|
|
10646
|
-
if (!data) throw new Error("that solution is not on this project any more");
|
|
10647
|
-
return data;
|
|
10648
|
-
}
|
|
10649
|
-
async function machineRefusal(adapter) {
|
|
10650
|
-
const found = await adapter.detect();
|
|
10651
|
-
const version = /\d+\.\d+\.\d+/.exec(found.version ?? "")?.[0];
|
|
10652
|
-
if (version && !newer(CLAUDE_CODE_MIN, version)) return null;
|
|
10653
|
-
return `a build needs Claude Code ${CLAUDE_CODE_MIN} or later on this machine${found.version ? ` (it has ${found.version})` : ""}: run claude update`;
|
|
10654
|
-
}
|
|
10655
|
-
async function handOverTheSpec(worktree, agentSpec) {
|
|
10656
|
-
await mkdir4(join11(worktree.dir, ".nanopm"), { recursive: true });
|
|
10657
|
-
await writeFile5(join11(worktree.dir, ".nanopm", "SPEC.md"), agentSpec);
|
|
10658
|
-
const exclude = join11(await commonGitDir(worktree), "info", "exclude");
|
|
10659
|
-
const current = await readFile7(exclude, "utf8").catch(() => "");
|
|
10660
|
-
if (!current.split("\n").includes(".nanopm/")) {
|
|
10661
|
-
await mkdir4(join11(exclude, ".."), { recursive: true });
|
|
10662
|
-
await writeFile5(exclude, `${current}${current && !current.endsWith("\n") ? "\n" : ""}.nanopm/
|
|
10663
|
-
`);
|
|
10664
|
-
}
|
|
10665
|
-
}
|
|
10666
|
-
function stepsOf(input) {
|
|
10667
|
-
const steps = input?.steps;
|
|
10668
|
-
return Array.isArray(steps) && steps.length && steps.every((s) => typeof s === "string") ? steps : null;
|
|
10669
|
-
}
|
|
10670
|
-
function carryOn(plan, done) {
|
|
10671
|
-
if (!plan) return "You stopped before saying your plan. Call build_plan with it now, then build it step by step, calling build_step after each.";
|
|
10672
|
-
const left = stepsLeft(plan, done);
|
|
10673
|
-
return [
|
|
10674
|
-
`Steps ${left.map((i) => i + 1).join(", ")} of your plan are not done yet:`,
|
|
10675
|
-
...left.map((i) => `${i + 1}. ${plan[i]}`),
|
|
10676
|
-
"",
|
|
10677
|
-
"Check git log and git status to see where you are, then carry on: finish each step, run its checks, and call build_step after it. Do not run anything in the background."
|
|
10678
|
-
].join("\n");
|
|
10679
|
-
}
|
|
10680
|
-
async function solutionPage(client, projectId, ref) {
|
|
10681
|
-
const { data } = await client.from("projects").select("slug").eq("id", projectId).maybeSingle();
|
|
10682
|
-
return data?.slug ? `${webUrl()}/p/${data.slug}/roadmap/${ref}` : null;
|
|
10683
|
-
}
|
|
10684
|
-
function prBody(said, page) {
|
|
10685
|
-
return [
|
|
10686
|
-
said.trim(),
|
|
10687
|
-
page ? `
|
|
10688
|
-
The solution and its spec: ${page}` : "",
|
|
10689
|
-
"\n\u2014 Built by NanoPM with Claude Code. A draft: review before merging."
|
|
10690
|
-
].filter(Boolean).join("\n");
|
|
10691
|
-
}
|
|
10692
|
-
async function startSolution(client, projectId, ref, jobId) {
|
|
10693
|
-
const { error } = await client.rpc("start_solution", { p_project_id: projectId, p_ref: ref, p_job_id: jobId });
|
|
10694
|
-
if (error) throw new Error(pgMessage(error));
|
|
10695
|
-
}
|
|
10696
|
-
async function unstartSolution(client, projectId, ref, was) {
|
|
10697
|
-
await client.rpc("unstart_solution", { p_project_id: projectId, p_ref: ref, p_to: was === "proposed" ? "proposed" : "accepted" });
|
|
10698
|
-
}
|
|
10699
|
-
|
|
10700
10625
|
// ../daemon/src/run-job.ts
|
|
10701
10626
|
import { hostname } from "os";
|
|
10702
10627
|
|
|
@@ -10731,11 +10656,13 @@ async function runJob(job, ctx) {
|
|
|
10731
10656
|
};
|
|
10732
10657
|
}
|
|
10733
10658
|
const durationMs = Date.now() - startedAt;
|
|
10659
|
+
const over = outcome.status !== "waiting";
|
|
10734
10660
|
await events.status(outcome.status, outcome.error ? { error: outcome.error } : crewRun(job) && outcome.result ? { text: outcome.result } : {});
|
|
10735
10661
|
await withRetry(
|
|
10736
10662
|
() => ctx.client.from("jobs").update({
|
|
10737
10663
|
status: outcome.status,
|
|
10738
|
-
|
|
10664
|
+
// A parked job is not over: it waits, with no heartbeat, for the founder or the limit.
|
|
10665
|
+
finished_at: over ? (/* @__PURE__ */ new Date()).toISOString() : null,
|
|
10739
10666
|
// A mapping run's root row belongs to SQL: checkpoints, receipt and text land there
|
|
10740
10667
|
// with the publication. A resumed job's own row is free to carry the text.
|
|
10741
10668
|
...job.skill === "problems" && !job.resume_of ? {} : { result: outcome.result !== void 0 ? { text: outcome.result, ...outcome.data } : null },
|
|
@@ -10748,7 +10675,7 @@ async function runJob(job, ctx) {
|
|
|
10748
10675
|
ctx,
|
|
10749
10676
|
job,
|
|
10750
10677
|
"job.end",
|
|
10751
|
-
outcome.status === "succeeded" ? `finished ${job.skill}` : outcome.status === "cancelled" ? `${job.skill} cancelled by the user` : `${job.skill} failed: ${outcome.error?.message}`,
|
|
10678
|
+
outcome.status === "succeeded" ? `finished ${job.skill}` : outcome.status === "waiting" ? `${job.skill} waits, with nothing running` : outcome.status === "cancelled" ? `${job.skill} cancelled by the user` : `${job.skill} failed: ${outcome.error?.message}`,
|
|
10752
10679
|
{
|
|
10753
10680
|
usage: {
|
|
10754
10681
|
tokens: usageTokens(outcome.usage),
|
|
@@ -10757,15 +10684,15 @@ async function runJob(job, ctx) {
|
|
|
10757
10684
|
...outcome.usage?.contextPeakTokens ? { context_peak: outcome.usage.contextPeakTokens } : {},
|
|
10758
10685
|
...outcome.usage?.reads ? { files_read: outcome.usage.reads.files, calls: outcome.usage.reads.calls } : {}
|
|
10759
10686
|
},
|
|
10760
|
-
outcome: outcome.status === "succeeded" ? "ok" : "error"
|
|
10687
|
+
outcome: outcome.status === "succeeded" || !over ? "ok" : "error"
|
|
10761
10688
|
}
|
|
10762
10689
|
);
|
|
10763
10690
|
return outcome;
|
|
10764
10691
|
}
|
|
10765
10692
|
async function execute(job, skill, ctx, events) {
|
|
10766
10693
|
const runDir = join12(nanopmHome(), "run", job.id);
|
|
10767
|
-
await
|
|
10768
|
-
const resumeSessionId = job.resume_of ? await sessionOf(ctx.client, job.resume_of) : void 0;
|
|
10694
|
+
await mkdir4(runDir, { recursive: true, mode: 448 });
|
|
10695
|
+
const resumeSessionId = job.resume_of ? await sessionOf(ctx.client, job.resume_of) : job.harness_session_id ?? void 0;
|
|
10769
10696
|
const sessionId = resumeSessionId ?? randomUUID12();
|
|
10770
10697
|
await ctx.client.from("jobs").update({ harness_session_id: sessionId }).eq("id", job.id);
|
|
10771
10698
|
const adapter = ctx.adapter ?? new ClaudeCodeAdapter();
|
|
@@ -10815,7 +10742,7 @@ async function execute(job, skill, ctx, events) {
|
|
|
10815
10742
|
if (quickRun2) {
|
|
10816
10743
|
const cwd2 = scratchDir(job.id);
|
|
10817
10744
|
try {
|
|
10818
|
-
await
|
|
10745
|
+
await mkdir4(cwd2, { recursive: true, mode: 448 });
|
|
10819
10746
|
const caps = skill.manifest.capabilities;
|
|
10820
10747
|
return await quickRun2({
|
|
10821
10748
|
client: ctx.client,
|
|
@@ -10894,6 +10821,7 @@ async function execute(job, skill, ctx, events) {
|
|
|
10894
10821
|
capabilities,
|
|
10895
10822
|
harnessWeb,
|
|
10896
10823
|
network: build2?.network,
|
|
10824
|
+
answerFile: build2 ? buildAnswerFile(job.id) : void 0,
|
|
10897
10825
|
withoutTools: skill.manifest.withoutTools,
|
|
10898
10826
|
// The workspace reaches the memory server only for a job that changes files; that is
|
|
10899
10827
|
// what lets the build's tools refuse everywhere else.
|
|
@@ -11075,7 +11003,7 @@ function harnessWebFor(searchProvider, machineCanSearch) {
|
|
|
11075
11003
|
}
|
|
11076
11004
|
async function machineWebSearch() {
|
|
11077
11005
|
try {
|
|
11078
|
-
const parsed = JSON.parse(await
|
|
11006
|
+
const parsed = JSON.parse(await readFile7(join12(nanopmHome(), "daemon.json"), "utf8"));
|
|
11079
11007
|
return parsed.webSearch === true;
|
|
11080
11008
|
} catch {
|
|
11081
11009
|
return false;
|
|
@@ -11126,12 +11054,12 @@ async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
|
|
|
11126
11054
|
const requested = params.repo_path;
|
|
11127
11055
|
if (typeof requested !== "string" || !requested) {
|
|
11128
11056
|
if (mustBeTheClone) refuse2("the job named no repository");
|
|
11129
|
-
await
|
|
11057
|
+
await mkdir4(fallback, { recursive: true, mode: 448 });
|
|
11130
11058
|
return fallback;
|
|
11131
11059
|
}
|
|
11132
11060
|
if (!job.project_id) {
|
|
11133
11061
|
if (mustBeTheClone) refuse2("the job belongs to no project");
|
|
11134
|
-
await
|
|
11062
|
+
await mkdir4(fallback, { recursive: true, mode: 448 });
|
|
11135
11063
|
return fallback;
|
|
11136
11064
|
}
|
|
11137
11065
|
const { data: clone } = await ctx.client.from("clones").select("repo_path").eq("project_id", job.project_id).eq("daemon_id", ctx.daemonId).maybeSingle();
|
|
@@ -11139,12 +11067,12 @@ async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
|
|
|
11139
11067
|
if (!allowed || resolve4(allowed) !== resolve4(requested)) {
|
|
11140
11068
|
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`);
|
|
11141
11069
|
console.error(`job ${job.id}: repo_path ${requested} is not this machine's clone; running in a scratch directory instead`);
|
|
11142
|
-
await
|
|
11070
|
+
await mkdir4(fallback, { recursive: true, mode: 448 });
|
|
11143
11071
|
return fallback;
|
|
11144
11072
|
}
|
|
11145
11073
|
if (existsSync4(allowed)) return resolve4(allowed);
|
|
11146
11074
|
if (mustBeTheClone) refuse2(`${allowed} is not on this machine any more`);
|
|
11147
|
-
await
|
|
11075
|
+
await mkdir4(fallback, { recursive: true, mode: 448 });
|
|
11148
11076
|
return fallback;
|
|
11149
11077
|
}
|
|
11150
11078
|
function asData8(label, body) {
|
|
@@ -11153,7 +11081,7 @@ ${body}
|
|
|
11153
11081
|
</${label}>`;
|
|
11154
11082
|
}
|
|
11155
11083
|
function harnessEnv(timeoutMs, source = process.env) {
|
|
11156
|
-
const home = source.HOME ??
|
|
11084
|
+
const home = source.HOME ?? homedir3();
|
|
11157
11085
|
const env = {
|
|
11158
11086
|
HOME: home,
|
|
11159
11087
|
PATH: [source.PATH ?? "/usr/local/bin:/usr/bin:/bin", join12(home, ".local", "bin")].join(":"),
|
|
@@ -11187,6 +11115,309 @@ function usageTokens(usage) {
|
|
|
11187
11115
|
return usage.inputTokens + (usage.cacheWriteTokens ?? 0) + usage.outputTokens;
|
|
11188
11116
|
}
|
|
11189
11117
|
|
|
11118
|
+
// ../daemon/src/build-job.ts
|
|
11119
|
+
var PLAN_TOOL = "mcp__nanopm__build_plan";
|
|
11120
|
+
var ASK_TOOL = "mcp__nanopm__ask_user";
|
|
11121
|
+
var STEP_TOOL = "mcp__nanopm__build_step";
|
|
11122
|
+
var CLAUDE_CODE_MIN = "2.1.283";
|
|
11123
|
+
async function prepareBuild(client, job, clone, adapter) {
|
|
11124
|
+
const params = job.params ?? {};
|
|
11125
|
+
const dir = buildDir(homedir4(), job.id);
|
|
11126
|
+
let made = false;
|
|
11127
|
+
try {
|
|
11128
|
+
const [solution, machine, history] = await Promise.all([
|
|
11129
|
+
loadSolution(client, job.project_id, String(params.solution_id ?? "")),
|
|
11130
|
+
machineRefusal(adapter),
|
|
11131
|
+
loadHistory(client, job)
|
|
11132
|
+
]);
|
|
11133
|
+
const refused = buildRefusal(solution, { anyway: params.anyway === true || Boolean(history.place) }) ?? machine;
|
|
11134
|
+
if (refused) return { ok: false, outcome: failed(refused) };
|
|
11135
|
+
const network = async (at) => buildNetwork(await readdir2(at));
|
|
11136
|
+
if (history.place) {
|
|
11137
|
+
const { place } = history;
|
|
11138
|
+
if (!existsSync5(place.dir)) return { ok: false, outcome: failed(`this build's copy of the code is gone (${place.dir}): build it again`) };
|
|
11139
|
+
const worktree2 = { dir: place.dir, admin: join13(await commonGitDir(clone), "worktrees", basename(place.dir)), commondir: "../..\n" };
|
|
11140
|
+
await startSolution(client, job.project_id, solution.ref, job.id);
|
|
11141
|
+
return { ok: true, build: { solution, clone, ...place, worktree: worktree2, network: await network(place.dir), history } };
|
|
11142
|
+
}
|
|
11143
|
+
const branch = buildBranch(solution.ref, solution.title);
|
|
11144
|
+
await mkdir5(join13(dir, ".."), { recursive: true, mode: 448 });
|
|
11145
|
+
const { base, fork, worktree } = await addWorktree(clone, dir, branch);
|
|
11146
|
+
made = true;
|
|
11147
|
+
await appendJobEvent(client, job.id, "worktree", { dir, branch, base, fork }, "nanopm");
|
|
11148
|
+
await handOverTheSpec(worktree, specForAgent(solution.agent_spec, solution.spec));
|
|
11149
|
+
await startSolution(client, job.project_id, solution.ref, job.id);
|
|
11150
|
+
return { ok: true, build: { solution, clone, dir, worktree, branch, base, fork, network: await network(dir), history } };
|
|
11151
|
+
} catch (err) {
|
|
11152
|
+
if (made) await removeWorktree(clone, dir);
|
|
11153
|
+
return { ok: false, outcome: failed(errorMessage(err)) };
|
|
11154
|
+
}
|
|
11155
|
+
}
|
|
11156
|
+
async function runBuildJob(input) {
|
|
11157
|
+
const { client, job, adapter, spec: spec2, events, log, verifyCommand: verifyCommand2 } = input;
|
|
11158
|
+
const { solution, clone, dir, worktree, branch, base, fork, history } = input.build;
|
|
11159
|
+
const was = wasOf(job.params);
|
|
11160
|
+
let plan = history.plan;
|
|
11161
|
+
const done = new Set(history.done);
|
|
11162
|
+
let usage;
|
|
11163
|
+
let said = "";
|
|
11164
|
+
const stop2 = async (message, opts = { keep: true }) => {
|
|
11165
|
+
await unstartSolution(client, job.project_id, solution.ref, was).catch(() => void 0);
|
|
11166
|
+
if (opts.keep) await commitAll(worktree, `${solution.ref}: unfinished \u2014 ${solution.title}`).catch(() => null);
|
|
11167
|
+
else await removeWorktree(clone, dir);
|
|
11168
|
+
return { ...failed(opts.keep ? `${message}. What was done is on ${branch}, in ${dir}.` : message), usage };
|
|
11169
|
+
};
|
|
11170
|
+
const park = async (why) => {
|
|
11171
|
+
if ("ask" in why) {
|
|
11172
|
+
await events.write("question", { ...why.ask, ...plan ? {} : { about: "plan" } });
|
|
11173
|
+
await log("build.ask", `${solution.ref} waits on the founder`);
|
|
11174
|
+
} else if (why.resetsAt) {
|
|
11175
|
+
await events.write("paused", { why: "limit", resets_at: new Date(why.resetsAt * 1e3).toISOString() });
|
|
11176
|
+
await log("build.pause", `${solution.ref} paused until the Claude limit resets`);
|
|
11177
|
+
} else {
|
|
11178
|
+
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" });
|
|
11179
|
+
}
|
|
11180
|
+
return { status: "waiting", usage };
|
|
11181
|
+
};
|
|
11182
|
+
const session2 = async (prompt, resume3) => {
|
|
11183
|
+
let end = { stop: failed("the agent produced no result") };
|
|
11184
|
+
for await (const event of adapter.run({ ...spec2, prompt, resumeSessionId: resume3 ? spec2.sessionId : void 0 })) {
|
|
11185
|
+
await input.onEvent(event);
|
|
11186
|
+
if (event.type === "tool_use" && event.tool === PLAN_TOOL && !plan) {
|
|
11187
|
+
plan = stepsOf(event.input);
|
|
11188
|
+
if (plan) await events.write("plan", { steps: plan });
|
|
11189
|
+
} else if (event.type === "tool_result" && event.tool === STEP_TOOL && !event.isError) {
|
|
11190
|
+
const { step, summary } = event.input ?? {};
|
|
11191
|
+
const index = Number(step) - 1;
|
|
11192
|
+
if (Number.isInteger(index) && index >= 0 && !done.has(index)) {
|
|
11193
|
+
const says = plan?.[index] ?? `step ${index + 1}`;
|
|
11194
|
+
const commit = await commitAll(worktree, `${solution.ref}: ${says}`, summary);
|
|
11195
|
+
done.add(index);
|
|
11196
|
+
await events.write("step", { step: index + 1, says, commit: commit?.sha ?? null, files: commit?.files ?? 0 });
|
|
11197
|
+
}
|
|
11198
|
+
} else if (event.type === "finished") {
|
|
11199
|
+
usage = sumUsage(usage, event.usage);
|
|
11200
|
+
said = event.result;
|
|
11201
|
+
end = null;
|
|
11202
|
+
} else if (event.type === "deferred" && event.tool === ASK_TOOL) {
|
|
11203
|
+
usage = sumUsage(usage, event.usage);
|
|
11204
|
+
end = { park: { ask: askOf(event.input) } };
|
|
11205
|
+
} else if (event.type === "failed") {
|
|
11206
|
+
end = event.code === "rate_limit" ? { park: { resetsAt: event.resetsAt } } : { stop: { status: "failed", error: { code: event.code, message: event.message } } };
|
|
11207
|
+
}
|
|
11208
|
+
}
|
|
11209
|
+
return end;
|
|
11210
|
+
};
|
|
11211
|
+
const resume2 = history.resume;
|
|
11212
|
+
const check = verifyCommand2 ? `
|
|
11213
|
+
|
|
11214
|
+
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*.` : "";
|
|
11215
|
+
const finished = () => Boolean(plan && !stepsLeft(plan, done).length);
|
|
11216
|
+
if (!resume2) {
|
|
11217
|
+
await log("build.start", `building ${solution.ref} on ${branch}, in a copy of your code`);
|
|
11218
|
+
await events.write("text", { text: `Working on ${branch}, in a copy of your code. Your own checkout is not touched.` });
|
|
11219
|
+
} else {
|
|
11220
|
+
await events.write("text", { text: AGAIN[resume2.why].said });
|
|
11221
|
+
if (resume2.why === "answer" && spec2.answerFile) await writeFile5(spec2.answerFile, answerForAgent(resume2.answer), { mode: 384 });
|
|
11222
|
+
}
|
|
11223
|
+
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);
|
|
11224
|
+
for (let resumes = 0; !turn && !finished(); resumes += 1) {
|
|
11225
|
+
if (resumes >= BUILD_RESUMES_MAX) {
|
|
11226
|
+
const left = stepsLeft(plan, done).length;
|
|
11227
|
+
return stop2(plan ? `Nano stopped with ${left} step${left === 1 ? "" : "s"} of its plan left` : "Nano stopped without saying its plan");
|
|
11228
|
+
}
|
|
11229
|
+
turn = await session2(carryOn(plan, done), true);
|
|
11230
|
+
}
|
|
11231
|
+
if (turn && "park" in turn) return park(turn.park);
|
|
11232
|
+
if (turn) return stop2(turn.stop.error?.message ?? "the build stopped");
|
|
11233
|
+
await commitAll(worktree, `${solution.ref}: ${solution.title}`);
|
|
11234
|
+
if (!await commitsSinceFork(worktree, fork)) return stop2("the build finished without changing anything", { keep: false });
|
|
11235
|
+
const rebased = await rebaseOnto(worktree, base);
|
|
11236
|
+
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"}`);
|
|
11237
|
+
const [pushed, noPr, page] = await Promise.all([pushBranch(worktree, branch), ghReady(worktree), solutionPage(client, job.project_id, solution.ref)]);
|
|
11238
|
+
if (!pushed.pushed) return stop2(`the push failed: ${pushed.reason}`);
|
|
11239
|
+
const pr = noPr ? null : await openPullRequest(worktree, { branch, base, title: `${solution.ref}: ${solution.title}`, body: prBody(said, page) });
|
|
11240
|
+
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 };
|
|
11241
|
+
if (pr) {
|
|
11242
|
+
await events.write("pr", { number: pr.number, url: pr.url });
|
|
11243
|
+
await log("build.pr", `draft pull request #${pr.number} for ${solution.ref}`);
|
|
11244
|
+
}
|
|
11245
|
+
await removeWorktree(clone, dir);
|
|
11246
|
+
return {
|
|
11247
|
+
status: "succeeded",
|
|
11248
|
+
result: [
|
|
11249
|
+
said.trim(),
|
|
11250
|
+
"",
|
|
11251
|
+
pr ? `Draft pull request #${pr.number} \xB7 ${pr.url}` : `Pushed ${branch}. No pull request: ${noPr}.${pushed.reviewUrl ? ` ${pushed.reviewUrl}` : ""}`
|
|
11252
|
+
].filter(Boolean).join("\n"),
|
|
11253
|
+
data: { branch, session: spec2.sessionId, ...pr ? { pr: { number: pr.number, url: pr.url } } : { review_url: pushed.reviewUrl ?? null } },
|
|
11254
|
+
usage
|
|
11255
|
+
};
|
|
11256
|
+
}
|
|
11257
|
+
var failed = (message) => ({ status: "failed", error: { code: "harness_error", message } });
|
|
11258
|
+
var AGAIN = {
|
|
11259
|
+
answer: { said: "Your answer is in: carrying on.", lead: "" },
|
|
11260
|
+
limit: { said: "Your Claude limit has reset: carrying on.", lead: "Your Claude limit has reset." },
|
|
11261
|
+
lost: { said: "Picked up again where it stopped.", lead: "You were interrupted: the machine stopped." }
|
|
11262
|
+
};
|
|
11263
|
+
function askOf(input) {
|
|
11264
|
+
const { question, options, why } = input ?? {};
|
|
11265
|
+
return {
|
|
11266
|
+
question: String(question ?? ""),
|
|
11267
|
+
...Array.isArray(options) ? { options: options.map(String) } : {},
|
|
11268
|
+
...typeof why === "string" ? { why } : {}
|
|
11269
|
+
};
|
|
11270
|
+
}
|
|
11271
|
+
async function loadHistory(client, job) {
|
|
11272
|
+
const named = (job.params ?? {}).origin;
|
|
11273
|
+
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;
|
|
11274
|
+
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}`);
|
|
11275
|
+
return loadBuildHistory(client, [.../* @__PURE__ */ new Set([origin, job.id, ...(runs ?? []).map((r) => r.id)])], { home: homedir4(), jobId: origin });
|
|
11276
|
+
}
|
|
11277
|
+
async function loadSolution(client, projectId, id) {
|
|
11278
|
+
const { data, error } = await client.from("solutions").select(BUILD_COLUMNS).eq("id", id).eq("project_id", projectId).is("superseded_by", null).maybeSingle();
|
|
11279
|
+
if (error) throw new Error(`could not read the solution: ${error.message}`);
|
|
11280
|
+
if (!data) throw new Error("that solution is not on this project any more");
|
|
11281
|
+
return data;
|
|
11282
|
+
}
|
|
11283
|
+
async function machineRefusal(adapter) {
|
|
11284
|
+
const found = await adapter.detect();
|
|
11285
|
+
const version = /\d+\.\d+\.\d+/.exec(found.version ?? "")?.[0];
|
|
11286
|
+
if (version && !newer(CLAUDE_CODE_MIN, version)) return null;
|
|
11287
|
+
return `a build needs Claude Code ${CLAUDE_CODE_MIN} or later on this machine${found.version ? ` (it has ${found.version})` : ""}: run claude update`;
|
|
11288
|
+
}
|
|
11289
|
+
async function handOverTheSpec(worktree, agentSpec) {
|
|
11290
|
+
await mkdir5(join13(worktree.dir, ".nanopm"), { recursive: true });
|
|
11291
|
+
await writeFile5(join13(worktree.dir, ".nanopm", "SPEC.md"), agentSpec);
|
|
11292
|
+
const exclude = join13(await commonGitDir(worktree), "info", "exclude");
|
|
11293
|
+
const current = await readFile8(exclude, "utf8").catch(() => "");
|
|
11294
|
+
if (!current.split("\n").includes(".nanopm/")) {
|
|
11295
|
+
await mkdir5(join13(exclude, ".."), { recursive: true });
|
|
11296
|
+
await writeFile5(exclude, `${current}${current && !current.endsWith("\n") ? "\n" : ""}.nanopm/
|
|
11297
|
+
`);
|
|
11298
|
+
}
|
|
11299
|
+
}
|
|
11300
|
+
function stepsOf(input) {
|
|
11301
|
+
const steps = input?.steps;
|
|
11302
|
+
return Array.isArray(steps) && steps.length && steps.every((s) => typeof s === "string") ? steps : null;
|
|
11303
|
+
}
|
|
11304
|
+
function carryOn(plan, done) {
|
|
11305
|
+
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.";
|
|
11306
|
+
const left = stepsLeft(plan, done);
|
|
11307
|
+
return [
|
|
11308
|
+
`Steps ${left.map((i) => i + 1).join(", ")} of your plan are not done yet:`,
|
|
11309
|
+
...left.map((i) => `${i + 1}. ${plan[i]}`),
|
|
11310
|
+
"",
|
|
11311
|
+
"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."
|
|
11312
|
+
].join("\n");
|
|
11313
|
+
}
|
|
11314
|
+
async function solutionPage(client, projectId, ref) {
|
|
11315
|
+
const { data } = await client.from("projects").select("slug").eq("id", projectId).maybeSingle();
|
|
11316
|
+
return data?.slug ? `${webUrl()}/p/${data.slug}/roadmap/${ref}` : null;
|
|
11317
|
+
}
|
|
11318
|
+
function prBody(said, page) {
|
|
11319
|
+
return [
|
|
11320
|
+
said.trim(),
|
|
11321
|
+
page ? `
|
|
11322
|
+
The solution and its spec: ${page}` : "",
|
|
11323
|
+
"\n\u2014 Built by NanoPM with Claude Code. A draft: review before merging."
|
|
11324
|
+
].filter(Boolean).join("\n");
|
|
11325
|
+
}
|
|
11326
|
+
async function startSolution(client, projectId, ref, jobId) {
|
|
11327
|
+
const { error } = await client.rpc("start_solution", { p_project_id: projectId, p_ref: ref, p_job_id: jobId });
|
|
11328
|
+
if (error) throw new Error(pgMessage(error));
|
|
11329
|
+
}
|
|
11330
|
+
async function unstartSolution(client, projectId, ref, was) {
|
|
11331
|
+
await client.rpc("unstart_solution", { p_project_id: projectId, p_ref: ref, p_to: was });
|
|
11332
|
+
}
|
|
11333
|
+
async function requeueLostBuild(client, job) {
|
|
11334
|
+
const params = job.params ?? {};
|
|
11335
|
+
if ((job.attempt ?? 1) < 2) {
|
|
11336
|
+
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");
|
|
11337
|
+
if (data?.length) return true;
|
|
11338
|
+
}
|
|
11339
|
+
const { data: solution } = await client.from("solutions").select("ref").eq("id", params.solution_id ?? "").maybeSingle();
|
|
11340
|
+
if (job.project_id && solution) await unstartSolution(client, job.project_id, solution.ref, wasOf(job.params)).catch(() => void 0);
|
|
11341
|
+
return false;
|
|
11342
|
+
}
|
|
11343
|
+
async function resumePausedBuilds(client, daemonId, now = Date.now()) {
|
|
11344
|
+
const { data: parked } = await client.from("jobs").select("id").eq("skill", BUILD_SKILL).eq("status", "waiting").eq("assigned_daemon_id", daemonId);
|
|
11345
|
+
const ids = (parked ?? []).map((j) => j.id);
|
|
11346
|
+
if (!ids.length) return [];
|
|
11347
|
+
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 });
|
|
11348
|
+
const last = /* @__PURE__ */ new Map();
|
|
11349
|
+
for (const e of stops ?? []) if (!last.has(e.job_id)) last.set(e.job_id, e);
|
|
11350
|
+
const due = ids.filter((id) => {
|
|
11351
|
+
const stop2 = last.get(id);
|
|
11352
|
+
return stop2?.kind === "paused" && Date.parse(String(stop2.payload?.resets_at ?? "")) <= now;
|
|
11353
|
+
});
|
|
11354
|
+
if (!due.length) return [];
|
|
11355
|
+
const { data } = await client.from("jobs").update({ status: "queued", assigned_daemon_id: null }).in("id", due).eq("status", "waiting").select("id");
|
|
11356
|
+
return (data ?? []).map((j) => j.id);
|
|
11357
|
+
}
|
|
11358
|
+
async function shippedBuilds(client, daemonId, userId) {
|
|
11359
|
+
const { data: building } = await client.from("solutions").select("id, ref, project_id").eq("status", "building");
|
|
11360
|
+
if (!building?.length) return [];
|
|
11361
|
+
const [{ data: built }, { data: clones }] = await Promise.all([
|
|
11362
|
+
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)),
|
|
11363
|
+
client.from("clones").select("project_id, repo_path").eq("daemon_id", daemonId)
|
|
11364
|
+
]);
|
|
11365
|
+
const cloneOf = new Map((clones ?? []).map((c) => [c.project_id, c.repo_path]));
|
|
11366
|
+
const shipped = [];
|
|
11367
|
+
for (const j of built ?? []) {
|
|
11368
|
+
const solution = building.find((s) => s.id === j.params?.solution_id);
|
|
11369
|
+
const number = j.pr?.number;
|
|
11370
|
+
const clone = solution ? cloneOf.get(solution.project_id) : void 0;
|
|
11371
|
+
if (!solution || typeof number !== "number" || number <= 0 || !clone || !existsSync5(clone) || !await pullRequestMerged(clone, number)) continue;
|
|
11372
|
+
const { error } = await client.rpc("finish_solution", { p_project_id: solution.project_id, p_ref: solution.ref, p_job_id: j.id });
|
|
11373
|
+
if (error) continue;
|
|
11374
|
+
await logActivity({ client }, { id: j.id, project_id: solution.project_id }, "shipped", `Pull request #${number} merged: ${solution.ref} shipped`);
|
|
11375
|
+
shipped.push(j.id);
|
|
11376
|
+
}
|
|
11377
|
+
return shipped;
|
|
11378
|
+
}
|
|
11379
|
+
async function tellTheApp(accessToken, jobId, fetchImpl = fetch) {
|
|
11380
|
+
try {
|
|
11381
|
+
const response = await fetchImpl(`${webUrl()}/api/build/news`, {
|
|
11382
|
+
method: "POST",
|
|
11383
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${accessToken}` },
|
|
11384
|
+
body: JSON.stringify({ job_id: jobId })
|
|
11385
|
+
});
|
|
11386
|
+
const body = await response.json().catch(() => ({}));
|
|
11387
|
+
return response.ok ? body.said ?? "nothing to say" : `not said \u2014 ${body.error ?? `the app answered ${response.status}`}`;
|
|
11388
|
+
} catch (err) {
|
|
11389
|
+
return `not said \u2014 ${errorMessage(err)}`;
|
|
11390
|
+
}
|
|
11391
|
+
}
|
|
11392
|
+
|
|
11393
|
+
// ../daemon/src/review-preflight.ts
|
|
11394
|
+
async function reviewWithoutData(client, job) {
|
|
11395
|
+
if (job.skill !== "review" || !job.project_id) return null;
|
|
11396
|
+
if (job.trigger === "user") return null;
|
|
11397
|
+
const { data: connections } = await client.from("connections").select("id, status").eq("project_id", job.project_id).is("removed_at", null);
|
|
11398
|
+
const usable = (connections ?? []).filter((c) => c.status === "ok");
|
|
11399
|
+
if (usable.length) return null;
|
|
11400
|
+
const attached = (connections ?? []).length;
|
|
11401
|
+
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.";
|
|
11402
|
+
const { error } = await client.from("reviews").insert({
|
|
11403
|
+
project_id: job.project_id,
|
|
11404
|
+
summary,
|
|
11405
|
+
confirms: [],
|
|
11406
|
+
contradicts: [],
|
|
11407
|
+
job_id: job.id
|
|
11408
|
+
});
|
|
11409
|
+
if (error) return { status: "failed", error: { code: "harness_error", message: `could not record that there was nothing to read: ${error.message}` } };
|
|
11410
|
+
await client.from("activity_log").insert({
|
|
11411
|
+
project_id: job.project_id,
|
|
11412
|
+
job_id: job.id,
|
|
11413
|
+
actor: "daemon",
|
|
11414
|
+
step: "review.skipped",
|
|
11415
|
+
summary: attached ? "no working data source" : "nothing connected",
|
|
11416
|
+
outcome: "ok"
|
|
11417
|
+
});
|
|
11418
|
+
return { status: "succeeded", result: summary };
|
|
11419
|
+
}
|
|
11420
|
+
|
|
11190
11421
|
// ../daemon/src/daemon.ts
|
|
11191
11422
|
var SESSION_HEADROOM_MS = 10 * 60 * 1e3;
|
|
11192
11423
|
var LONGEST_CAP_MS = 3 * 60 * 60 * 1e3;
|
|
@@ -11236,6 +11467,10 @@ async function runDaemon(options = {}) {
|
|
|
11236
11467
|
const startedAt = Date.now();
|
|
11237
11468
|
let lastDeployCheck = 0;
|
|
11238
11469
|
const DEPLOY_CHECK_MS = 30 * 6e4;
|
|
11470
|
+
const PAUSE_CHECK_MS = 6e4;
|
|
11471
|
+
let lastPauseCheck = 0;
|
|
11472
|
+
const SHIP_CHECK_MS = 10 * 6e4;
|
|
11473
|
+
let lastShipCheck = 0;
|
|
11239
11474
|
const UPDATE_CHECK_MS = 60 * 6e4;
|
|
11240
11475
|
const updatable = !options.once && installKind() === "installer";
|
|
11241
11476
|
let lastUpdateCheck = Date.now() - UPDATE_CHECK_MS + 5 * 6e4;
|
|
@@ -11287,12 +11522,23 @@ async function runDaemon(options = {}) {
|
|
|
11287
11522
|
const digest = await sendDigestFor(auth.client, auth.accessToken, job.id);
|
|
11288
11523
|
if (digest) log(`digest for job ${job.id}: ${digest.ok ? digest.detail : `not sent \u2014 ${digest.detail}`}`);
|
|
11289
11524
|
}
|
|
11525
|
+
if (job.skill === BUILD_SKILL && (outcome.status === "waiting" || outcome.status === "succeeded")) {
|
|
11526
|
+
log(`build ${job.id}: ${await tellTheApp(auth.accessToken, job.id)}`);
|
|
11527
|
+
}
|
|
11290
11528
|
await chainAfter(auth, job, outcome, log);
|
|
11291
11529
|
processed += 1;
|
|
11292
11530
|
if (options.once) break;
|
|
11293
11531
|
continue;
|
|
11294
11532
|
}
|
|
11295
11533
|
if (options.once && Date.now() - startedAt > onceTimeoutMs) break;
|
|
11534
|
+
if (!options.once && Date.now() - lastPauseCheck > PAUSE_CHECK_MS) {
|
|
11535
|
+
lastPauseCheck = Date.now();
|
|
11536
|
+
for (const id of await resumePausedBuilds(auth.client, daemonId).catch(() => [])) log(`build ${id}: the Claude limit reset, carrying on`);
|
|
11537
|
+
}
|
|
11538
|
+
if (!options.once && Date.now() - lastShipCheck > SHIP_CHECK_MS) {
|
|
11539
|
+
lastShipCheck = Date.now();
|
|
11540
|
+
for (const id of await shippedBuilds(auth.client, daemonId, auth.userId).catch(() => [])) log(`build ${id}: its pull request merged, shipped`);
|
|
11541
|
+
}
|
|
11296
11542
|
if (!options.once && Date.now() - lastDeployCheck > DEPLOY_CHECK_MS) {
|
|
11297
11543
|
lastDeployCheck = Date.now();
|
|
11298
11544
|
for (const check of await reviewDeploys(auth.client, daemonId).catch(() => [])) {
|
|
@@ -11421,7 +11667,7 @@ var heldInThisProcess = false;
|
|
|
11421
11667
|
function acquireLock() {
|
|
11422
11668
|
if (heldInThisProcess) throw new Error("a nanopm daemon is already running in this process");
|
|
11423
11669
|
mkdirSync(nanopmHome(), { recursive: true, mode: 448 });
|
|
11424
|
-
const path =
|
|
11670
|
+
const path = join14(nanopmHome(), "daemon.pid");
|
|
11425
11671
|
for (; ; ) {
|
|
11426
11672
|
try {
|
|
11427
11673
|
const fd = openSync(path, "wx");
|
|
@@ -11493,10 +11739,11 @@ async function claimNext(client, daemonId, skills) {
|
|
|
11493
11739
|
return rows2[0] ?? null;
|
|
11494
11740
|
}
|
|
11495
11741
|
async function reclaimOwnJobs(client, daemonId) {
|
|
11496
|
-
const { data: stale } = await client.from("jobs").select("id, project_id, skill").eq("assigned_daemon_id", daemonId).in("status", ["running", "waiting"]);
|
|
11742
|
+
const { data: stale } = await client.from("jobs").select("id, project_id, skill, status, attempt, params").eq("assigned_daemon_id", daemonId).in("status", ["running", "waiting"]);
|
|
11497
11743
|
const ids = [];
|
|
11498
11744
|
for (const job of stale ?? []) {
|
|
11499
11745
|
await killOrphanedHarness(job.id);
|
|
11746
|
+
if (job.skill === BUILD_SKILL && (job.status === "waiting" || await requeueLostBuild(client, job))) continue;
|
|
11500
11747
|
const error = { code: "interrupted", message: "the daemon stopped while this job was running" };
|
|
11501
11748
|
await client.from("jobs").update({ status: "failed", finished_at: (/* @__PURE__ */ new Date()).toISOString(), error }).eq("id", job.id).in("status", ["running", "waiting"]);
|
|
11502
11749
|
await client.from("job_events").insert({ job_id: job.id, seq: 0, kind: "status", payload: { status: "failed", error }, author_kind: "nanopm" });
|
|
@@ -11508,13 +11755,13 @@ async function reclaimOwnJobs(client, daemonId) {
|
|
|
11508
11755
|
async function harnessPids(runDir) {
|
|
11509
11756
|
const dirs = [runDir];
|
|
11510
11757
|
try {
|
|
11511
|
-
for (const d of await readdir3(runDir, { withFileTypes: true })) if (d.isDirectory()) dirs.push(
|
|
11758
|
+
for (const d of await readdir3(runDir, { withFileTypes: true })) if (d.isDirectory()) dirs.push(join14(runDir, d.name));
|
|
11512
11759
|
} catch {
|
|
11513
11760
|
}
|
|
11514
11761
|
const pids = [];
|
|
11515
11762
|
for (const dir of dirs) {
|
|
11516
11763
|
try {
|
|
11517
|
-
const pid = Number((await readFile9(
|
|
11764
|
+
const pid = Number((await readFile9(join14(dir, "harness.pid"), "utf8")).trim());
|
|
11518
11765
|
if (pid > 1) pids.push(pid);
|
|
11519
11766
|
} catch {
|
|
11520
11767
|
}
|
|
@@ -11522,7 +11769,7 @@ async function harnessPids(runDir) {
|
|
|
11522
11769
|
return pids;
|
|
11523
11770
|
}
|
|
11524
11771
|
async function killOrphanedHarness(jobId) {
|
|
11525
|
-
const runDir =
|
|
11772
|
+
const runDir = join14(nanopmHome(), "run", jobId);
|
|
11526
11773
|
for (const pid of await harnessPids(runDir)) {
|
|
11527
11774
|
try {
|
|
11528
11775
|
process.kill(pid, "SIGTERM");
|
|
@@ -11532,7 +11779,7 @@ async function killOrphanedHarness(jobId) {
|
|
|
11532
11779
|
await rm5(runDir, { recursive: true, force: true });
|
|
11533
11780
|
}
|
|
11534
11781
|
async function registerDaemon(auth, version, opts = {}) {
|
|
11535
|
-
const path =
|
|
11782
|
+
const path = join14(nanopmHome(), "daemon.json");
|
|
11536
11783
|
let identity = null;
|
|
11537
11784
|
try {
|
|
11538
11785
|
identity = JSON.parse(await readFile9(path, "utf8"));
|
|
@@ -11574,7 +11821,7 @@ async function registerDaemon(auth, version, opts = {}) {
|
|
|
11574
11821
|
}
|
|
11575
11822
|
async function rememberDaemon(id, shared = false) {
|
|
11576
11823
|
await mkdir6(nanopmHome(), { recursive: true, mode: 448 });
|
|
11577
|
-
await writeFile6(
|
|
11824
|
+
await writeFile6(join14(nanopmHome(), "daemon.json"), JSON.stringify({ id, shared }, null, 2) + "\n", { mode: 384 });
|
|
11578
11825
|
}
|
|
11579
11826
|
async function heartbeat(client, daemonId) {
|
|
11580
11827
|
await client.from("daemons").update({ status: "online", last_seen_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", daemonId);
|
|
@@ -11628,13 +11875,15 @@ function contextFromEnv(env = process.env) {
|
|
|
11628
11875
|
userId: env.NANOPM_USER_ID,
|
|
11629
11876
|
actor: env.NANOPM_ACTOR,
|
|
11630
11877
|
askTimeoutMs: Number(env.NANOPM_ASK_TIMEOUT_MS ?? 3 * 60 * 60 * 1e3),
|
|
11631
|
-
workspace: env.NANOPM_WORKSPACE || void 0
|
|
11878
|
+
workspace: env.NANOPM_WORKSPACE || void 0,
|
|
11879
|
+
answerFile: env.NANOPM_ANSWER_FILE || void 0
|
|
11632
11880
|
};
|
|
11633
11881
|
}
|
|
11634
11882
|
|
|
11635
11883
|
// ../memory-mcp/src/server.ts
|
|
11636
11884
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11637
11885
|
import { randomUUID as randomUUID13 } from "crypto";
|
|
11886
|
+
import { readFile as readFile10, unlink } from "fs/promises";
|
|
11638
11887
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
11639
11888
|
import { z as z21 } from "zod";
|
|
11640
11889
|
|
|
@@ -12160,6 +12409,8 @@ ${renderMap()}`,
|
|
|
12160
12409
|
}
|
|
12161
12410
|
},
|
|
12162
12411
|
async ({ question, options, why }, extra) => {
|
|
12412
|
+
const handed = ctx.answerFile ? await takeAnswer(ctx.answerFile) : null;
|
|
12413
|
+
if (handed !== null) return json({ answer: handed, skipped: false });
|
|
12163
12414
|
const answer = await askUser(ctx, { question, options, why }, extra.signal);
|
|
12164
12415
|
if (answer === null) return failure("No answer arrived in time, or the job was cancelled. Do not ask again; continue with what you have.");
|
|
12165
12416
|
return json({ answer, skipped: /^\s*(skip|passe?r?)\s*$/i.test(answer) || answer.trim() === "" });
|
|
@@ -12724,6 +12975,12 @@ async function writeContext(ctx, items, refused = []) {
|
|
|
12724
12975
|
await logActivity2(ctx, "memory.write", `wrote ${written.length} items under ${topics}: ${written.map((w) => w.key).join(", ")}${refusedNote}`);
|
|
12725
12976
|
return { written };
|
|
12726
12977
|
}
|
|
12978
|
+
async function takeAnswer(answerFile) {
|
|
12979
|
+
const taken = `${answerFile}.taken`;
|
|
12980
|
+
const answer = await readFile10(taken, "utf8").catch(() => null);
|
|
12981
|
+
if (answer !== null) await unlink(taken).catch(() => void 0);
|
|
12982
|
+
return answer;
|
|
12983
|
+
}
|
|
12727
12984
|
async function askUser(ctx, q, signal) {
|
|
12728
12985
|
const seq = await nextSeq(ctx);
|
|
12729
12986
|
const { error } = await ctx.client.from("job_events").insert({ job_id: ctx.jobId, seq, kind: "question", payload: { question: q.question, options: q.options ?? null, why: q.why ?? null }, author_kind: "nanopm" });
|
|
@@ -13509,19 +13766,19 @@ async function startMemoryServer(env = process.env) {
|
|
|
13509
13766
|
}
|
|
13510
13767
|
|
|
13511
13768
|
// src/commands/add.ts
|
|
13512
|
-
import { readFile as
|
|
13769
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
13513
13770
|
|
|
13514
13771
|
// src/project.ts
|
|
13515
13772
|
import { execFile as execFile7 } from "child_process";
|
|
13516
|
-
import { basename } from "path";
|
|
13773
|
+
import { basename as basename2 } from "path";
|
|
13517
13774
|
import { promisify as promisify7 } from "util";
|
|
13518
13775
|
|
|
13519
13776
|
// src/local.ts
|
|
13520
|
-
import { readFile as
|
|
13521
|
-
import { join as
|
|
13777
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
13778
|
+
import { join as join15 } from "path";
|
|
13522
13779
|
async function localDaemonId() {
|
|
13523
13780
|
try {
|
|
13524
|
-
const parsed = JSON.parse(await
|
|
13781
|
+
const parsed = JSON.parse(await readFile11(join15(nanopmHome(), "daemon.json"), "utf8"));
|
|
13525
13782
|
return parsed.id ?? null;
|
|
13526
13783
|
} catch {
|
|
13527
13784
|
return null;
|
|
@@ -13546,7 +13803,7 @@ async function detectRepo(cwd) {
|
|
|
13546
13803
|
} catch {
|
|
13547
13804
|
remote = null;
|
|
13548
13805
|
}
|
|
13549
|
-
const name = remote ? remote.split("/").pop() :
|
|
13806
|
+
const name = remote ? remote.split("/").pop() : basename2(root2);
|
|
13550
13807
|
return { root: root2, remote, name, empty: await repoIsEmpty(root2) };
|
|
13551
13808
|
}
|
|
13552
13809
|
async function repoIsEmpty(root2) {
|
|
@@ -13557,7 +13814,7 @@ async function repoIsEmpty(root2) {
|
|
|
13557
13814
|
}
|
|
13558
13815
|
const files = (await execFileAsync7("git", ["-C", root2, "ls-files"])).stdout.split("\n").filter(Boolean);
|
|
13559
13816
|
return files.every((f) => {
|
|
13560
|
-
const base =
|
|
13817
|
+
const base = basename2(f);
|
|
13561
13818
|
return base.startsWith(".") || /^(readme|licen[cs]e)(\.|$)/i.test(base);
|
|
13562
13819
|
});
|
|
13563
13820
|
}
|
|
@@ -13656,7 +13913,7 @@ async function add(file, opts) {
|
|
|
13656
13913
|
text2 = await readStdin();
|
|
13657
13914
|
} else {
|
|
13658
13915
|
try {
|
|
13659
|
-
text2 = await
|
|
13916
|
+
text2 = await readFile12(file, "utf8");
|
|
13660
13917
|
} catch {
|
|
13661
13918
|
throw new Error(`could not read ${file}`);
|
|
13662
13919
|
}
|
|
@@ -13671,7 +13928,7 @@ async function add(file, opts) {
|
|
|
13671
13928
|
}
|
|
13672
13929
|
|
|
13673
13930
|
// src/commands/run.ts
|
|
13674
|
-
import { readFile as
|
|
13931
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
13675
13932
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
13676
13933
|
import { setTimeout as sleep3 } from "timers/promises";
|
|
13677
13934
|
|
|
@@ -14003,7 +14260,7 @@ function questionBlock(p) {
|
|
|
14003
14260
|
|
|
14004
14261
|
// src/commands/run.ts
|
|
14005
14262
|
async function answersFromFile(path) {
|
|
14006
|
-
const raw = JSON.parse(await
|
|
14263
|
+
const raw = JSON.parse(await readFile13(path, "utf8"));
|
|
14007
14264
|
const entries = raw.map((a) => typeof a === "string" ? { answer: a } : a);
|
|
14008
14265
|
return scriptedAnswerer(entries);
|
|
14009
14266
|
}
|
|
@@ -14083,23 +14340,24 @@ async function streamJob(client, jobId, opts) {
|
|
|
14083
14340
|
let cancelSent = false;
|
|
14084
14341
|
let cancelDeadline = Infinity;
|
|
14085
14342
|
let finalStatus = null;
|
|
14086
|
-
const
|
|
14343
|
+
const answered2 = /* @__PURE__ */ new Set();
|
|
14087
14344
|
const shownAnswers = /* @__PURE__ */ new Set();
|
|
14088
14345
|
for (; ; ) {
|
|
14089
14346
|
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 });
|
|
14347
|
+
for (const ev of events ?? []) if (ev.kind === "answer") answered2.add(ev.seq);
|
|
14090
14348
|
for (const ev of events ?? []) {
|
|
14091
14349
|
lastId = Math.max(lastId, Number(ev.id));
|
|
14092
14350
|
const payload = ev.payload ?? {};
|
|
14093
14351
|
if (ev.kind === "answer" && shownAnswers.has(Number(ev.id))) continue;
|
|
14094
14352
|
show(progress.feed({ kind: ev.kind, payload, author_kind: ev.author_kind }));
|
|
14095
|
-
if (ev.kind === "question" && payload.about === "site" && !
|
|
14096
|
-
|
|
14353
|
+
if (ev.kind === "question" && payload.about === "site" && !answered2.has(ev.seq)) {
|
|
14354
|
+
answered2.add(ev.seq);
|
|
14097
14355
|
renderer.settle(" (answer in the browser)");
|
|
14098
14356
|
continue;
|
|
14099
14357
|
}
|
|
14100
|
-
if (ev.kind === "question" && opts.answerer && !cancelSent && !
|
|
14101
|
-
if (
|
|
14102
|
-
|
|
14358
|
+
if (ev.kind === "question" && opts.answerer && !cancelSent && !answered2.has(ev.seq)) {
|
|
14359
|
+
if (answered2.size === 0 && opts.link) renderer.settle(` (answer here, or in the browser: ${opts.link})`);
|
|
14360
|
+
answered2.add(ev.seq);
|
|
14103
14361
|
const q = payload;
|
|
14104
14362
|
renderer.pause();
|
|
14105
14363
|
const outcome = await answerOrWatch(client, jobId, ev.seq, q, opts.answerer, opts.cancelSignal, opts.pollMs);
|
|
@@ -14225,16 +14483,16 @@ async function askOne(auth, projectId, question, daemonId, opts, out) {
|
|
|
14225
14483
|
|
|
14226
14484
|
// src/commands/daemon-service.ts
|
|
14227
14485
|
import { execFile as execFile8 } from "child_process";
|
|
14228
|
-
import { existsSync as
|
|
14229
|
-
import { mkdir as mkdir7, readFile as
|
|
14486
|
+
import { existsSync as existsSync6 } from "fs";
|
|
14487
|
+
import { mkdir as mkdir7, readFile as readFile14, rm as rm6, writeFile as writeFile7 } from "fs/promises";
|
|
14230
14488
|
import { homedir as homedir5 } from "os";
|
|
14231
|
-
import { join as
|
|
14489
|
+
import { join as join16 } from "path";
|
|
14232
14490
|
import { promisify as promisify8 } from "util";
|
|
14233
14491
|
|
|
14234
14492
|
// package.json
|
|
14235
14493
|
var package_default = {
|
|
14236
14494
|
name: "@nanopm/cli",
|
|
14237
|
-
version: "0.1.
|
|
14495
|
+
version: "0.1.13",
|
|
14238
14496
|
description: "An autonomous product manager for builders who run several small products. Runs on your machine, drives your own coding harness.",
|
|
14239
14497
|
type: "module",
|
|
14240
14498
|
license: "MIT",
|
|
@@ -14286,10 +14544,10 @@ var VERSION = package_default.version;
|
|
|
14286
14544
|
var execFileAsync8 = promisify8(execFile8);
|
|
14287
14545
|
var LAUNCHD_LABEL = "pm.nano.daemon";
|
|
14288
14546
|
function plistPath() {
|
|
14289
|
-
return
|
|
14547
|
+
return join16(homedir5(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
14290
14548
|
}
|
|
14291
14549
|
function daemonLogPath() {
|
|
14292
|
-
return
|
|
14550
|
+
return join16(nanopmHome(), "daemon.log");
|
|
14293
14551
|
}
|
|
14294
14552
|
function renderPlist(opts) {
|
|
14295
14553
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -14349,8 +14607,8 @@ async function installDaemon(out = (l) => console.log(l), opts = {}) {
|
|
|
14349
14607
|
const bin = process.argv[1];
|
|
14350
14608
|
if (!bin) throw new Error("cannot locate the nanopm binary");
|
|
14351
14609
|
const home = homedir5();
|
|
14352
|
-
const path = [process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
14353
|
-
await mkdir7(
|
|
14610
|
+
const path = [process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", join16(home, ".local", "bin")].join(":");
|
|
14611
|
+
await mkdir7(join16(home, "Library", "LaunchAgents"), { recursive: true });
|
|
14354
14612
|
await mkdir7(nanopmHome(), { recursive: true, mode: 448 });
|
|
14355
14613
|
if (opts.shared !== void 0) await rememberShared(opts.shared);
|
|
14356
14614
|
await found({ ...detection, authenticated: true });
|
|
@@ -14367,7 +14625,7 @@ function guiTarget() {
|
|
|
14367
14625
|
return `gui/${process.getuid?.() ?? 501}`;
|
|
14368
14626
|
}
|
|
14369
14627
|
async function restartDaemon() {
|
|
14370
|
-
if (!
|
|
14628
|
+
if (!existsSync6(plistPath())) return false;
|
|
14371
14629
|
await execFileAsync8("launchctl", ["kickstart", "-k", `${guiTarget()}/${LAUNCHD_LABEL}`]);
|
|
14372
14630
|
return true;
|
|
14373
14631
|
}
|
|
@@ -14380,7 +14638,7 @@ async function uninstallDaemon(out = (l) => console.log(l)) {
|
|
|
14380
14638
|
}
|
|
14381
14639
|
async function daemonStatus(out = (l) => console.log(l)) {
|
|
14382
14640
|
const id = await localDaemonId();
|
|
14383
|
-
const installed = process.platform === "darwin" &&
|
|
14641
|
+
const installed = process.platform === "darwin" && existsSync6(plistPath());
|
|
14384
14642
|
out(`launchd: ${installed ? "installed" : "not installed"}`);
|
|
14385
14643
|
if (!id) {
|
|
14386
14644
|
out("daemon: never registered on this machine");
|
|
@@ -14402,7 +14660,7 @@ async function daemonStatus(out = (l) => console.log(l)) {
|
|
|
14402
14660
|
}
|
|
14403
14661
|
async function daemonLogs(lines = 50) {
|
|
14404
14662
|
try {
|
|
14405
|
-
const text2 = await
|
|
14663
|
+
const text2 = await readFile14(daemonLogPath(), "utf8");
|
|
14406
14664
|
return text2.trimEnd().split("\n").slice(-lines).join("\n");
|
|
14407
14665
|
} catch {
|
|
14408
14666
|
return `no log at ${daemonLogPath()} yet`;
|
|
@@ -14410,17 +14668,17 @@ async function daemonLogs(lines = 50) {
|
|
|
14410
14668
|
}
|
|
14411
14669
|
async function localWebSearch() {
|
|
14412
14670
|
try {
|
|
14413
|
-
const parsed = JSON.parse(await
|
|
14671
|
+
const parsed = JSON.parse(await readFile14(join16(nanopmHome(), "daemon.json"), "utf8"));
|
|
14414
14672
|
return parsed.webSearch === true;
|
|
14415
14673
|
} catch {
|
|
14416
14674
|
return false;
|
|
14417
14675
|
}
|
|
14418
14676
|
}
|
|
14419
14677
|
async function rememberWebSearch(webSearch2) {
|
|
14420
|
-
const path =
|
|
14678
|
+
const path = join16(nanopmHome(), "daemon.json");
|
|
14421
14679
|
let parsed = {};
|
|
14422
14680
|
try {
|
|
14423
|
-
parsed = JSON.parse(await
|
|
14681
|
+
parsed = JSON.parse(await readFile14(path, "utf8"));
|
|
14424
14682
|
} catch {
|
|
14425
14683
|
parsed = {};
|
|
14426
14684
|
}
|
|
@@ -14429,17 +14687,17 @@ async function rememberWebSearch(webSearch2) {
|
|
|
14429
14687
|
}
|
|
14430
14688
|
async function localShared() {
|
|
14431
14689
|
try {
|
|
14432
|
-
const parsed = JSON.parse(await
|
|
14690
|
+
const parsed = JSON.parse(await readFile14(join16(nanopmHome(), "daemon.json"), "utf8"));
|
|
14433
14691
|
return Boolean(parsed.shared);
|
|
14434
14692
|
} catch {
|
|
14435
14693
|
return false;
|
|
14436
14694
|
}
|
|
14437
14695
|
}
|
|
14438
14696
|
async function rememberShared(shared) {
|
|
14439
|
-
const path =
|
|
14697
|
+
const path = join16(nanopmHome(), "daemon.json");
|
|
14440
14698
|
let parsed = {};
|
|
14441
14699
|
try {
|
|
14442
|
-
parsed = JSON.parse(await
|
|
14700
|
+
parsed = JSON.parse(await readFile14(path, "utf8"));
|
|
14443
14701
|
} catch {
|
|
14444
14702
|
parsed = {};
|
|
14445
14703
|
}
|
|
@@ -14751,6 +15009,12 @@ async function verifyCommand(command, opts) {
|
|
|
14751
15009
|
return [`${project.name} will verify with:`, "", ` ${next}`, "", "A build runs it in its sandboxed copy of the repository before its last step."].join("\n");
|
|
14752
15010
|
}
|
|
14753
15011
|
|
|
15012
|
+
// src/commands/build.ts
|
|
15013
|
+
import { execFile as execFile10 } from "child_process";
|
|
15014
|
+
import { existsSync as existsSync7 } from "fs";
|
|
15015
|
+
import { homedir as homedir6 } from "os";
|
|
15016
|
+
import { promisify as promisify9 } from "util";
|
|
15017
|
+
|
|
14754
15018
|
// src/format/opportunities.ts
|
|
14755
15019
|
function renderOpportunities(rows2, lens, opts) {
|
|
14756
15020
|
const paint = painter(opts.color);
|
|
@@ -15625,35 +15889,64 @@ function normalizeRef(input, items) {
|
|
|
15625
15889
|
}
|
|
15626
15890
|
|
|
15627
15891
|
// src/commands/build.ts
|
|
15892
|
+
var execFileAsync9 = promisify9(execFile10);
|
|
15628
15893
|
async function build(ref, opts) {
|
|
15629
15894
|
const { renderer, out } = rendererFor(opts.out);
|
|
15630
15895
|
const auth = await authenticate();
|
|
15631
15896
|
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15632
15897
|
const repo = await detectRepo(opts.cwd);
|
|
15633
15898
|
if (!repo) throw new Error("run this inside the repository, so NanoPM builds from the clone you are looking at");
|
|
15634
|
-
const
|
|
15635
|
-
|
|
15636
|
-
|
|
15637
|
-
|
|
15638
|
-
|
|
15899
|
+
const { data: solutions } = await auth.client.from("solutions").select("id, ref, title, status").eq("project_id", project.id).is("superseded_by", null);
|
|
15900
|
+
const wanted = normalizeRef(ref, solutions ?? []);
|
|
15901
|
+
const solution = (solutions ?? []).find((s) => s.ref === wanted);
|
|
15902
|
+
if (!solution) throw new Error(`No solution ${wanted} on the Roadmap.`);
|
|
15903
|
+
const stream = async (jobId2) => {
|
|
15904
|
+
out(`${solution.ref} ${solution.title}`);
|
|
15905
|
+
const answerer = opts.answersFile ? await answersFromFile(opts.answersFile) : answersFromTerminal(out);
|
|
15906
|
+
return streamJob(auth.client, jobId2, { pollMs: 700, out, renderer, cancelSignal: opts.cancelSignal, answerer, verbose: verboseWanted() });
|
|
15907
|
+
};
|
|
15908
|
+
const live = await buildOf(auth.client, project.id, [...ACTIVE_STATUSES]);
|
|
15909
|
+
if (live) {
|
|
15910
|
+
if (live.solutionId !== solution.id) throw new Error(`${project.name} already has a build on its way: one at a time. \`nanopm build\` on that solution comes back to it.`);
|
|
15911
|
+
if (opts.throwAway) throw new Error(`${solution.ref}'s build is still on its way: stop it first (\`nanopm stop\`).`);
|
|
15912
|
+
return stream(live.id);
|
|
15913
|
+
}
|
|
15914
|
+
const stopped = opts.continue || opts.throwAway ? await buildOf(auth.client, project.id, ["failed", "cancelled"], solution.id) : null;
|
|
15915
|
+
if ((opts.continue || opts.throwAway) && !stopped) throw new Error(`${solution.ref} has no stopped build to ${opts.continue ? "continue" : "throw away"}.`);
|
|
15916
|
+
if (opts.throwAway) return throwAway(auth.client, stopped.origin, repo.root, out);
|
|
15917
|
+
const [daemon, local] = await Promise.all([awakeDaemon(auth.client, auth.userId), localDaemonId()]);
|
|
15639
15918
|
if (!local || daemon?.id !== local) throw new Error("This machine\u2019s daemon is asleep, so nothing was queued. `nanopm daemon install` starts it; then build again.");
|
|
15640
15919
|
await rememberClone(auth.client, project.id, repo.root);
|
|
15641
|
-
const { jobId
|
|
15920
|
+
const { jobId } = await queueBuild(auth.client, {
|
|
15642
15921
|
projectId: project.id,
|
|
15643
15922
|
userId: auth.userId,
|
|
15644
15923
|
daemonId: local,
|
|
15645
|
-
ref:
|
|
15924
|
+
ref: solution.ref,
|
|
15646
15925
|
repoPath: repo.root,
|
|
15647
|
-
anyway: opts.anyway
|
|
15926
|
+
anyway: opts.anyway,
|
|
15927
|
+
...stopped ? { resumeOf: { id: stopped.id, was: stopped.was, origin: stopped.origin } } : {}
|
|
15648
15928
|
});
|
|
15649
|
-
|
|
15650
|
-
|
|
15651
|
-
|
|
15929
|
+
return stream(jobId);
|
|
15930
|
+
}
|
|
15931
|
+
async function buildOf(client, projectId, statuses, solutionId) {
|
|
15932
|
+
let query = client.from("jobs").select("id, params").eq("project_id", projectId).eq("skill", BUILD_SKILL).in("status", statuses);
|
|
15933
|
+
if (solutionId) query = query.eq("params->>solution_id", solutionId);
|
|
15934
|
+
const { data } = await query.order("created_at", { ascending: false }).limit(1).maybeSingle();
|
|
15935
|
+
const params = data?.params ?? {};
|
|
15936
|
+
return data ? { id: data.id, solutionId: params.solution_id ?? "", was: wasOf(data.params), origin: params.origin ?? data.id } : null;
|
|
15937
|
+
}
|
|
15938
|
+
async function throwAway(client, jobId, clone, out) {
|
|
15939
|
+
const { place } = await loadBuildHistory(client, [jobId], { home: homedir6(), jobId });
|
|
15940
|
+
if (!place) throw new Error("that build left nothing on this machine.");
|
|
15941
|
+
if (existsSync7(place.dir)) await execFileAsync9("git", ["-C", clone, "worktree", "remove", "--force", place.dir]);
|
|
15942
|
+
await execFileAsync9("git", ["-C", clone, "branch", "-D", place.branch]).catch(() => void 0);
|
|
15943
|
+
out(`Thrown away: the copy of the code and the local branch ${place.branch}.`);
|
|
15944
|
+
return 0;
|
|
15652
15945
|
}
|
|
15653
15946
|
|
|
15654
15947
|
// src/detect-sources.ts
|
|
15655
|
-
import { readFile as
|
|
15656
|
-
import { join as
|
|
15948
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
15949
|
+
import { join as join17 } from "path";
|
|
15657
15950
|
var ENV_FILES = [".env", ".env.local", ".env.development.local", ".env.development", ".env.example"];
|
|
15658
15951
|
var POSTGRES_KEYS = [
|
|
15659
15952
|
"DATABASE_URL",
|
|
@@ -15703,7 +15996,7 @@ async function envFiles(repoRoot) {
|
|
|
15703
15996
|
const out = [];
|
|
15704
15997
|
for (const file of ENV_FILES) {
|
|
15705
15998
|
try {
|
|
15706
|
-
out.push({ file, env: parseEnv(await
|
|
15999
|
+
out.push({ file, env: parseEnv(await readFile15(join17(repoRoot, file), "utf8")) });
|
|
15707
16000
|
} catch {
|
|
15708
16001
|
continue;
|
|
15709
16002
|
}
|
|
@@ -15915,13 +16208,13 @@ async function spec(ref, opts) {
|
|
|
15915
16208
|
const auth = await authenticate();
|
|
15916
16209
|
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15917
16210
|
const wanted = ref.trim().toUpperCase();
|
|
15918
|
-
const { data: solution } = await auth.client.from("solutions").select("id, ref, title, agent_spec").eq("project_id", project.id).eq("ref", wanted).maybeSingle();
|
|
16211
|
+
const { data: solution } = await auth.client.from("solutions").select("id, ref, title, agent_spec, spec").eq("project_id", project.id).eq("ref", wanted).maybeSingle();
|
|
15919
16212
|
if (!solution) throw new Error(`no solution ${wanted}. Run \`nanopm roadmap\` to see them.`);
|
|
15920
16213
|
if (opts.write && opts.rework) throw new Error("--write or --rework, not both.");
|
|
15921
16214
|
const mode = opts.rework ? "rework" : opts.write ? "write" : null;
|
|
15922
16215
|
if (!mode) {
|
|
15923
16216
|
if (!solution.agent_spec) return `${solution.ref} has no specs yet: \`nanopm spec ${solution.ref} --write\`, or *Write the specs* on its sheet.`;
|
|
15924
|
-
return solution.agent_spec.trimEnd();
|
|
16217
|
+
return specForAgent(solution.agent_spec, solution.spec).trimEnd();
|
|
15925
16218
|
}
|
|
15926
16219
|
const daemon = await awakeDaemon(auth.client, auth.userId);
|
|
15927
16220
|
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.");
|
|
@@ -16472,7 +16765,7 @@ async function seedWatches(watches2, read6) {
|
|
|
16472
16765
|
}
|
|
16473
16766
|
|
|
16474
16767
|
// src/commands/watch.ts
|
|
16475
|
-
import { readFile as
|
|
16768
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
16476
16769
|
function cursorFor(source, watchId) {
|
|
16477
16770
|
const all = source.cursor ?? {};
|
|
16478
16771
|
return all[watchId] ?? {};
|
|
@@ -16533,7 +16826,7 @@ async function addWatch(labelOrId, id, opts) {
|
|
|
16533
16826
|
const paint = painter(color);
|
|
16534
16827
|
if (!labelOrId || !id) throw new Error('say which feed and what to call it: nanopm watch add <source> <id> --query "\u2026"');
|
|
16535
16828
|
if (opts.query && opts.queryFile) throw new Error("--query or --query-file, not both");
|
|
16536
|
-
const query = opts.queryFile ? await
|
|
16829
|
+
const query = opts.queryFile ? await readFile16(opts.queryFile, "utf8") : opts.query ?? "";
|
|
16537
16830
|
const watch = watchInput(id, query, opts);
|
|
16538
16831
|
const auth = await authenticate();
|
|
16539
16832
|
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
@@ -16807,7 +17100,7 @@ Review this suggestion on the Problems page.`);
|
|
|
16807
17100
|
async function problemList(client, projectId, color) {
|
|
16808
17101
|
const list = await problemRows(client, projectId);
|
|
16809
17102
|
const { count: manualMerges } = await client.from("decisions").select("id", { count: "exact", head: true }).eq("project_id", projectId).eq("kind", "merge");
|
|
16810
|
-
return renderProblems(list, await
|
|
17103
|
+
return renderProblems(list, await openQuestions2(client, projectId, list), {
|
|
16811
17104
|
color,
|
|
16812
17105
|
manualMerges: manualMerges ?? 0
|
|
16813
17106
|
});
|
|
@@ -16842,7 +17135,7 @@ async function keptSeparate(client, projectId) {
|
|
|
16842
17135
|
return a && b ? [a, b].sort().join("|") : null;
|
|
16843
17136
|
}).filter((k) => Boolean(k));
|
|
16844
17137
|
}
|
|
16845
|
-
async function
|
|
17138
|
+
async function openQuestions2(client, projectId, list) {
|
|
16846
17139
|
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 });
|
|
16847
17140
|
const open = new Set(list.map((p) => p.ref));
|
|
16848
17141
|
const pairs = [];
|
|
@@ -17466,7 +17759,7 @@ async function invite(email, opts) {
|
|
|
17466
17759
|
const [, ...rest] = inviteLines({ name: project.name, code, email: email.trim().toLowerCase(), web: webUrl() });
|
|
17467
17760
|
return [`Invite to ${paint(ANSI.bold, project.name)}: ${paint(ANSI.bold, code)}`, ...rest.map((l) => l ? paint(ANSI.dim, l) : l)].join("\n");
|
|
17468
17761
|
}
|
|
17469
|
-
async function
|
|
17762
|
+
async function join18(code, opts) {
|
|
17470
17763
|
const normalized2 = code.trim().toUpperCase();
|
|
17471
17764
|
if (!INVITE_CODE.test(normalized2)) throw new Error(`that does not look like an invite code: ten letters and digits, like ${"K7PQ2XM9RT"}`);
|
|
17472
17765
|
const repo = await detectRepo(opts.cwd);
|
|
@@ -17751,7 +18044,7 @@ async function showSchedule(opts) {
|
|
|
17751
18044
|
}
|
|
17752
18045
|
|
|
17753
18046
|
// src/commands/status.ts
|
|
17754
|
-
import { existsSync as
|
|
18047
|
+
import { existsSync as existsSync8 } from "fs";
|
|
17755
18048
|
async function status(opts) {
|
|
17756
18049
|
const auth = await authenticate();
|
|
17757
18050
|
const color = opts.color ?? useColor();
|
|
@@ -17804,7 +18097,7 @@ async function projectStatus(client, project) {
|
|
|
17804
18097
|
openProblems: (problems2.data ?? []).length,
|
|
17805
18098
|
unprovenProblems: (problems2.data ?? []).filter((p) => p.importance_basis === "judgment").length,
|
|
17806
18099
|
search: describeSearch(search, await localWebSearch()),
|
|
17807
|
-
onThisMachine: Boolean(project.repoPath &&
|
|
18100
|
+
onThisMachine: Boolean(project.repoPath && existsSync8(project.repoPath)),
|
|
17808
18101
|
measures: measures({
|
|
17809
18102
|
solutionStatuses: (roadmap2.data ?? []).map((r) => r.status),
|
|
17810
18103
|
decisions: decisions2.data ?? [],
|
|
@@ -18167,7 +18460,7 @@ function buildProgram() {
|
|
|
18167
18460
|
console.log(await invite(email, { cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
|
|
18168
18461
|
});
|
|
18169
18462
|
program.command("join <code>").description("Join a project you were invited to, from inside your clone of its repository").option("--no-color", "plain output").action(async (code, opts) => {
|
|
18170
|
-
console.log(await
|
|
18463
|
+
console.log(await join18(code, { cwd: process.cwd(), color: colorOf(opts) }));
|
|
18171
18464
|
});
|
|
18172
18465
|
program.command("members").description("Who is on this project, and whether their daemon is awake").option("--project <id>", "project id (default: the project of the current repo)").option("--no-color", "plain output").action(async (opts) => {
|
|
18173
18466
|
console.log(await members({ cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
|
|
@@ -18189,10 +18482,11 @@ function buildProgram() {
|
|
|
18189
18482
|
program.command("status").description("What is happening with this product: next review, what is waiting on you, whether the daemon is awake").option("--project <id>", "project id (default: the project of the current repo)").option("--no-color", "plain output").action(async (opts) => {
|
|
18190
18483
|
console.log(await status({ cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
|
|
18191
18484
|
});
|
|
18192
|
-
program.command("build <ref>").description("Build a solution from its agent spec, in a copy of this repository, to a draft pull request").option("--project <id>", "project id (default: the project of the current repo)").option("--anyway", "build although its questions for you are not answered").option("--answers <file>", "answer its questions from a file instead of the terminal").action(async (ref, opts) => {
|
|
18485
|
+
program.command("build <ref>").description("Build a solution from its agent spec, in a copy of this repository, to a draft pull request").option("--project <id>", "project id (default: the project of the current repo)").option("--anyway", "build although its questions for you are not answered").option("--continue", "take up a stopped build where it was").option("--throw-away", "delete a stopped build's copy of the code and its local branch").option("--answers <file>", "answer its questions from a file instead of the terminal").action(async (ref, opts) => {
|
|
18193
18486
|
const cancel = new AbortController();
|
|
18194
18487
|
process.once("SIGINT", () => cancel.abort());
|
|
18195
|
-
|
|
18488
|
+
if (opts.continue && opts.throwAway) throw new Error("--continue or --throw-away, not both.");
|
|
18489
|
+
process.exitCode = await build(ref, { cwd: process.cwd(), projectId: opts.project, anyway: opts.anyway, continue: opts.continue, throwAway: opts.throwAway, answersFile: opts.answers, cancelSignal: cancel.signal });
|
|
18196
18490
|
});
|
|
18197
18491
|
program.command("autonomy [level]").description("How far NanoPM may go on its own: read it, or set it").option("--project <id>", "project id (default: the project of the current repo)").action(async (level, opts) => {
|
|
18198
18492
|
console.log(await autonomy(level, { cwd: process.cwd(), projectId: opts.project }));
|