@nanopm/cli 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1323 -1378
- package/package.json +1 -1
- package/skills/build/SKILL.md +62 -0
- package/skills/build/skill.json +15 -0
- package/skills/next/SKILL.md +0 -75
- package/skills/next/skill.json +0 -8
package/dist/index.js
CHANGED
|
@@ -156,7 +156,8 @@ var NEEDS_GO_AHEAD = /* @__PURE__ */ new Set(["execute_after_validation"]);
|
|
|
156
156
|
function requiresGoAhead(autonomy2) {
|
|
157
157
|
return NEEDS_GO_AHEAD.has(autonomy2);
|
|
158
158
|
}
|
|
159
|
-
function capCapabilities(requested, autonomy2) {
|
|
159
|
+
function capCapabilities(requested, autonomy2, opts = {}) {
|
|
160
|
+
if (opts.asked) return [...requested];
|
|
160
161
|
const denied = CAPPED_BELOW[autonomy2];
|
|
161
162
|
const granted = requested.filter((c) => !denied.has(c));
|
|
162
163
|
if (granted.includes("write_repo") && requiresGoAhead(autonomy2) && !granted.includes("ask_user")) {
|
|
@@ -387,6 +388,12 @@ var SkillManifestSchema = z2.object({
|
|
|
387
388
|
},
|
|
388
389
|
{ message: "timeout_deep must look like 3h, 5m, 90s" }
|
|
389
390
|
).optional(),
|
|
391
|
+
/**
|
|
392
|
+
* Run only when the founder asks for it on one item, by a click or a command, never by Nano on
|
|
393
|
+
* its own: the autonomy dial, which governs what Nano does unasked, does not cap it
|
|
394
|
+
* (`capCapabilities`). A build is the one today (docs/⏳nanopm-75-build-it.md, settled 2).
|
|
395
|
+
*/
|
|
396
|
+
asked: z2.boolean().default(false),
|
|
390
397
|
/** The first user message sent to the harness, unless the job params provide one. */
|
|
391
398
|
prompt: z2.string().min(1),
|
|
392
399
|
/**
|
|
@@ -628,14 +635,6 @@ var SECRET_PATTERNS = [
|
|
|
628
635
|
"**/credentials.json",
|
|
629
636
|
"**/service-account*.json"
|
|
630
637
|
];
|
|
631
|
-
function looksLikeASecret(path) {
|
|
632
|
-
const base = path.replace(/\\/g, "/").split("/").pop() ?? "";
|
|
633
|
-
return SECRET_PATTERNS.some((pattern) => {
|
|
634
|
-
const glob = pattern.replace(/^\*\*\//, "");
|
|
635
|
-
const re = new RegExp(`^${glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`);
|
|
636
|
-
return re.test(base);
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
638
|
|
|
640
639
|
// ../shared/src/stream.ts
|
|
641
640
|
function formatStreamEvent(event) {
|
|
@@ -944,7 +943,7 @@ var AFTER_DEEP = {
|
|
|
944
943
|
function daemonQueues(skill, depth = 0, parent = null, deep = false) {
|
|
945
944
|
if (deep) return depth >= CHAIN_DEPTH_MAX ? [] : AFTER_DEEP[skill] ?? [];
|
|
946
945
|
const theirs = new Set(SQL_OWNED[skill] ?? []);
|
|
947
|
-
return chainedAfter(skill, depth, parent).filter((
|
|
946
|
+
return chainedAfter(skill, depth, parent).filter((next) => !theirs.has(next));
|
|
948
947
|
}
|
|
949
948
|
var NOT_CHAINED_BY_DAEMON = ["ingest"];
|
|
950
949
|
var CHAIN_DEPTH_MAX = 3;
|
|
@@ -954,7 +953,7 @@ function chainedAfter(skill, depth = 0, parent = null) {
|
|
|
954
953
|
const gates = ONLY_AFTER[skill] ?? {};
|
|
955
954
|
const bars2 = NEVER_AFTER[skill] ?? {};
|
|
956
955
|
return (CHAINED[skill] ?? []).filter(
|
|
957
|
-
(
|
|
956
|
+
(next) => (!gates[next] || parent !== null && gates[next].includes(parent)) && !(parent !== null && bars2[next]?.includes(parent))
|
|
958
957
|
);
|
|
959
958
|
}
|
|
960
959
|
var CHAINED_SKILLS = [...new Set(Object.values(CHAINED).flatMap((s) => s ?? []))];
|
|
@@ -974,7 +973,7 @@ var SKILL_SENTENCE = {
|
|
|
974
973
|
direction: "working out where this is going",
|
|
975
974
|
solutions: "proposing options",
|
|
976
975
|
review: "reviewing",
|
|
977
|
-
|
|
976
|
+
build: "building",
|
|
978
977
|
ask: "answering a question",
|
|
979
978
|
want: "shaping what you want",
|
|
980
979
|
ingest: "watching the numbers",
|
|
@@ -982,22 +981,22 @@ var SKILL_SENTENCE = {
|
|
|
982
981
|
spec: "writing specs"
|
|
983
982
|
};
|
|
984
983
|
function skillSentence(skill, opts = {}) {
|
|
985
|
-
if (skill === "
|
|
984
|
+
if (skill === "build") return opts.ref ? `building ${opts.ref}` : "building an option";
|
|
986
985
|
return SKILL_SENTENCE[skill] ?? `running ${skill}`;
|
|
987
986
|
}
|
|
988
987
|
|
|
989
988
|
// ../shared/src/time.ts
|
|
990
989
|
var DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
991
990
|
function describeNextRun(schedule, now = /* @__PURE__ */ new Date(), opts = {}) {
|
|
992
|
-
const
|
|
991
|
+
const next = new Date(schedule.next_due_at);
|
|
993
992
|
const local = new Intl.DateTimeFormat("en-GB", {
|
|
994
993
|
timeZone: schedule.timezone,
|
|
995
994
|
weekday: "long",
|
|
996
995
|
hour: "2-digit",
|
|
997
996
|
minute: "2-digit",
|
|
998
997
|
hour12: false
|
|
999
|
-
}).format(
|
|
1000
|
-
const hours = Math.round((
|
|
998
|
+
}).format(next);
|
|
999
|
+
const hours = Math.round((next.getTime() - now.getTime()) / 36e5);
|
|
1001
1000
|
const when = hours < 1 ? "any minute" : hours < 36 ? `in ${hours}h` : `in ${Math.round(hours / 24)} days`;
|
|
1002
1001
|
return opts.timezone === false ? `${local}, ${when}` : `${local} ${schedule.timezone}, ${when}`;
|
|
1003
1002
|
}
|
|
@@ -2018,10 +2017,10 @@ function placeRanks(crew, pinned) {
|
|
|
2018
2017
|
while (slots[at] !== null && at > 0) at -= 1;
|
|
2019
2018
|
slots[at] = id;
|
|
2020
2019
|
}
|
|
2021
|
-
let
|
|
2020
|
+
let next = 0;
|
|
2022
2021
|
for (let i = 0; i < total; i += 1) {
|
|
2023
2022
|
if (slots[i] !== null) continue;
|
|
2024
|
-
slots[i] = loose[
|
|
2023
|
+
slots[i] = loose[next++] ?? null;
|
|
2025
2024
|
}
|
|
2026
2025
|
const out = /* @__PURE__ */ new Map();
|
|
2027
2026
|
slots.forEach((id, i) => id && out.set(id, i + 1));
|
|
@@ -2672,7 +2671,7 @@ import { z as z9 } from "zod";
|
|
|
2672
2671
|
var PITCH_LINE_KEYS = ["what", "who", "need"];
|
|
2673
2672
|
var PITCH_TOPIC = "identity";
|
|
2674
2673
|
var PITCH_KEY = /^pitch-/;
|
|
2675
|
-
var pitchKey = (line,
|
|
2674
|
+
var pitchKey = (line, next = false) => `pitch-${line}${next ? "-next" : ""}`;
|
|
2676
2675
|
var PITCH_LINE_TITLE = { what: "What it is", who: "Who it is for", need: "What they come for" };
|
|
2677
2676
|
|
|
2678
2677
|
// ../shared/src/pitch-crew.ts
|
|
@@ -2724,7 +2723,7 @@ var DIRECTION_ROWS = ["mission", "vision"];
|
|
|
2724
2723
|
var MISSION_TOPIC = "mission";
|
|
2725
2724
|
var VISION_TOPIC = "vision";
|
|
2726
2725
|
var DIRECTION_KEY = /^(?:mission|vision)(?:-next)?$/;
|
|
2727
|
-
var directionKey = (row,
|
|
2726
|
+
var directionKey = (row, next = false) => `${row}${next ? "-next" : ""}`;
|
|
2728
2727
|
var DIRECTION_ROW_TITLE = { mission: "Mission", vision: "Vision" };
|
|
2729
2728
|
|
|
2730
2729
|
// ../shared/src/direction-crew.ts
|
|
@@ -3483,7 +3482,7 @@ function foldRound(r) {
|
|
|
3483
3482
|
}
|
|
3484
3483
|
}
|
|
3485
3484
|
const answer = TalkAnswer.safeParse(r.result);
|
|
3486
|
-
const
|
|
3485
|
+
const failed2 = r.status === "failed" || r.status === "cancelled";
|
|
3487
3486
|
const error = r.error ?? {};
|
|
3488
3487
|
return {
|
|
3489
3488
|
id: r.id,
|
|
@@ -3494,8 +3493,8 @@ function foldRound(r) {
|
|
|
3494
3493
|
depth: params.data.depth,
|
|
3495
3494
|
actions: [...actions.values()],
|
|
3496
3495
|
answer: answer.success ? answer.data : null,
|
|
3497
|
-
state: r.status === "succeeded" ? "done" :
|
|
3498
|
-
...
|
|
3496
|
+
state: r.status === "succeeded" ? "done" : failed2 ? "failed" : "working",
|
|
3497
|
+
...failed2 ? { error: error.code === "timeout" ? "That took too long. Send it again?" : r.status === "cancelled" ? "Stopped." : `I couldn\u2019t answer${error.message ? `: ${clip(error.message, 140)}` : "."}` } : {},
|
|
3499
3498
|
...doing ? { doing } : {}
|
|
3500
3499
|
};
|
|
3501
3500
|
}
|
|
@@ -3522,7 +3521,7 @@ var PROBLEM_TOOLS = ["propose_problems", "add_evidence", "flag_duplicate", "file
|
|
|
3522
3521
|
var READ_WEB_TOOLS = ["web_search", "web_fetch"];
|
|
3523
3522
|
var STORE_TOOLS = ["app_store_search", "play_store_search"];
|
|
3524
3523
|
var MARKET_TOOLS = ["write_market"];
|
|
3525
|
-
var
|
|
3524
|
+
var BUILD_TOOLS = ["build_plan", "build_step"];
|
|
3526
3525
|
var ALL_MEMORY_TOOLS = [
|
|
3527
3526
|
...MEMORY_READ_TOOLS,
|
|
3528
3527
|
...MEMORY_WRITE_TOOLS,
|
|
@@ -3532,7 +3531,7 @@ var ALL_MEMORY_TOOLS = [
|
|
|
3532
3531
|
...PROBLEM_TOOLS,
|
|
3533
3532
|
...READ_WEB_TOOLS,
|
|
3534
3533
|
...MARKET_TOOLS,
|
|
3535
|
-
...
|
|
3534
|
+
...BUILD_TOOLS,
|
|
3536
3535
|
...TALK_TOOLS,
|
|
3537
3536
|
"last_review",
|
|
3538
3537
|
"list_problems",
|
|
@@ -3574,7 +3573,7 @@ function claudeCodeToolFlags(capabilities, opts = {}) {
|
|
|
3574
3573
|
tools.add(t);
|
|
3575
3574
|
allowed.add(`${t}(${pathRule(workspace)})`);
|
|
3576
3575
|
}
|
|
3577
|
-
for (const t of
|
|
3576
|
+
for (const t of BUILD_TOOLS) allowed.add(mcpTool(t));
|
|
3578
3577
|
} else {
|
|
3579
3578
|
for (const t of WRITE_TOOLS) disallowed.add(t);
|
|
3580
3579
|
}
|
|
@@ -3632,7 +3631,7 @@ function claudeCodeToolFlags(capabilities, opts = {}) {
|
|
|
3632
3631
|
allowed.delete(t);
|
|
3633
3632
|
disallowed.add(t);
|
|
3634
3633
|
}
|
|
3635
|
-
return { tools: [...tools], allowedTools: [...allowed], disallowedTools: [...disallowed] };
|
|
3634
|
+
return { tools: [...tools], allowedTools: [...allowed], disallowedTools: [...disallowed], permissionMode: tools.has("Bash") ? "auto" : "default" };
|
|
3636
3635
|
}
|
|
3637
3636
|
|
|
3638
3637
|
// ../daemon/src/adapters/claude-code/sandbox.ts
|
|
@@ -3674,10 +3673,18 @@ function buildSandboxSettings(home) {
|
|
|
3674
3673
|
}
|
|
3675
3674
|
return { permissions: { deny, defaultMode: "default" } };
|
|
3676
3675
|
}
|
|
3677
|
-
function sandboxSettingsFor(home, grantedTools) {
|
|
3676
|
+
function sandboxSettingsFor(home, grantedTools, network = []) {
|
|
3678
3677
|
const settings = buildSandboxSettings(home);
|
|
3679
3678
|
const granted = new Set(grantedTools);
|
|
3680
3679
|
settings.permissions.deny = settings.permissions.deny.filter((rule) => !granted.has(rule));
|
|
3680
|
+
if (granted.has("Bash")) {
|
|
3681
|
+
settings.sandbox = {
|
|
3682
|
+
enabled: true,
|
|
3683
|
+
allowUnsandboxedCommands: false,
|
|
3684
|
+
failIfUnavailable: true,
|
|
3685
|
+
network: { allowedDomains: network.filter((h) => !/(^|\.)github(usercontent)?\.com$/.test(h)) }
|
|
3686
|
+
};
|
|
3687
|
+
}
|
|
3681
3688
|
return settings;
|
|
3682
3689
|
}
|
|
3683
3690
|
|
|
@@ -3818,8 +3825,8 @@ function classifyExit(stderr, aborted) {
|
|
|
3818
3825
|
if (t.includes("not logged in") || t.includes("invalid api key") || t.includes("authentication") || t.includes("subscription") || t.includes("please run /login") || t.includes("oauth")) {
|
|
3819
3826
|
return { code: "auth", message: "Claude Code is not logged in on this machine. Run `claude login` or set ANTHROPIC_API_KEY." };
|
|
3820
3827
|
}
|
|
3821
|
-
const
|
|
3822
|
-
return { code: "harness_error", message:
|
|
3828
|
+
const tail = stderr.trim().split("\n").slice(-3).join(" ").trim();
|
|
3829
|
+
return { code: "harness_error", message: tail || "claude exited without a result" };
|
|
3823
3830
|
}
|
|
3824
3831
|
|
|
3825
3832
|
// ../daemon/src/adapters/claude-code/index.ts
|
|
@@ -3856,10 +3863,10 @@ var ClaudeCodeAdapter = class {
|
|
|
3856
3863
|
const result = parsePrintResult(stdout);
|
|
3857
3864
|
return result?.is_error ? { ok: false, message: result.result ?? "claude reported an error" } : { ok: true, message: `claude answered in ${result?.subtype ?? "success"}` };
|
|
3858
3865
|
} catch (err) {
|
|
3859
|
-
const
|
|
3860
|
-
const reported = parsePrintResult(
|
|
3866
|
+
const failed2 = err;
|
|
3867
|
+
const reported = parsePrintResult(failed2.stdout)?.result;
|
|
3861
3868
|
if (reported) return { ok: false, message: reported };
|
|
3862
|
-
const text2 =
|
|
3869
|
+
const text2 = failed2.stderr?.trim() || failed2.message || String(err);
|
|
3863
3870
|
return { ok: false, message: classifyExit(text2, false).message };
|
|
3864
3871
|
}
|
|
3865
3872
|
}
|
|
@@ -3895,9 +3902,9 @@ var ClaudeCodeAdapter = class {
|
|
|
3895
3902
|
child.stderr.on("data", (chunk) => {
|
|
3896
3903
|
stderr += chunk.toString();
|
|
3897
3904
|
});
|
|
3898
|
-
const exit = new Promise((
|
|
3899
|
-
child.on("close", (code) =>
|
|
3900
|
-
child.on("error", () =>
|
|
3905
|
+
const exit = new Promise((resolve5) => {
|
|
3906
|
+
child.on("close", (code) => resolve5(code));
|
|
3907
|
+
child.on("error", () => resolve5(127));
|
|
3901
3908
|
});
|
|
3902
3909
|
const parser = new StreamParser();
|
|
3903
3910
|
let searched = false;
|
|
@@ -3937,7 +3944,7 @@ var ClaudeCodeAdapter = class {
|
|
|
3937
3944
|
"--append-system-prompt",
|
|
3938
3945
|
spec2.systemPrompt,
|
|
3939
3946
|
"--permission-mode",
|
|
3940
|
-
|
|
3947
|
+
flags.permissionMode,
|
|
3941
3948
|
"--max-turns",
|
|
3942
3949
|
String(spec2.maxTurns),
|
|
3943
3950
|
"--setting-sources",
|
|
@@ -3986,7 +3993,7 @@ var ClaudeCodeAdapter = class {
|
|
|
3986
3993
|
await writeFile2(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 384 });
|
|
3987
3994
|
const settingsPath = join3(spec2.runDir, "settings.json");
|
|
3988
3995
|
const flags = claudeCodeToolFlags(spec2.capabilities, { workspace: spec2.cwd, harnessWeb: spec2.harnessWeb, without: spec2.withoutTools });
|
|
3989
|
-
await writeFile2(settingsPath, JSON.stringify(sandboxSettingsFor(spec2.env.HOME ?? "", flags.tools)), { mode: 384 });
|
|
3996
|
+
await writeFile2(settingsPath, JSON.stringify(sandboxSettingsFor(spec2.env.HOME ?? "", flags.tools, spec2.network)), { mode: 384 });
|
|
3990
3997
|
const child = spawn(this.claudePath, this.buildArgs(spec2, mcpConfigPath, settingsPath), {
|
|
3991
3998
|
cwd: spec2.cwd,
|
|
3992
3999
|
env: spec2.env,
|
|
@@ -4005,9 +4012,9 @@ var ClaudeCodeAdapter = class {
|
|
|
4005
4012
|
stderr += chunk.toString();
|
|
4006
4013
|
if (stderr.length > 64e3) stderr = stderr.slice(-32e3);
|
|
4007
4014
|
});
|
|
4008
|
-
const exit = new Promise((
|
|
4009
|
-
child.on("close", (code, signal) =>
|
|
4010
|
-
child.on("error", () =>
|
|
4015
|
+
const exit = new Promise((resolve5) => {
|
|
4016
|
+
child.on("close", (code, signal) => resolve5({ code, signal }));
|
|
4017
|
+
child.on("error", () => resolve5({ code: 127, signal: null }));
|
|
4011
4018
|
});
|
|
4012
4019
|
const parser = new StreamParser(spec2.outputSchema !== void 0);
|
|
4013
4020
|
try {
|
|
@@ -4042,10 +4049,10 @@ function parsePrintResult(stdout) {
|
|
|
4042
4049
|
}
|
|
4043
4050
|
|
|
4044
4051
|
// ../daemon/src/daemon.ts
|
|
4045
|
-
import { mkdir as
|
|
4052
|
+
import { mkdir as mkdir6, readdir as readdir3, readFile as readFile9, rm as rm5, writeFile as writeFile6 } from "fs/promises";
|
|
4046
4053
|
import { openSync, closeSync, unlinkSync, writeFileSync, readFileSync as readFileSyncNode, mkdirSync } from "fs";
|
|
4047
4054
|
import { hostname as hostname2 } from "os";
|
|
4048
|
-
import { join as
|
|
4055
|
+
import { join as join13 } from "path";
|
|
4049
4056
|
import { setTimeout as sleep } from "timers/promises";
|
|
4050
4057
|
|
|
4051
4058
|
// ../daemon/src/awake.ts
|
|
@@ -4114,8 +4121,8 @@ async function installVersion(version, home = homedir2()) {
|
|
|
4114
4121
|
});
|
|
4115
4122
|
return { ok: true };
|
|
4116
4123
|
} catch (err) {
|
|
4117
|
-
const
|
|
4118
|
-
return { ok: false, why: (
|
|
4124
|
+
const failed2 = err;
|
|
4125
|
+
return { ok: false, why: (failed2.stderr?.trim() || failed2.message || String(err)).split("\n").slice(-3).join(" ") };
|
|
4119
4126
|
}
|
|
4120
4127
|
}
|
|
4121
4128
|
function autoUpdateOff(home = homedir2()) {
|
|
@@ -4177,15 +4184,15 @@ function stripLiterals(sql) {
|
|
|
4177
4184
|
};
|
|
4178
4185
|
while (i < n) {
|
|
4179
4186
|
const c = sql[i];
|
|
4180
|
-
const
|
|
4181
|
-
if (c === "-" &&
|
|
4187
|
+
const next = sql[i + 1];
|
|
4188
|
+
if (c === "-" && next === "-") {
|
|
4182
4189
|
const end = sql.indexOf("\n", i);
|
|
4183
4190
|
const stop2 = end === -1 ? n : end;
|
|
4184
4191
|
blank(i, stop2);
|
|
4185
4192
|
i = stop2;
|
|
4186
4193
|
continue;
|
|
4187
4194
|
}
|
|
4188
|
-
if (c === "/" &&
|
|
4195
|
+
if (c === "/" && next === "*") {
|
|
4189
4196
|
let depth = 1;
|
|
4190
4197
|
let k = i + 2;
|
|
4191
4198
|
while (k < n && depth > 0) {
|
|
@@ -5273,14 +5280,142 @@ async function sendDigestFor(client, accessToken, jobId, fetchImpl = fetch) {
|
|
|
5273
5280
|
}
|
|
5274
5281
|
|
|
5275
5282
|
// ../daemon/src/deploys.ts
|
|
5283
|
+
import { execFile as execFile4 } from "child_process";
|
|
5284
|
+
import { promisify as promisify4 } from "util";
|
|
5285
|
+
|
|
5286
|
+
// ../daemon/src/repo.ts
|
|
5276
5287
|
import { execFile as execFile3 } from "child_process";
|
|
5288
|
+
import { readFile as readFile2, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
5289
|
+
import { join as join5 } from "path";
|
|
5290
|
+
|
|
5291
|
+
// ../shared/src/build.ts
|
|
5292
|
+
var BUILD_SKILL = "build";
|
|
5293
|
+
var BUILD_STEPS_MIN = 1;
|
|
5294
|
+
var BUILD_STEPS_MAX = 8;
|
|
5295
|
+
var BUILD_STEP_MAX = 120;
|
|
5296
|
+
var BUILD_RESUMES_MAX = 3;
|
|
5297
|
+
var BUILD_TRAILER = "Co-Authored-By: NanoPM <noreply@nanopm.app>";
|
|
5298
|
+
var BUILD_COLUMNS = "id, ref, title, status, actor, agent_spec, spec";
|
|
5299
|
+
function buildRefusal(solution, opts = {}) {
|
|
5300
|
+
if (solution.actor === "founder") return `${solution.ref} is yours to do, not Nano's to build.`;
|
|
5301
|
+
if (!["proposed", "accepted", "building"].includes(solution.status)) return `${solution.ref} is ${solution.status}: only a solution on the roadmap can be built.`;
|
|
5302
|
+
if (!solution.agent_spec) return `${solution.ref} has no agent spec yet. Write its specs first: nanopm spec ${solution.ref} --write`;
|
|
5303
|
+
const open = solution.spec?.questions?.length ?? 0;
|
|
5304
|
+
if (open && !opts.anyway) {
|
|
5305
|
+
return `${solution.ref} still has ${open} question${open === 1 ? "" : "s"} for you. Answer ${open === 1 ? "it" : "them"} in its spec, or build anyway: nanopm build ${solution.ref} --anyway`;
|
|
5306
|
+
}
|
|
5307
|
+
return null;
|
|
5308
|
+
}
|
|
5309
|
+
function stepsLeft(plan, done) {
|
|
5310
|
+
return (plan ?? []).map((_, i) => i).filter((i) => !done.has(i));
|
|
5311
|
+
}
|
|
5312
|
+
function buildDir(home, jobId) {
|
|
5313
|
+
return `${home.replace(/\/+$/, "")}/.cache/nanopm/builds/${jobId}`;
|
|
5314
|
+
}
|
|
5315
|
+
function buildBranch(ref, title) {
|
|
5316
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
|
|
5317
|
+
return `nano/${ref.toLowerCase()}${slug ? `-${slug}` : ""}`;
|
|
5318
|
+
}
|
|
5319
|
+
var REGISTRIES = {
|
|
5320
|
+
"package-lock.json": ["registry.npmjs.org"],
|
|
5321
|
+
"npm-shrinkwrap.json": ["registry.npmjs.org"],
|
|
5322
|
+
"pnpm-lock.yaml": ["registry.npmjs.org"],
|
|
5323
|
+
"yarn.lock": ["registry.yarnpkg.com", "registry.npmjs.org"],
|
|
5324
|
+
"bun.lockb": ["registry.npmjs.org"],
|
|
5325
|
+
"bun.lock": ["registry.npmjs.org"],
|
|
5326
|
+
"poetry.lock": ["pypi.org", "files.pythonhosted.org"],
|
|
5327
|
+
"uv.lock": ["pypi.org", "files.pythonhosted.org"],
|
|
5328
|
+
"Pipfile.lock": ["pypi.org", "files.pythonhosted.org"],
|
|
5329
|
+
"requirements.txt": ["pypi.org", "files.pythonhosted.org"],
|
|
5330
|
+
"Cargo.lock": ["crates.io", "index.crates.io", "static.crates.io"],
|
|
5331
|
+
"Gemfile.lock": ["rubygems.org", "index.rubygems.org"],
|
|
5332
|
+
"go.sum": ["proxy.golang.org", "sum.golang.org"],
|
|
5333
|
+
"composer.lock": ["repo.packagist.org"]
|
|
5334
|
+
};
|
|
5335
|
+
function buildNetwork(rootFiles) {
|
|
5336
|
+
return [...new Set(rootFiles.flatMap((f) => REGISTRIES[f] ?? []))].sort();
|
|
5337
|
+
}
|
|
5338
|
+
async function queueBuild(client, ask7) {
|
|
5339
|
+
const { data, error: readError } = await client.from("solutions").select(BUILD_COLUMNS).eq("project_id", ask7.projectId).eq("ref", ask7.ref).is("superseded_by", null).maybeSingle();
|
|
5340
|
+
if (readError) throw new Error(`could not read the solution: ${pgMessage(readError)}`);
|
|
5341
|
+
if (!data) throw new Error(`No solution ${ask7.ref} on the Roadmap.`);
|
|
5342
|
+
const solution = data;
|
|
5343
|
+
const refused = buildRefusal(solution, { anyway: ask7.anyway });
|
|
5344
|
+
if (refused) throw new Error(refused);
|
|
5345
|
+
const { data: job, error } = await client.from("jobs").insert({
|
|
5346
|
+
project_id: ask7.projectId,
|
|
5347
|
+
skill: BUILD_SKILL,
|
|
5348
|
+
requested_by: ask7.userId,
|
|
5349
|
+
trigger: "user",
|
|
5350
|
+
target_daemon_id: ask7.daemonId,
|
|
5351
|
+
params: { solution_id: solution.id, repo_path: ask7.repoPath, ...ask7.anyway ? { anyway: true } : {} }
|
|
5352
|
+
}).select("id").single();
|
|
5353
|
+
if (error?.code === "23505") throw new Error("A build of this product is already running. One at a time: `nanopm status` says where it is.");
|
|
5354
|
+
if (error || !job) throw new Error(`could not queue the build: ${error ? pgMessage(error) : "no row"}`);
|
|
5355
|
+
return { jobId: job.id, solution };
|
|
5356
|
+
}
|
|
5357
|
+
|
|
5358
|
+
// ../daemon/src/repo.ts
|
|
5277
5359
|
import { promisify as promisify3 } from "util";
|
|
5278
5360
|
var execFileAsync3 = promisify3(execFile3);
|
|
5279
|
-
var
|
|
5280
|
-
var
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5361
|
+
var MAX_BUFFER = 4 * 1024 * 1024;
|
|
5362
|
+
var TIMEOUT_MS = 6e4;
|
|
5363
|
+
var LOCKED = {
|
|
5364
|
+
GIT_CONFIG_COUNT: "2",
|
|
5365
|
+
GIT_CONFIG_KEY_0: "core.hooksPath",
|
|
5366
|
+
GIT_CONFIG_VALUE_0: "/dev/null",
|
|
5367
|
+
GIT_CONFIG_KEY_1: "core.fsmonitor",
|
|
5368
|
+
GIT_CONFIG_VALUE_1: "false"
|
|
5369
|
+
};
|
|
5370
|
+
async function exec(cmd, repo, args, timeoutMs = TIMEOUT_MS) {
|
|
5371
|
+
if (typeof repo === "string") return execFileAsync3(cmd, cmd === "git" ? ["-C", repo, ...args] : args, { cwd: repo, maxBuffer: MAX_BUFFER, timeout: timeoutMs });
|
|
5372
|
+
await writeFile4(join5(repo.dir, ".git"), `gitdir: ${repo.admin}
|
|
5373
|
+
`);
|
|
5374
|
+
await writeFile4(join5(repo.admin, "commondir"), repo.commondir);
|
|
5375
|
+
await rm3(join5(repo.admin, "config.worktree"), { force: true });
|
|
5376
|
+
return execFileAsync3(cmd, args, { cwd: repo.dir, env: { ...process.env, GIT_DIR: repo.admin, GIT_WORK_TREE: repo.dir, ...LOCKED }, maxBuffer: MAX_BUFFER, timeout: timeoutMs });
|
|
5377
|
+
}
|
|
5378
|
+
async function git(repo, args, timeoutMs = TIMEOUT_MS) {
|
|
5379
|
+
return (await exec("git", repo, args, timeoutMs)).stdout.trim();
|
|
5380
|
+
}
|
|
5381
|
+
var RepoError = class extends Error {
|
|
5382
|
+
};
|
|
5383
|
+
async function commitAll(cwd, message, note) {
|
|
5384
|
+
await git(cwd, ["add", "-A"]);
|
|
5385
|
+
const staged = await git(cwd, ["diff", "--cached", "--name-only"]).catch(() => "");
|
|
5386
|
+
if (!staged) return null;
|
|
5387
|
+
await git(cwd, ["commit", "--no-verify", "-m", message, ...note ? ["-m", note] : [], "-m", `Written by NanoPM. Review before merging.
|
|
5388
|
+
|
|
5389
|
+
${BUILD_TRAILER}`]);
|
|
5390
|
+
const sha = await git(cwd, ["rev-parse", "--short", "HEAD"]);
|
|
5391
|
+
return { sha, ...await diffStat(cwd) };
|
|
5392
|
+
}
|
|
5393
|
+
async function diffStat(cwd) {
|
|
5394
|
+
const numstat = await git(cwd, ["show", "--numstat", "--format=", "HEAD"]).catch(() => "");
|
|
5395
|
+
let files = 0;
|
|
5396
|
+
let insertions = 0;
|
|
5397
|
+
let deletions = 0;
|
|
5398
|
+
for (const line of numstat.split("\n").filter(Boolean)) {
|
|
5399
|
+
const [add2 = "", del = ""] = line.split(" ");
|
|
5400
|
+
files += 1;
|
|
5401
|
+
insertions += Number(add2) || 0;
|
|
5402
|
+
deletions += Number(del) || 0;
|
|
5403
|
+
}
|
|
5404
|
+
return { files, insertions, deletions };
|
|
5405
|
+
}
|
|
5406
|
+
async function pushBranch(cwd, name) {
|
|
5407
|
+
if (!await originUrl(cwd)) return { pushed: false, reason: "this repository has no origin remote" };
|
|
5408
|
+
try {
|
|
5409
|
+
const { stderr } = await exec("git", cwd, ["push", "--no-verify", "--set-upstream", "origin", name], 12e4);
|
|
5410
|
+
return { pushed: true, reviewUrl: reviewUrlFrom(stderr) };
|
|
5411
|
+
} catch (err) {
|
|
5412
|
+
const text2 = errorMessage(err);
|
|
5413
|
+
return { pushed: false, reason: text2.split("\n").slice(-3).join(" ").trim() || "git push failed" };
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
function reviewUrlFrom(pushOutput) {
|
|
5417
|
+
const match = /https?:\/\/\S+/.exec(pushOutput);
|
|
5418
|
+
return match?.[0]?.replace(/[).,]+$/, "");
|
|
5284
5419
|
}
|
|
5285
5420
|
async function defaultBranch(cwd) {
|
|
5286
5421
|
try {
|
|
@@ -5290,9 +5425,78 @@ async function defaultBranch(cwd) {
|
|
|
5290
5425
|
return git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "HEAD");
|
|
5291
5426
|
}
|
|
5292
5427
|
}
|
|
5428
|
+
async function originUrl(cwd) {
|
|
5429
|
+
return git(cwd, ["remote", "get-url", "origin"]).catch(() => "");
|
|
5430
|
+
}
|
|
5431
|
+
async function baseRef(cwd, branch) {
|
|
5432
|
+
if (!await originUrl(cwd)) return branch;
|
|
5433
|
+
await git(cwd, ["fetch", "--quiet", "origin", branch], 12e4).catch(() => "");
|
|
5434
|
+
return await git(cwd, ["rev-parse", "--verify", "--quiet", `origin/${branch}`]).catch(() => "") ? `origin/${branch}` : branch;
|
|
5435
|
+
}
|
|
5436
|
+
async function addWorktree(clone, dir, branch) {
|
|
5437
|
+
if (!await git(clone, ["rev-parse", "HEAD"]).catch(() => "")) throw new RepoError("this repository has no commits yet; make the first commit, then build");
|
|
5438
|
+
const existing = await git(clone, ["branch", "--list", branch]).catch(() => "");
|
|
5439
|
+
if (existing.trim()) throw new RepoError(`branch ${branch} already exists. Delete it or finish what is on it first.`);
|
|
5440
|
+
const base = await defaultBranch(clone);
|
|
5441
|
+
const onto = await baseRef(clone, base);
|
|
5442
|
+
await git(clone, ["worktree", "add", "-b", branch, dir, onto]);
|
|
5443
|
+
const admin = await git(dir, ["rev-parse", "--absolute-git-dir"]);
|
|
5444
|
+
const worktree = { dir, admin, commondir: await readFile2(join5(admin, "commondir"), "utf8") };
|
|
5445
|
+
return { base, fork: await git(worktree, ["rev-parse", "HEAD"]), worktree };
|
|
5446
|
+
}
|
|
5447
|
+
async function removeWorktree(clone, dir) {
|
|
5448
|
+
await git(clone, ["worktree", "remove", "--force", dir]).catch(() => "");
|
|
5449
|
+
}
|
|
5450
|
+
async function commonGitDir(cwd) {
|
|
5451
|
+
return git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
5452
|
+
}
|
|
5453
|
+
async function commitsSinceFork(cwd, fork) {
|
|
5454
|
+
return Number(await git(cwd, ["rev-list", "--count", `${fork}..HEAD`]).catch(() => "0")) || 0;
|
|
5455
|
+
}
|
|
5456
|
+
async function rebaseOnto(cwd, base) {
|
|
5457
|
+
const onto = await baseRef(cwd, base);
|
|
5458
|
+
try {
|
|
5459
|
+
await git(cwd, ["rebase", "--no-verify", onto], 12e4);
|
|
5460
|
+
return { ok: true };
|
|
5461
|
+
} catch {
|
|
5462
|
+
const files = (await git(cwd, ["diff", "--name-only", "--diff-filter=U"]).catch(() => "")).split("\n").filter(Boolean);
|
|
5463
|
+
await git(cwd, ["rebase", "--abort"]).catch(() => "");
|
|
5464
|
+
return { ok: false, files };
|
|
5465
|
+
}
|
|
5466
|
+
}
|
|
5467
|
+
async function gh(cwd, args) {
|
|
5468
|
+
return (await exec("gh", cwd, args, 12e4)).stdout.trim();
|
|
5469
|
+
}
|
|
5470
|
+
async function ghReady(cwd) {
|
|
5471
|
+
if (!/github\.com[:/]/.test(await originUrl(cwd))) return "this repository has no GitHub origin, so there is nowhere to open a pull request";
|
|
5472
|
+
try {
|
|
5473
|
+
await gh(cwd, ["auth", "status"]);
|
|
5474
|
+
return null;
|
|
5475
|
+
} catch (err) {
|
|
5476
|
+
return /ENOENT/.test(errorMessage(err)) ? "the GitHub CLI is not installed on this machine: brew install gh, then gh auth login" : "the GitHub CLI is not signed in on this machine: run gh auth login in your terminal";
|
|
5477
|
+
}
|
|
5478
|
+
}
|
|
5479
|
+
async function openPullRequest(cwd, pr) {
|
|
5480
|
+
try {
|
|
5481
|
+
const out = await gh(cwd, ["pr", "create", "--draft", "--head", pr.branch, "--base", pr.base, "--title", pr.title, "--body", pr.body]);
|
|
5482
|
+
const url = reviewUrlFrom(out) ?? out.split("\n").at(-1) ?? "";
|
|
5483
|
+
return { ok: true, url, number: Number(/\/pull\/(\d+)/.exec(url)?.[1] ?? 0) };
|
|
5484
|
+
} catch (err) {
|
|
5485
|
+
return { ok: false, reason: errorMessage(err).split("\n").filter(Boolean).slice(-2).join(" ") || "gh pr create failed" };
|
|
5486
|
+
}
|
|
5487
|
+
}
|
|
5488
|
+
|
|
5489
|
+
// ../daemon/src/deploys.ts
|
|
5490
|
+
var execFileAsync4 = promisify4(execFile4);
|
|
5491
|
+
var SETTLE_HOURS = 3;
|
|
5492
|
+
var MIN_COMMITS = 3;
|
|
5493
|
+
async function git2(cwd, args) {
|
|
5494
|
+
const { stdout } = await execFileAsync4("git", ["-C", cwd, ...args], { timeout: 2e4, maxBuffer: 1024 * 1024 });
|
|
5495
|
+
return stdout.trim();
|
|
5496
|
+
}
|
|
5293
5497
|
async function commitsSince(cwd, since) {
|
|
5294
5498
|
const branch = await defaultBranch(cwd);
|
|
5295
|
-
const out = await
|
|
5499
|
+
const out = await git2(cwd, ["log", branch, `--since=${since.toISOString()}`, "--format=%cI"]);
|
|
5296
5500
|
const stamps = out.split("\n").filter(Boolean);
|
|
5297
5501
|
return { count: stamps.length, newestAt: stamps[0] ? new Date(stamps[0]) : null };
|
|
5298
5502
|
}
|
|
@@ -5376,16 +5580,16 @@ async function reviewWithoutData(client, job) {
|
|
|
5376
5580
|
|
|
5377
5581
|
// ../daemon/src/skills.ts
|
|
5378
5582
|
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
5379
|
-
import { readFile as
|
|
5380
|
-
import { dirname as dirname2, join as
|
|
5583
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
5584
|
+
import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
|
|
5381
5585
|
function findSkillsDir(env = process.env) {
|
|
5382
5586
|
if (env.NANOPM_SKILLS_DIR) return env.NANOPM_SKILLS_DIR;
|
|
5383
5587
|
const candidates = [
|
|
5384
|
-
|
|
5588
|
+
join6(process.cwd(), "skills"),
|
|
5385
5589
|
...process.argv[1] ? [resolve2(dirname2(process.argv[1]), "..", "skills")] : []
|
|
5386
5590
|
];
|
|
5387
5591
|
for (const dir of candidates) {
|
|
5388
|
-
if (existsSync2(
|
|
5592
|
+
if (existsSync2(join6(dir, "onboard", "skill.json"))) return dir;
|
|
5389
5593
|
}
|
|
5390
5594
|
throw new Error(`Cannot find the skills directory (tried ${candidates.join(", ")}). Set NANOPM_SKILLS_DIR.`);
|
|
5391
5595
|
}
|
|
@@ -5395,9 +5599,9 @@ function skillDirs(skillsDir, env = process.env) {
|
|
|
5395
5599
|
}
|
|
5396
5600
|
async function loadSkill(name, skillsDir = findSkillsDir()) {
|
|
5397
5601
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new Error(`Invalid skill name "${name}"`);
|
|
5398
|
-
const dir = skillDirs(skillsDir).map((d) =>
|
|
5602
|
+
const dir = skillDirs(skillsDir).map((d) => join6(d, name)).find((d) => existsSync2(join6(d, "skill.json")));
|
|
5399
5603
|
if (!dir) throw new Error(`Unknown skill "${name}"`);
|
|
5400
|
-
const raw = JSON.parse(await
|
|
5604
|
+
const raw = JSON.parse(await readFile3(join6(dir, "skill.json"), "utf8"));
|
|
5401
5605
|
let manifest;
|
|
5402
5606
|
try {
|
|
5403
5607
|
manifest = parseSkillManifest(raw);
|
|
@@ -5406,7 +5610,7 @@ async function loadSkill(name, skillsDir = findSkillsDir()) {
|
|
|
5406
5610
|
}
|
|
5407
5611
|
const tainted = taintedByTheWeb(manifest);
|
|
5408
5612
|
if (tainted) throw new Error(`the "${name}" skill cannot be loaded: ${tainted}`);
|
|
5409
|
-
const body = await
|
|
5613
|
+
const body = await readFile3(join6(dir, "SKILL.md"), "utf8");
|
|
5410
5614
|
return { manifest, body, dir };
|
|
5411
5615
|
}
|
|
5412
5616
|
var NOT_WITH_READ_WEB = ["write_memory", "problems", "propose", "review", "write_repo", "run_commands"];
|
|
@@ -5441,7 +5645,7 @@ function renderPrompt(template, params) {
|
|
|
5441
5645
|
var NEEDS_THE_REQUESTERS_CLONE = ["write_repo", "run_commands"];
|
|
5442
5646
|
var NEEDS_THE_REQUESTERS_IDENTITY = /* @__PURE__ */ new Set(["want", "start"]);
|
|
5443
5647
|
async function clonelessSkills(skillsDir = findSkillsDir()) {
|
|
5444
|
-
const names = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync2(
|
|
5648
|
+
const names = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync2(join6(skillsDir, d.name, "skill.json"))).map((d) => d.name);
|
|
5445
5649
|
const out = [];
|
|
5446
5650
|
for (const name of names) {
|
|
5447
5651
|
try {
|
|
@@ -5568,9 +5772,9 @@ async function runProblemMapping(ports) {
|
|
|
5568
5772
|
if (cycle === 1 && ports.collect) {
|
|
5569
5773
|
const maxTurns = Math.min(8, MAPPING_TURN_LIMIT - (usage?.turns ?? 0) - 2);
|
|
5570
5774
|
await ports.checkpoint("collector.started", { maxTurns: Math.max(0, maxTurns), startedAt: Date.now() });
|
|
5571
|
-
let
|
|
5775
|
+
let next = null;
|
|
5572
5776
|
try {
|
|
5573
|
-
|
|
5777
|
+
next = await ports.collect(proposal.investigation, signal, maxTurns);
|
|
5574
5778
|
} catch (err) {
|
|
5575
5779
|
signal.throwIfAborted();
|
|
5576
5780
|
failure2 = errorMessage(err);
|
|
@@ -5578,13 +5782,13 @@ async function runProblemMapping(ports) {
|
|
|
5578
5782
|
usage = sumUsage(usage, spent);
|
|
5579
5783
|
await ports.checkpoint("collector", { usage: spent, failed: failure2 });
|
|
5580
5784
|
}
|
|
5581
|
-
if (!
|
|
5785
|
+
if (!next && !failure2) await ports.checkpoint("collector", { unavailable: true });
|
|
5582
5786
|
signal.throwIfAborted();
|
|
5583
|
-
if (
|
|
5584
|
-
if (
|
|
5585
|
-
await ports.checkpoint("collector", { usage:
|
|
5787
|
+
if (next) {
|
|
5788
|
+
if (next.usage.turns > 0) usage = sumUsage(usage, next.usage);
|
|
5789
|
+
await ports.checkpoint("collector", { usage: next.usage });
|
|
5586
5790
|
if ((usage?.turns ?? 0) >= MAPPING_TURN_LIMIT) throw new Error("Collection exhausted the shared turn budget.");
|
|
5587
|
-
snapshot3 = copySnapshot(
|
|
5791
|
+
snapshot3 = copySnapshot(next.snapshot);
|
|
5588
5792
|
feedback = { previous_proposal: proposal, instruction: proposal.investigation.collector === "sources" ? "The seen sources you named are now in the snapshot in full. Reconsider the proposal with them, then return the final version." : "Reconsider the proposal with the collected findings, then return the final version." };
|
|
5589
5793
|
await ports.checkpoint("collection", { snapshot_hash: mappingHash(snapshot3), request: proposal.investigation });
|
|
5590
5794
|
continue;
|
|
@@ -5770,8 +5974,8 @@ async function askAgainIfUnanswered(attempt7, say, role7) {
|
|
|
5770
5974
|
var UNANSWERED = /structured[ _]output|returned no result/i;
|
|
5771
5975
|
|
|
5772
5976
|
// ../daemon/src/site.ts
|
|
5773
|
-
import { readFile as
|
|
5774
|
-
import { join as
|
|
5977
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
5978
|
+
import { join as join7 } from "path";
|
|
5775
5979
|
var SITE_PATHS = ["/", "/about", "/pricing"];
|
|
5776
5980
|
var PAGE_CHARS = 6e3;
|
|
5777
5981
|
var SITE_TIMEOUT_MS = 2e4;
|
|
@@ -5894,7 +6098,7 @@ function siteFromSearch(name, hits) {
|
|
|
5894
6098
|
}
|
|
5895
6099
|
async function read(repoRoot, name) {
|
|
5896
6100
|
try {
|
|
5897
|
-
const text2 = await
|
|
6101
|
+
const text2 = await readFile4(join7(repoRoot, name), "utf8");
|
|
5898
6102
|
return text2.length > 2e5 ? null : text2;
|
|
5899
6103
|
} catch {
|
|
5900
6104
|
return null;
|
|
@@ -5948,8 +6152,8 @@ async function fromReadme(repoRoot) {
|
|
|
5948
6152
|
}
|
|
5949
6153
|
|
|
5950
6154
|
// ../daemon/src/repo-docs.ts
|
|
5951
|
-
import { readFile as
|
|
5952
|
-
import { join as
|
|
6155
|
+
import { readFile as readFile5, readdir, stat } from "fs/promises";
|
|
6156
|
+
import { join as join8, relative } from "path";
|
|
5953
6157
|
var WRITTEN = /(persona|audience|customer|user-research|research\/|icp|segment)/i;
|
|
5954
6158
|
var ABOUT_PEOPLE = /(persona|audience|customer|user-research|research\/|interview|feedback|survey|verbatim|voice|icp|segment)/i;
|
|
5955
6159
|
var DOC = /\.(md|mdx|txt)$/i;
|
|
@@ -5967,7 +6171,7 @@ async function readDocs(cwd, opts = {}) {
|
|
|
5967
6171
|
return;
|
|
5968
6172
|
}
|
|
5969
6173
|
for (const entry of entries) {
|
|
5970
|
-
const full =
|
|
6174
|
+
const full = join8(dir, entry.name);
|
|
5971
6175
|
if (SKIP.test(`/${relative(cwd, full)}`)) continue;
|
|
5972
6176
|
if (entry.isDirectory()) await walk(full, depth + 1);
|
|
5973
6177
|
else if (DOC.test(entry.name) && (!opts.onlyAboutPeople || ABOUT_PEOPLE.test(relative(cwd, full)))) found.push(full);
|
|
@@ -5979,7 +6183,7 @@ async function readDocs(cwd, opts = {}) {
|
|
|
5979
6183
|
for (const path of found.slice(0, DOCS_MAX)) {
|
|
5980
6184
|
try {
|
|
5981
6185
|
if ((await stat(path)).size > 4e5) continue;
|
|
5982
|
-
out.push({ path: relative(cwd, path), text: (await
|
|
6186
|
+
out.push({ path: relative(cwd, path), text: (await readFile5(path, "utf8")).slice(0, DOC_CHARS) });
|
|
5983
6187
|
} catch {
|
|
5984
6188
|
}
|
|
5985
6189
|
}
|
|
@@ -6451,10 +6655,10 @@ function rank(candidates, readings, verdict, faults) {
|
|
|
6451
6655
|
const order = (lands) => DIRECTION_SHAPE_ORDER.indexOf(lands);
|
|
6452
6656
|
return candidates.filter((c) => readings.some((r) => r.lands === c.lands)).map((candidate) => {
|
|
6453
6657
|
const v = verdict.verdicts.find((x) => x.lands === candidate.lands);
|
|
6454
|
-
const
|
|
6455
|
-
const fatal =
|
|
6658
|
+
const failed2 = v?.failed ?? [];
|
|
6659
|
+
const fatal = failed2.filter((t) => MUST_PASS_TESTS.includes(t.test));
|
|
6456
6660
|
const gates = faults.get(candidate.lands) ?? [];
|
|
6457
|
-
const answered = /* @__PURE__ */ new Set([...v?.passed ?? [], ...
|
|
6661
|
+
const answered = /* @__PURE__ */ new Set([...v?.passed ?? [], ...failed2.map((t) => t.test)]);
|
|
6458
6662
|
const unanswered = MUST_PASS_TESTS.filter((t) => !answered.has(t));
|
|
6459
6663
|
const reservations = [
|
|
6460
6664
|
...fatal.map((t) => `${t.test} \u2014 ${t.because}`),
|
|
@@ -6527,17 +6731,17 @@ async function publish2(client, projectId, job, winner, current, because, confid
|
|
|
6527
6731
|
for (const row of DIRECTION_ROWS) {
|
|
6528
6732
|
const content = winner[row].trim();
|
|
6529
6733
|
const base = current.find((r) => r.topic === row && r.key === directionKey(row));
|
|
6530
|
-
const
|
|
6734
|
+
const next = current.find((r) => r.topic === row && r.key === directionKey(row, true));
|
|
6531
6735
|
const sources2 = [
|
|
6532
6736
|
...winner.restsOn.map((ref) => ({ kind: "memory", ref, note: because })),
|
|
6533
6737
|
...runnerUp && runnerUp[row]?.trim() && runnerUp[row].trim() !== content ? [{ kind: CONSIDERED, ref: runnerUp.lands, note: runnerUp[row].trim() }] : []
|
|
6534
6738
|
];
|
|
6535
6739
|
if (base?.provenance === "stated") {
|
|
6536
6740
|
if (base.content.trim() === content) {
|
|
6537
|
-
if (
|
|
6741
|
+
if (next) await retire(client, projectId, next.id, "withdrawn: the crew now agrees with your line");
|
|
6538
6742
|
continue;
|
|
6539
6743
|
}
|
|
6540
|
-
await write2(client, projectId, jobId, row, { key: directionKey(row, true), content, confidence, sources: sources2, supersedes:
|
|
6744
|
+
await write2(client, projectId, jobId, row, { key: directionKey(row, true), content, confidence, sources: sources2, supersedes: next?.id ?? null });
|
|
6541
6745
|
proposed.push(row);
|
|
6542
6746
|
} else {
|
|
6543
6747
|
if (base && base.content.trim() === content) continue;
|
|
@@ -6758,17 +6962,17 @@ async function publish3(client, projectId, job, winner, current, because, runner
|
|
|
6758
6962
|
for (const line of PITCH_LINE_KEYS) {
|
|
6759
6963
|
const content = winner[line].trim();
|
|
6760
6964
|
const base = current.find((r) => r.key === pitchKey(line));
|
|
6761
|
-
const
|
|
6965
|
+
const next = current.find((r) => r.key === pitchKey(line, true));
|
|
6762
6966
|
const sources2 = [
|
|
6763
6967
|
...winner.restsOn.map((ref) => ({ kind: "memory", ref, note: because })),
|
|
6764
6968
|
...runnerUp && !sameLine(runnerUp[line], content) ? [{ kind: CONSIDERED, ref: runnerUp.form, note: runnerUp[line].trim() }] : []
|
|
6765
6969
|
];
|
|
6766
6970
|
if (base?.provenance === "stated") {
|
|
6767
6971
|
if (sameLine(base.content, content)) {
|
|
6768
|
-
if (
|
|
6972
|
+
if (next) await retire2(client, projectId, next.id, "withdrawn: the crew now agrees with your line");
|
|
6769
6973
|
continue;
|
|
6770
6974
|
}
|
|
6771
|
-
await write3(client, projectId, jobId, { key: pitchKey(line, true), title: PITCH_LINE_TITLE[line], content, confidence: winner.confidence, sources: sources2, supersedes:
|
|
6975
|
+
await write3(client, projectId, jobId, { key: pitchKey(line, true), title: PITCH_LINE_TITLE[line], content, confidence: winner.confidence, sources: sources2, supersedes: next?.id ?? null });
|
|
6772
6976
|
proposed.push(line);
|
|
6773
6977
|
} else {
|
|
6774
6978
|
if (base && sameLine(base.content, content)) continue;
|
|
@@ -7023,10 +7227,10 @@ async function runMarketJob({ client, job, adapter, spec: spec2, onEvent }) {
|
|
|
7023
7227
|
}
|
|
7024
7228
|
async function writeRow(client, projectId, job, rows2, item) {
|
|
7025
7229
|
const base = rows2.find((r) => r.key === item.key);
|
|
7026
|
-
const
|
|
7230
|
+
const next = rows2.find((r) => r.key === `${item.key}-next`);
|
|
7027
7231
|
if (base && base.content.trim() === item.content) return { kind: "written" };
|
|
7028
7232
|
if (base?.provenance === "stated") {
|
|
7029
|
-
await write4(client, projectId, job.id, { ...item, key: `${item.key}-next`, supersedes:
|
|
7233
|
+
await write4(client, projectId, job.id, { ...item, key: `${item.key}-next`, supersedes: next?.id ?? null });
|
|
7030
7234
|
return { kind: "written" };
|
|
7031
7235
|
}
|
|
7032
7236
|
const held = keepsTheLine(job, base, item.confidence);
|
|
@@ -7782,7 +7986,7 @@ ${body}
|
|
|
7782
7986
|
// ../daemon/src/crew.ts
|
|
7783
7987
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
7784
7988
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
7785
|
-
import { join as
|
|
7989
|
+
import { join as join9 } from "path";
|
|
7786
7990
|
var READ_ONLY = {
|
|
7787
7991
|
read_repo: /^(Read|Glob|Grep)$/,
|
|
7788
7992
|
read_data: /(^|__)(list_connections|describe_source|query)$/
|
|
@@ -7797,7 +8001,7 @@ async function attempt6({ adapter, spec: spec2, onEvent, skill: skillName, name,
|
|
|
7797
8001
|
if (skill.manifest.capabilities.some((c) => !holds.includes(c))) throw new Error(`The ${name} may not hold tools.`);
|
|
7798
8002
|
const refused = grant.find((c) => !READ_ONLY[c] || !skill.manifest.capabilities.includes(c));
|
|
7799
8003
|
if (refused) throw new Error(`The ${name} may not hold ${refused}.`);
|
|
7800
|
-
const runDir = dir ?
|
|
8004
|
+
const runDir = dir ? join9(spec2.runDir, dir) : spec2.runDir;
|
|
7801
8005
|
if (dir) await mkdir2(runDir, { recursive: true, mode: 448 });
|
|
7802
8006
|
let result;
|
|
7803
8007
|
let usage = null;
|
|
@@ -8595,7 +8799,7 @@ function critiqueWording(o, v, reworked) {
|
|
|
8595
8799
|
return { problem: problem && problem.length <= PROBLEM_MAX ? problem : o.problem, description: v.description?.trim() || o.description };
|
|
8596
8800
|
}
|
|
8597
8801
|
function shape(o, input) {
|
|
8598
|
-
const
|
|
8802
|
+
const resolve5 = (ids, stance) => ids.map((id) => {
|
|
8599
8803
|
const source = input.cites.get(id.trim().toUpperCase());
|
|
8600
8804
|
if (!source) input.unresolved?.add(id.trim());
|
|
8601
8805
|
return source;
|
|
@@ -8607,7 +8811,7 @@ function shape(o, input) {
|
|
|
8607
8811
|
if (source) quoted.push({ ...source, note: clip3(stripContactDetails(e.quote), 140), stance: "supports" });
|
|
8608
8812
|
}
|
|
8609
8813
|
const quotedRefs = new Set(quoted.map((q) => q.ref));
|
|
8610
|
-
const cited = [...
|
|
8814
|
+
const cited = [...resolve5(o.restsOn, "supports"), ...resolve5(o.against.filter((id) => !supporting.has(id.trim().toUpperCase())), "contradicts")].filter((c) => !(c.stance === "supports" && quotedRefs.has(c.ref)));
|
|
8611
8815
|
const all = [...quoted, ...cited];
|
|
8612
8816
|
let sources2 = all.filter((c, i) => all.findIndex((x) => x.kind === c.kind && x.ref === c.ref && x.stance === c.stance && x.note === c.note) === i);
|
|
8613
8817
|
if (!sources2.some((s) => s.stance === "supports")) sources2 = [{ kind: "inference", ref: "nanopm", note: null, stance: "supports" }, ...sources2];
|
|
@@ -8897,11 +9101,11 @@ async function runSolutionsJob({ client, job, adapter, spec: spec2, onEvent }) {
|
|
|
8897
9101
|
await journal("solutions.citations", `${ids.length} citation${ids.length === 1 ? "" : "s"} named nothing the crew was handed, and ${ids.length === 1 ? "was" : "were"} left out: ${ids.slice(0, 12).join(", ")}.`);
|
|
8898
9102
|
}
|
|
8899
9103
|
const recorded = results.filter((r) => "kept" in r);
|
|
8900
|
-
const
|
|
9104
|
+
const failed2 = results.filter((r) => "failed" in r);
|
|
8901
9105
|
const took = `Took ${duration(Date.now() - started)}${timings.length ? `: ${timings.map((t) => `${t.what} ${duration(t.ms)}`).join(", ")}` : ""}.`;
|
|
8902
9106
|
const summary = [
|
|
8903
|
-
deep ? leaves.length ? recorded.length ? `Deep dive on \u201C${clip3(leaves[0].problem, 120)}\u201D: kept ${recorded[0].kept.map((t) => `\u201C${t}\u201D`).join(", ")}.` : `Deep dive on \u201C${clip3(leaves[0].problem, 120)}\u201D: nothing recorded \u2014 ${
|
|
8904
|
-
!deep &&
|
|
9107
|
+
deep ? leaves.length ? recorded.length ? `Deep dive on \u201C${clip3(leaves[0].problem, 120)}\u201D: kept ${recorded[0].kept.map((t) => `\u201C${t}\u201D`).join(", ")}.` : `Deep dive on \u201C${clip3(leaves[0].problem, 120)}\u201D: nothing recorded \u2014 ${failed2[0]?.failed ?? "no solution came back"}.` : "Deep dive: nothing to think through." : `Flash: solutions for ${recorded.length} of ${leaves.length} problem${leaves.length === 1 ? "" : "s"} in ${branches.length} branch${branches.length === 1 ? "" : "es"}.`,
|
|
9108
|
+
!deep && failed2.length ? `Not recorded: ${failed2.map((f) => `${f.leaf.ref} (${f.failed})`).join("; ")}.` : "",
|
|
8905
9109
|
skipped.length ? `Skipped: ${skipped.join("; ")}.` : "",
|
|
8906
9110
|
took
|
|
8907
9111
|
].filter(Boolean).join(" ");
|
|
@@ -9071,9 +9275,9 @@ async function flashBranch(ctx, w, n, places) {
|
|
|
9071
9275
|
const why = clip3(error instanceof Error ? error.message : String(error), 300);
|
|
9072
9276
|
for (const leaf of w.leaves) if (!out.some((o) => o.leaf === leaf)) out.push({ leaf, failed: why });
|
|
9073
9277
|
}
|
|
9074
|
-
const
|
|
9075
|
-
if (
|
|
9076
|
-
await ctx.journal("solutions.failed", `\u201C${name}\u201D could not finish: ${
|
|
9278
|
+
const failed2 = out.filter((o) => "failed" in o);
|
|
9279
|
+
if (failed2.length) {
|
|
9280
|
+
await ctx.journal("solutions.failed", `\u201C${name}\u201D could not finish: ${failed2.map((f) => `${f.leaf.ref} \u2014 ${f.failed}`).join("; ")}. The other branches were not held.`);
|
|
9077
9281
|
}
|
|
9078
9282
|
return out;
|
|
9079
9283
|
}
|
|
@@ -9438,32 +9642,32 @@ function specOutputSchema(role7) {
|
|
|
9438
9642
|
}
|
|
9439
9643
|
|
|
9440
9644
|
// ../daemon/src/git.ts
|
|
9441
|
-
import { execFile as
|
|
9442
|
-
import { promisify as
|
|
9443
|
-
var
|
|
9444
|
-
var
|
|
9645
|
+
import { execFile as execFile5 } from "child_process";
|
|
9646
|
+
import { promisify as promisify5 } from "util";
|
|
9647
|
+
var execFileAsync5 = promisify5(execFile5);
|
|
9648
|
+
var MAX_BUFFER2 = 4 * 1024 * 1024;
|
|
9445
9649
|
var WIKI_DIR = ".nanopm/wiki/docs";
|
|
9446
|
-
async function
|
|
9447
|
-
const { stdout } = await
|
|
9650
|
+
async function git3(cwd, args) {
|
|
9651
|
+
const { stdout } = await execFileAsync5("git", ["-C", cwd, ...args], { maxBuffer: MAX_BUFFER2, timeout: 2e4 });
|
|
9448
9652
|
return stdout.trim();
|
|
9449
9653
|
}
|
|
9450
9654
|
async function listTrackedFiles(cwd) {
|
|
9451
|
-
const lines = (args) =>
|
|
9655
|
+
const lines = (args) => git3(cwd, args).then((out) => out ? out.split("\n") : [], () => []);
|
|
9452
9656
|
const [files, wiki] = await Promise.all([lines(["ls-files"]), lines(["ls-files", "--others", "--ignored", "--exclude-standard", "--", WIKI_DIR])]);
|
|
9453
9657
|
return { files, wiki };
|
|
9454
9658
|
}
|
|
9455
9659
|
async function headCommit(cwd) {
|
|
9456
|
-
return
|
|
9660
|
+
return git3(cwd, ["rev-parse", "--short", "HEAD"]).then((sha) => sha || null, () => null);
|
|
9457
9661
|
}
|
|
9458
9662
|
async function collectGitDigest(cwd, recent = 30) {
|
|
9459
9663
|
try {
|
|
9460
|
-
await
|
|
9664
|
+
await git3(cwd, ["rev-parse", "--git-dir"]);
|
|
9461
9665
|
} catch {
|
|
9462
9666
|
return null;
|
|
9463
9667
|
}
|
|
9464
9668
|
const safe = async (args) => {
|
|
9465
9669
|
try {
|
|
9466
|
-
return await
|
|
9670
|
+
return await git3(cwd, args);
|
|
9467
9671
|
} catch {
|
|
9468
9672
|
return "";
|
|
9469
9673
|
}
|
|
@@ -9853,13 +10057,13 @@ var told = (r) => ({
|
|
|
9853
10057
|
});
|
|
9854
10058
|
|
|
9855
10059
|
// ../daemon/src/objective-definitions.ts
|
|
9856
|
-
import { execFile as
|
|
10060
|
+
import { execFile as execFile6 } from "child_process";
|
|
9857
10061
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
9858
10062
|
import { existsSync as existsSync3 } from "fs";
|
|
9859
|
-
import { mkdir as mkdir3, readFile as
|
|
9860
|
-
import { join as
|
|
9861
|
-
import { promisify as
|
|
9862
|
-
var
|
|
10063
|
+
import { mkdir as mkdir3, readFile as readFile6 } from "fs/promises";
|
|
10064
|
+
import { join as join10 } from "path";
|
|
10065
|
+
import { promisify as promisify6 } from "util";
|
|
10066
|
+
var execFileAsync6 = promisify6(execFile6);
|
|
9863
10067
|
var DEFINITIONS_TIMEOUT_MS = 9e4;
|
|
9864
10068
|
var DEFINITIONS_THINKING_TOKENS = 2e3;
|
|
9865
10069
|
var READ_TOOLS3 = /* @__PURE__ */ new Set(["Read", "Glob", "Grep", "StructuredOutput"]);
|
|
@@ -9868,7 +10072,7 @@ var COUNTING_LINES_MAX = 80;
|
|
|
9868
10072
|
var PER_FILE = 4;
|
|
9869
10073
|
async function countingLines(cwd) {
|
|
9870
10074
|
try {
|
|
9871
|
-
const { stdout } = await
|
|
10075
|
+
const { stdout } = await execFileAsync6("git", ["grep", "-n", "-I", "-i", "-E", COUNTING, "--", ".", ":!*lock*", ":!*.min.*", ":!*.map", ":!*.svg", ":!*.snap"], {
|
|
9872
10076
|
cwd,
|
|
9873
10077
|
maxBuffer: 16 * 1024 * 1024,
|
|
9874
10078
|
timeout: 5e3
|
|
@@ -9893,7 +10097,7 @@ function pickLines(raw) {
|
|
|
9893
10097
|
}
|
|
9894
10098
|
async function killRecorded(runDir) {
|
|
9895
10099
|
try {
|
|
9896
|
-
const pid = Number((await
|
|
10100
|
+
const pid = Number((await readFile6(join10(runDir, "harness.pid"), "utf8")).trim());
|
|
9897
10101
|
if (pid > 1) process.kill(pid, "SIGKILL");
|
|
9898
10102
|
} catch {
|
|
9899
10103
|
}
|
|
@@ -9913,7 +10117,7 @@ async function readDefinitions({ adapter, spec: spec2, publish: publish5, journa
|
|
|
9913
10117
|
if (!hasRepo) return give({}, "No repository to read: the card offers the common definitions.", "ok");
|
|
9914
10118
|
try {
|
|
9915
10119
|
const skill = await loadSkill("define-objective");
|
|
9916
|
-
const runDir =
|
|
10120
|
+
const runDir = join10(spec2.runDir, "definitions");
|
|
9917
10121
|
await mkdir3(runDir, { recursive: true, mode: 448 });
|
|
9918
10122
|
const schema = definitionsOutputSchema();
|
|
9919
10123
|
const [lines, listed] = await Promise.all([countingLines(spec2.cwd), listTrackedFiles(spec2.cwd)]);
|
|
@@ -9975,7 +10179,7 @@ ${renderTrackedFiles(listed.files, void 0, listed.wiki)}
|
|
|
9975
10179
|
if (!result) throw new Error("no answer");
|
|
9976
10180
|
const parsed = DefinitionsAnswer.safeParse(JSON.parse(result));
|
|
9977
10181
|
if (!parsed.success) throw new Error("an answer in the wrong shape");
|
|
9978
|
-
const checked = checkProposals(parsed.data, (file) => existsSync3(
|
|
10182
|
+
const checked = checkProposals(parsed.data, (file) => existsSync3(join10(spec2.cwd, file)));
|
|
9979
10183
|
const definitions2 = lever ? checked[lever] ? { [lever]: checked[lever] } : {} : checked;
|
|
9980
10184
|
const kept = Object.values(definitions2).reduce((n, list) => n + (list?.length ?? 0), 0);
|
|
9981
10185
|
const read6 = Object.values(definitions2).flat().filter((d) => d?.found).length;
|
|
@@ -10249,9 +10453,9 @@ function localDate(timezone, at) {
|
|
|
10249
10453
|
// ../daemon/src/run-job.ts
|
|
10250
10454
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
10251
10455
|
import { existsSync as existsSync4 } from "fs";
|
|
10252
|
-
import { mkdir as
|
|
10253
|
-
import { homedir as
|
|
10254
|
-
import { join as
|
|
10456
|
+
import { mkdir as mkdir5, readFile as readFile8, rm as rm4 } from "fs/promises";
|
|
10457
|
+
import { homedir as homedir4, tmpdir } from "os";
|
|
10458
|
+
import { join as join12, resolve as resolve4 } from "path";
|
|
10255
10459
|
|
|
10256
10460
|
// ../daemon/src/events.ts
|
|
10257
10461
|
var JobEventWriter = class {
|
|
@@ -10331,195 +10535,166 @@ function lighten(value) {
|
|
|
10331
10535
|
return value ?? null;
|
|
10332
10536
|
}
|
|
10333
10537
|
|
|
10334
|
-
// ../daemon/src/
|
|
10335
|
-
import {
|
|
10336
|
-
import {
|
|
10337
|
-
|
|
10338
|
-
var
|
|
10339
|
-
var
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
};
|
|
10346
|
-
function branchName(ref) {
|
|
10347
|
-
const slug = ref.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
10348
|
-
if (!slug) throw new RepoError(`"${ref}" has no letters or digits to make a branch name from`);
|
|
10349
|
-
return `nanopm/${slug}`;
|
|
10350
|
-
}
|
|
10351
|
-
async function readState(cwd) {
|
|
10352
|
-
try {
|
|
10353
|
-
await git3(cwd, ["rev-parse", "--git-dir"]);
|
|
10354
|
-
} catch {
|
|
10355
|
-
throw new RepoError(`${cwd} is not a git repository`);
|
|
10356
|
-
}
|
|
10357
|
-
const [branch, head, status2, remoteUrl] = await Promise.all([
|
|
10358
|
-
git3(cwd, ["branch", "--show-current"]).catch(() => ""),
|
|
10359
|
-
git3(cwd, ["rev-parse", "HEAD"]).catch(() => ""),
|
|
10360
|
-
git3(cwd, ["status", "--porcelain"]).catch(() => ""),
|
|
10361
|
-
git3(cwd, ["remote", "get-url", "origin"]).catch(() => "")
|
|
10362
|
-
]);
|
|
10363
|
-
const dirty = status2.split("\n").filter(Boolean).map((l) => l.slice(3));
|
|
10364
|
-
return { branch, head, clean: dirty.length === 0, dirty, hasRemote: Boolean(remoteUrl), remoteUrl: remoteUrl || void 0 };
|
|
10365
|
-
}
|
|
10366
|
-
async function ensureCleanTree(cwd) {
|
|
10367
|
-
const state = await readState(cwd);
|
|
10368
|
-
if (!state.head) {
|
|
10369
|
-
throw new RepoError("this repository has no commits yet; make the first commit, then run nanopm next");
|
|
10370
|
-
}
|
|
10371
|
-
if (!state.clean) {
|
|
10372
|
-
const shown = state.dirty.slice(0, 5).join(", ");
|
|
10373
|
-
const more = state.dirty.length > 5 ? `, and ${state.dirty.length - 5} more` : "";
|
|
10374
|
-
throw new RepoError(
|
|
10375
|
-
`the working tree has uncommitted changes (${shown}${more}). NanoPM commits only its own work, so commit or stash yours first.`
|
|
10376
|
-
);
|
|
10377
|
-
}
|
|
10378
|
-
return state;
|
|
10379
|
-
}
|
|
10380
|
-
async function createBranch(cwd, name) {
|
|
10381
|
-
const existing = await git3(cwd, ["branch", "--list", name]).catch(() => "");
|
|
10382
|
-
if (existing.trim()) {
|
|
10383
|
-
throw new RepoError(`branch ${name} already exists. Delete it or finish what is on it first.`);
|
|
10384
|
-
}
|
|
10385
|
-
await git3(cwd, ["checkout", "-b", name]);
|
|
10386
|
-
}
|
|
10387
|
-
async function commitAll(cwd, message) {
|
|
10388
|
-
await git3(cwd, ["add", "-A"]);
|
|
10389
|
-
const staged = await git3(cwd, ["diff", "--cached", "--name-only"]).catch(() => "");
|
|
10390
|
-
if (!staged) return null;
|
|
10391
|
-
await git3(cwd, ["commit", "-m", message, "-m", "Written by NanoPM. Review before merging."]);
|
|
10392
|
-
const sha = await git3(cwd, ["rev-parse", "--short", "HEAD"]);
|
|
10393
|
-
return { sha, ...await diffStat(cwd) };
|
|
10394
|
-
}
|
|
10395
|
-
async function diffStat(cwd) {
|
|
10396
|
-
const numstat = await git3(cwd, ["show", "--numstat", "--format=", "HEAD"]).catch(() => "");
|
|
10397
|
-
let files = 0;
|
|
10398
|
-
let insertions = 0;
|
|
10399
|
-
let deletions = 0;
|
|
10400
|
-
for (const line of numstat.split("\n").filter(Boolean)) {
|
|
10401
|
-
const [add2 = "", del = ""] = line.split(" ");
|
|
10402
|
-
files += 1;
|
|
10403
|
-
insertions += Number(add2) || 0;
|
|
10404
|
-
deletions += Number(del) || 0;
|
|
10405
|
-
}
|
|
10406
|
-
return { files, insertions, deletions };
|
|
10407
|
-
}
|
|
10408
|
-
async function pushBranch(cwd, name) {
|
|
10409
|
-
const state = await readState(cwd);
|
|
10410
|
-
if (!state.hasRemote) return { pushed: false, reason: "this repository has no origin remote" };
|
|
10538
|
+
// ../daemon/src/build-job.ts
|
|
10539
|
+
import { mkdir as mkdir4, readdir as readdir2, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
|
|
10540
|
+
import { homedir as homedir3 } from "os";
|
|
10541
|
+
import { join as join11 } from "path";
|
|
10542
|
+
var PLAN_TOOL = "mcp__nanopm__build_plan";
|
|
10543
|
+
var STEP_TOOL = "mcp__nanopm__build_step";
|
|
10544
|
+
var CLAUDE_CODE_MIN = "2.1.283";
|
|
10545
|
+
async function prepareBuild(client, job, clone, adapter) {
|
|
10546
|
+
const params = job.params ?? {};
|
|
10547
|
+
const dir = buildDir(homedir3(), job.id);
|
|
10548
|
+
let made = false;
|
|
10411
10549
|
try {
|
|
10412
|
-
const
|
|
10413
|
-
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10550
|
+
const [solution, machine] = await Promise.all([loadSolution(client, job.project_id, String(params.solution_id ?? "")), machineRefusal(adapter)]);
|
|
10551
|
+
const refused = buildRefusal(solution, { anyway: params.anyway === true }) ?? machine;
|
|
10552
|
+
if (refused) return { ok: false, outcome: failed(refused) };
|
|
10553
|
+
const branch = buildBranch(solution.ref, solution.title);
|
|
10554
|
+
await mkdir4(join11(dir, ".."), { recursive: true, mode: 448 });
|
|
10555
|
+
const { base, fork, worktree } = await addWorktree(clone, dir, branch);
|
|
10556
|
+
made = true;
|
|
10557
|
+
await handOverTheSpec(worktree, solution.agent_spec);
|
|
10558
|
+
await startSolution(client, job.project_id, solution.ref, job.id);
|
|
10559
|
+
return { ok: true, build: { solution, clone, dir, worktree, branch, base, fork, network: buildNetwork(await readdir2(dir)) } };
|
|
10417
10560
|
} catch (err) {
|
|
10418
|
-
|
|
10419
|
-
return {
|
|
10561
|
+
if (made) await removeWorktree(clone, dir);
|
|
10562
|
+
return { ok: false, outcome: failed(errorMessage(err)) };
|
|
10420
10563
|
}
|
|
10421
10564
|
}
|
|
10422
|
-
function
|
|
10423
|
-
const
|
|
10424
|
-
|
|
10425
|
-
}
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
|
|
10430
|
-
|
|
10431
|
-
}
|
|
10432
|
-
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
}
|
|
10565
|
+
async function runBuildJob(input) {
|
|
10566
|
+
const { client, job, adapter, spec: spec2, events, log, verifyCommand: verifyCommand2 } = input;
|
|
10567
|
+
const { solution, clone, dir, worktree, branch, base, fork } = input.build;
|
|
10568
|
+
await log("build.start", `building ${solution.ref} on ${branch}, in a copy of your code`);
|
|
10569
|
+
await events.write("text", { text: `Working on ${branch}, in a copy of your code. Your own checkout is not touched.` });
|
|
10570
|
+
let plan = null;
|
|
10571
|
+
const done = /* @__PURE__ */ new Set();
|
|
10572
|
+
let usage;
|
|
10573
|
+
let said = "";
|
|
10574
|
+
const stop2 = async (message, opts = { keep: true }) => {
|
|
10575
|
+
await unstartSolution(client, job.project_id, solution.ref, solution.status).catch(() => void 0);
|
|
10576
|
+
if (opts.keep) await commitAll(worktree, `${solution.ref}: unfinished \u2014 ${solution.title}`).catch(() => null);
|
|
10577
|
+
else await removeWorktree(clone, dir);
|
|
10578
|
+
return { ...failed(opts.keep ? `${message}. What was done is on ${branch}, in ${dir}.` : message), usage };
|
|
10579
|
+
};
|
|
10580
|
+
const session2 = async (prompt, resume2) => {
|
|
10581
|
+
let end;
|
|
10582
|
+
for await (const event of adapter.run({ ...spec2, prompt, resumeSessionId: resume2 ? spec2.sessionId : void 0 })) {
|
|
10583
|
+
await input.onEvent(event);
|
|
10584
|
+
if (event.type === "tool_use" && event.tool === PLAN_TOOL && !plan) {
|
|
10585
|
+
plan = stepsOf(event.input);
|
|
10586
|
+
if (plan) await events.write("plan", { steps: plan });
|
|
10587
|
+
} else if (event.type === "tool_result" && event.tool === STEP_TOOL && !event.isError) {
|
|
10588
|
+
const { step, summary } = event.input ?? {};
|
|
10589
|
+
const index = Number(step) - 1;
|
|
10590
|
+
if (Number.isInteger(index) && index >= 0 && !done.has(index)) {
|
|
10591
|
+
const says = plan?.[index] ?? `step ${index + 1}`;
|
|
10592
|
+
const commit = await commitAll(worktree, `${solution.ref}: ${says}`, summary);
|
|
10593
|
+
done.add(index);
|
|
10594
|
+
await events.write("step", { step: index + 1, says, commit: commit?.sha ?? null, files: commit?.files ?? 0 });
|
|
10595
|
+
}
|
|
10596
|
+
} else if (event.type === "finished") {
|
|
10597
|
+
usage = sumUsage(usage, event.usage);
|
|
10598
|
+
said = event.result;
|
|
10599
|
+
end = { status: "succeeded" };
|
|
10600
|
+
} else if (event.type === "failed") {
|
|
10601
|
+
end = { status: "failed", error: { code: event.code, message: event.message } };
|
|
10602
|
+
}
|
|
10603
|
+
}
|
|
10604
|
+
return end?.status === "succeeded" ? null : end ?? failed("the agent produced no result");
|
|
10605
|
+
};
|
|
10606
|
+
const check = verifyCommand2 ? `
|
|
10436
10607
|
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
|
|
10608
|
+
The project's own check is \`${verifyCommand2}\`. Run it before your last build_step, fix what it reports, and give its result under *How I checked it*.` : "";
|
|
10609
|
+
let ended = await session2(spec2.prompt + check, false);
|
|
10610
|
+
for (let resumes = 0; !ended && !(plan && !stepsLeft(plan, done).length); resumes += 1) {
|
|
10611
|
+
if (resumes >= BUILD_RESUMES_MAX) {
|
|
10612
|
+
const left = stepsLeft(plan, done).length;
|
|
10613
|
+
return stop2(plan ? `Nano stopped with ${left} step${left === 1 ? "" : "s"} of its plan left` : "Nano stopped without saying its plan");
|
|
10614
|
+
}
|
|
10615
|
+
ended = await session2(carryOn(plan, done), true);
|
|
10616
|
+
}
|
|
10617
|
+
if (ended) return stop2(ended.error?.message ?? "the build stopped");
|
|
10618
|
+
await commitAll(worktree, `${solution.ref}: ${solution.title}`);
|
|
10619
|
+
if (!await commitsSinceFork(worktree, fork)) return stop2("the build finished without changing anything", { keep: false });
|
|
10620
|
+
const rebased = await rebaseOnto(worktree, base);
|
|
10621
|
+
if (!rebased.ok) return stop2(`${base} moved while Nano was building, and the same files changed on both sides: ${rebased.files.join(", ") || "see git status"}`);
|
|
10622
|
+
const [pushed, noPr, page] = await Promise.all([pushBranch(worktree, branch), ghReady(worktree), solutionPage(client, job.project_id, solution.ref)]);
|
|
10623
|
+
if (!pushed.pushed) return stop2(`the push failed: ${pushed.reason}`);
|
|
10624
|
+
const pr = noPr ? null : await openPullRequest(worktree, { branch, base, title: `${solution.ref}: ${solution.title}`, body: prBody(said, page) });
|
|
10625
|
+
if (pr && !pr.ok) return { ...failed(`the branch ${branch} is pushed, but GitHub refused the pull request: ${pr.reason}${pushed.reviewUrl ? `. Open it yourself: ${pushed.reviewUrl}` : ""}`), usage };
|
|
10626
|
+
if (pr) {
|
|
10627
|
+
await events.write("pr", { number: pr.number, url: pr.url });
|
|
10628
|
+
await log("build.pr", `draft pull request #${pr.number} for ${solution.ref}`);
|
|
10629
|
+
}
|
|
10630
|
+
await removeWorktree(clone, dir);
|
|
10631
|
+
return {
|
|
10632
|
+
status: "succeeded",
|
|
10633
|
+
result: [
|
|
10634
|
+
said.trim(),
|
|
10635
|
+
"",
|
|
10636
|
+
pr ? `Draft pull request #${pr.number} \xB7 ${pr.url}` : `Pushed ${branch}. No pull request: ${noPr}.${pushed.reviewUrl ? ` ${pushed.reviewUrl}` : ""}`
|
|
10637
|
+
].filter(Boolean).join("\n"),
|
|
10638
|
+
data: { branch, session: spec2.sessionId, ...pr ? { pr: { number: pr.number, url: pr.url } } : { review_url: pushed.reviewUrl ?? null } },
|
|
10639
|
+
usage
|
|
10640
|
+
};
|
|
10641
|
+
}
|
|
10642
|
+
var failed = (message) => ({ status: "failed", error: { code: "harness_error", message } });
|
|
10643
|
+
async function loadSolution(client, projectId, id) {
|
|
10644
|
+
const { data, error } = await client.from("solutions").select(BUILD_COLUMNS).eq("id", id).eq("project_id", projectId).is("superseded_by", null).maybeSingle();
|
|
10440
10645
|
if (error) throw new Error(`could not read the solution: ${error.message}`);
|
|
10441
10646
|
if (!data) throw new Error("that solution is not on this project any more");
|
|
10442
|
-
|
|
10443
|
-
throw new Error(`${data.ref} is ${data.status}. Only a solution you accepted can be built.`);
|
|
10444
|
-
}
|
|
10445
|
-
return { ...data, sources: data.sources ?? [] };
|
|
10446
|
-
}
|
|
10447
|
-
async function startSolution(client, projectId, ref, jobId) {
|
|
10448
|
-
const { error } = await client.rpc("start_solution", { p_project_id: projectId, p_ref: ref, p_job_id: jobId });
|
|
10449
|
-
if (error && !/only an accepted solution/.test(error.message)) throw new Error(pgMessage(error));
|
|
10450
|
-
}
|
|
10451
|
-
async function unstartSolution(client, projectId, ref) {
|
|
10452
|
-
await client.rpc("unstart_solution", { p_project_id: projectId, p_ref: ref });
|
|
10647
|
+
return data;
|
|
10453
10648
|
}
|
|
10454
|
-
function
|
|
10455
|
-
const
|
|
10456
|
-
|
|
10457
|
-
if (
|
|
10458
|
-
|
|
10459
|
-
|
|
10649
|
+
async function machineRefusal(adapter) {
|
|
10650
|
+
const found = await adapter.detect();
|
|
10651
|
+
const version = /\d+\.\d+\.\d+/.exec(found.version ?? "")?.[0];
|
|
10652
|
+
if (version && !newer(CLAUDE_CODE_MIN, version)) return null;
|
|
10653
|
+
return `a build needs Claude Code ${CLAUDE_CODE_MIN} or later on this machine${found.version ? ` (it has ${found.version})` : ""}: run claude update`;
|
|
10654
|
+
}
|
|
10655
|
+
async function handOverTheSpec(worktree, agentSpec) {
|
|
10656
|
+
await mkdir4(join11(worktree.dir, ".nanopm"), { recursive: true });
|
|
10657
|
+
await writeFile5(join11(worktree.dir, ".nanopm", "SPEC.md"), agentSpec);
|
|
10658
|
+
const exclude = join11(await commonGitDir(worktree), "info", "exclude");
|
|
10659
|
+
const current = await readFile7(exclude, "utf8").catch(() => "");
|
|
10660
|
+
if (!current.split("\n").includes(".nanopm/")) {
|
|
10661
|
+
await mkdir4(join11(exclude, ".."), { recursive: true });
|
|
10662
|
+
await writeFile5(exclude, `${current}${current && !current.endsWith("\n") ? "\n" : ""}.nanopm/
|
|
10663
|
+
`);
|
|
10460
10664
|
}
|
|
10461
|
-
return lines.join("\n");
|
|
10462
10665
|
}
|
|
10463
|
-
|
|
10464
|
-
const
|
|
10465
|
-
|
|
10466
|
-
throw new RepoError("this repository has a detached HEAD; check out a branch first");
|
|
10467
|
-
}
|
|
10468
|
-
const branch = branchName(ref);
|
|
10469
|
-
await createBranch(cwd, branch);
|
|
10470
|
-
return { branch, baseBranch: state.branch };
|
|
10666
|
+
function stepsOf(input) {
|
|
10667
|
+
const steps = input?.steps;
|
|
10668
|
+
return Array.isArray(steps) && steps.length && steps.every((s) => typeof s === "string") ? steps : null;
|
|
10471
10669
|
}
|
|
10472
|
-
|
|
10473
|
-
|
|
10670
|
+
function carryOn(plan, done) {
|
|
10671
|
+
if (!plan) return "You stopped before saying your plan. Call build_plan with it now, then build it step by step, calling build_step after each.";
|
|
10672
|
+
const left = stepsLeft(plan, done);
|
|
10673
|
+
return [
|
|
10674
|
+
`Steps ${left.map((i) => i + 1).join(", ")} of your plan are not done yet:`,
|
|
10675
|
+
...left.map((i) => `${i + 1}. ${plan[i]}`),
|
|
10676
|
+
"",
|
|
10677
|
+
"Check git log and git status to see where you are, then carry on: finish each step, run its checks, and call build_step after it. Do not run anything in the background."
|
|
10678
|
+
].join("\n");
|
|
10474
10679
|
}
|
|
10475
|
-
async function
|
|
10476
|
-
|
|
10680
|
+
async function solutionPage(client, projectId, ref) {
|
|
10681
|
+
const { data } = await client.from("projects").select("slug").eq("id", projectId).maybeSingle();
|
|
10682
|
+
return data?.slug ? `${webUrl()}/p/${data.slug}/roadmap/${ref}` : null;
|
|
10477
10683
|
}
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
commit = null;
|
|
10486
|
-
}
|
|
10487
|
-
if (!commit) {
|
|
10488
|
-
await checkout(cwd, session2.baseBranch).catch(() => void 0);
|
|
10489
|
-
await deleteBranch(cwd, session2.branch);
|
|
10490
|
-
return { branch: session2.branch, pushed: false, note: "nothing was changed, so no branch was kept" };
|
|
10491
|
-
}
|
|
10492
|
-
const push = await pushBranch(cwd, session2.branch);
|
|
10493
|
-
await checkout(cwd, session2.baseBranch).catch(() => void 0);
|
|
10494
|
-
return {
|
|
10495
|
-
branch: session2.branch,
|
|
10496
|
-
commit,
|
|
10497
|
-
pushed: push.pushed,
|
|
10498
|
-
reviewUrl: push.reviewUrl,
|
|
10499
|
-
note: push.pushed ? void 0 : `not pushed: ${push.reason}`
|
|
10500
|
-
};
|
|
10684
|
+
function prBody(said, page) {
|
|
10685
|
+
return [
|
|
10686
|
+
said.trim(),
|
|
10687
|
+
page ? `
|
|
10688
|
+
The solution and its spec: ${page}` : "",
|
|
10689
|
+
"\n\u2014 Built by NanoPM with Claude Code. A draft: review before merging."
|
|
10690
|
+
].filter(Boolean).join("\n");
|
|
10501
10691
|
}
|
|
10502
|
-
async function
|
|
10503
|
-
const
|
|
10504
|
-
if (
|
|
10505
|
-
|
|
10506
|
-
|
|
10507
|
-
|
|
10508
|
-
p_project_id: job.project_id,
|
|
10509
|
-
p_ref: solution.ref,
|
|
10510
|
-
p_job_id: job.id,
|
|
10511
|
-
p_sources: sources2
|
|
10512
|
-
});
|
|
10513
|
-
if (error) throw new Error(`the work is committed on ${result.branch}, but recording it failed: ${error.message}`);
|
|
10514
|
-
}
|
|
10515
|
-
function renderRepoResult(result) {
|
|
10516
|
-
if (!result.commit) return result.note ?? "nothing changed";
|
|
10517
|
-
const { files, insertions, deletions } = result.commit;
|
|
10518
|
-
const stat2 = `${files} file${files === 1 ? "" : "s"} changed, ${insertions} insertion${insertions === 1 ? "" : "s"}, ${deletions} deletion${deletions === 1 ? "" : "s"}`;
|
|
10519
|
-
const lines = [`${result.branch} \xB7 ${result.commit.sha} \xB7 ${stat2}`];
|
|
10520
|
-
if (result.reviewUrl) lines.push(result.reviewUrl);
|
|
10521
|
-
else if (result.note) lines.push(result.note);
|
|
10522
|
-
return lines.join("\n");
|
|
10692
|
+
async function startSolution(client, projectId, ref, jobId) {
|
|
10693
|
+
const { error } = await client.rpc("start_solution", { p_project_id: projectId, p_ref: ref, p_job_id: jobId });
|
|
10694
|
+
if (error) throw new Error(pgMessage(error));
|
|
10695
|
+
}
|
|
10696
|
+
async function unstartSolution(client, projectId, ref, was) {
|
|
10697
|
+
await client.rpc("unstart_solution", { p_project_id: projectId, p_ref: ref, p_to: was === "proposed" ? "proposed" : "accepted" });
|
|
10523
10698
|
}
|
|
10524
10699
|
|
|
10525
10700
|
// ../daemon/src/run-job.ts
|
|
@@ -10537,107 +10712,6 @@ async function askAboutSite(opts) {
|
|
|
10537
10712
|
});
|
|
10538
10713
|
}
|
|
10539
10714
|
|
|
10540
|
-
// ../daemon/src/verify.ts
|
|
10541
|
-
import { spawn as spawn3 } from "child_process";
|
|
10542
|
-
var VERIFY_TIMEOUT_MS = 10 * 6e4;
|
|
10543
|
-
var OUTPUT_TAIL = 4e3;
|
|
10544
|
-
var DEFINES_THE_COMMAND = [
|
|
10545
|
-
/(^|\/)package\.json$/,
|
|
10546
|
-
/(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb)$/,
|
|
10547
|
-
/(^|\/)(Makefile|justfile|Justfile|Taskfile\.ya?ml)$/,
|
|
10548
|
-
/(^|\/)\.husky\//,
|
|
10549
|
-
/(^|\/)\.github\//,
|
|
10550
|
-
/(^|\/)(pyproject\.toml|uv\.lock|Cargo\.toml|Cargo\.lock|Gemfile|Gemfile\.lock)$/
|
|
10551
|
-
];
|
|
10552
|
-
function pathsNamedIn(command) {
|
|
10553
|
-
return command.split(/[\s;&|()<>]+/).filter((t) => /[/.]/.test(t) && !/^-/.test(t) && t.length > 2).map((t) => t.replace(/^\.\//, "").replace(/^["']|["']$/g, ""));
|
|
10554
|
-
}
|
|
10555
|
-
function guardVerify(changed, command) {
|
|
10556
|
-
for (const file of changed) {
|
|
10557
|
-
const pattern = DEFINES_THE_COMMAND.find((p) => p.test(file));
|
|
10558
|
-
if (pattern) {
|
|
10559
|
-
return `the change touched ${file}, which decides what the verify command does, so the command was not run`;
|
|
10560
|
-
}
|
|
10561
|
-
}
|
|
10562
|
-
const named = pathsNamedIn(command);
|
|
10563
|
-
for (const file of changed) {
|
|
10564
|
-
if (named.some((n) => file === n || file.endsWith(`/${n}`) || n.endsWith(file))) {
|
|
10565
|
-
return `the change touched ${file}, which the verify command runs, so the command was not run`;
|
|
10566
|
-
}
|
|
10567
|
-
}
|
|
10568
|
-
return null;
|
|
10569
|
-
}
|
|
10570
|
-
async function verify(opts) {
|
|
10571
|
-
const skipped = guardVerify(opts.changed, opts.command);
|
|
10572
|
-
if (skipped) return { ran: false, passed: false, skipped, output: "", durationMs: 0, timedOut: false };
|
|
10573
|
-
return runCommand(opts);
|
|
10574
|
-
}
|
|
10575
|
-
async function runCommand(opts) {
|
|
10576
|
-
const timeoutMs = opts.timeoutMs ?? VERIFY_TIMEOUT_MS;
|
|
10577
|
-
const startedAt = Date.now();
|
|
10578
|
-
const child = spawn3("sh", ["-c", opts.command], {
|
|
10579
|
-
cwd: opts.cwd,
|
|
10580
|
-
env: opts.env,
|
|
10581
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
10582
|
-
});
|
|
10583
|
-
let output = "";
|
|
10584
|
-
const collect6 = (chunk) => {
|
|
10585
|
-
output += chunk.toString();
|
|
10586
|
-
if (output.length > OUTPUT_TAIL * 4) output = output.slice(-OUTPUT_TAIL * 2);
|
|
10587
|
-
};
|
|
10588
|
-
child.stdout.on("data", collect6);
|
|
10589
|
-
child.stderr.on("data", collect6);
|
|
10590
|
-
let timedOut = false;
|
|
10591
|
-
const timer = setTimeout(() => {
|
|
10592
|
-
timedOut = true;
|
|
10593
|
-
child.kill("SIGTERM");
|
|
10594
|
-
setTimeout(() => child.kill("SIGKILL"), 5e3).unref();
|
|
10595
|
-
}, timeoutMs);
|
|
10596
|
-
const onAbort = () => child.kill("SIGTERM");
|
|
10597
|
-
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
10598
|
-
const exitCode = await new Promise((resolve6) => {
|
|
10599
|
-
child.on("close", (code) => resolve6(code));
|
|
10600
|
-
child.on("error", () => resolve6(127));
|
|
10601
|
-
});
|
|
10602
|
-
clearTimeout(timer);
|
|
10603
|
-
opts.signal?.removeEventListener("abort", onAbort);
|
|
10604
|
-
return {
|
|
10605
|
-
ran: true,
|
|
10606
|
-
passed: exitCode === 0 && !timedOut,
|
|
10607
|
-
output: tail(output, OUTPUT_TAIL),
|
|
10608
|
-
exitCode: exitCode ?? void 0,
|
|
10609
|
-
durationMs: Date.now() - startedAt,
|
|
10610
|
-
timedOut
|
|
10611
|
-
};
|
|
10612
|
-
}
|
|
10613
|
-
function tail(text2, max) {
|
|
10614
|
-
const trimmed = text2.trimEnd();
|
|
10615
|
-
if (trimmed.length <= max) return trimmed;
|
|
10616
|
-
return `\u2026 ${trimmed.slice(-max)}`;
|
|
10617
|
-
}
|
|
10618
|
-
function renderVerifyResult(result, command) {
|
|
10619
|
-
if (!result.ran) return result.skipped ?? "no verify command is set for this project";
|
|
10620
|
-
const secs = (result.durationMs / 1e3).toFixed(0);
|
|
10621
|
-
if (result.timedOut) return `the check NanoPM could not run itself, \`${command}\`, did not finish within ${secs}s and was stopped`;
|
|
10622
|
-
return result.passed ? `the check NanoPM could not run itself, \`${command}\`, passed in ${secs}s` : `the check NanoPM could not run itself, \`${command}\`, failed (exit ${result.exitCode}) after ${secs}s`;
|
|
10623
|
-
}
|
|
10624
|
-
function retryPrompt(result, command) {
|
|
10625
|
-
return [
|
|
10626
|
-
`Your change does not pass this project's own check.`,
|
|
10627
|
-
``,
|
|
10628
|
-
`The daemon ran \`${command}\` in the repository root and it failed with exit ${result.exitCode}.`,
|
|
10629
|
-
`You did not run this and cannot run it; this output is the only evidence you get.`,
|
|
10630
|
-
``,
|
|
10631
|
-
"```",
|
|
10632
|
-
result.output || "(the command produced no output)",
|
|
10633
|
-
"```",
|
|
10634
|
-
``,
|
|
10635
|
-
`Fix what this reports, in the files you already changed where possible. Do not disable`,
|
|
10636
|
-
`the check, do not edit what the command runs, and do not change unrelated code to make`,
|
|
10637
|
-
`it pass. If the failure is not caused by your change, say so plainly and stop.`
|
|
10638
|
-
].join("\n");
|
|
10639
|
-
}
|
|
10640
|
-
|
|
10641
10715
|
// ../daemon/src/run-job.ts
|
|
10642
10716
|
var QUICK_RUNS = { reword: runRewordJob, talk: runTalkJob };
|
|
10643
10717
|
async function runJob(job, ctx) {
|
|
@@ -10689,8 +10763,8 @@ async function runJob(job, ctx) {
|
|
|
10689
10763
|
return outcome;
|
|
10690
10764
|
}
|
|
10691
10765
|
async function execute(job, skill, ctx, events) {
|
|
10692
|
-
const runDir =
|
|
10693
|
-
await
|
|
10766
|
+
const runDir = join12(nanopmHome(), "run", job.id);
|
|
10767
|
+
await mkdir5(runDir, { recursive: true, mode: 448 });
|
|
10694
10768
|
const resumeSessionId = job.resume_of ? await sessionOf(ctx.client, job.resume_of) : void 0;
|
|
10695
10769
|
const sessionId = resumeSessionId ?? randomUUID12();
|
|
10696
10770
|
await ctx.client.from("jobs").update({ harness_session_id: sessionId }).eq("id", job.id);
|
|
@@ -10734,14 +10808,14 @@ async function execute(job, skill, ctx, events) {
|
|
|
10734
10808
|
clearTimeout(timer);
|
|
10735
10809
|
clearInterval(heartbeat2);
|
|
10736
10810
|
clearInterval(cancelPoll);
|
|
10737
|
-
await
|
|
10811
|
+
await rm4(runDir, { recursive: true, force: true });
|
|
10738
10812
|
}
|
|
10739
10813
|
}
|
|
10740
10814
|
const quickRun2 = QUICK_RUNS[job.skill];
|
|
10741
10815
|
if (quickRun2) {
|
|
10742
10816
|
const cwd2 = scratchDir(job.id);
|
|
10743
10817
|
try {
|
|
10744
|
-
await
|
|
10818
|
+
await mkdir5(cwd2, { recursive: true, mode: 448 });
|
|
10745
10819
|
const caps = skill.manifest.capabilities;
|
|
10746
10820
|
return await quickRun2({
|
|
10747
10821
|
client: ctx.client,
|
|
@@ -10761,13 +10835,22 @@ async function execute(job, skill, ctx, events) {
|
|
|
10761
10835
|
clearTimeout(timer);
|
|
10762
10836
|
clearInterval(heartbeat2);
|
|
10763
10837
|
clearInterval(cancelPoll);
|
|
10764
|
-
await
|
|
10765
|
-
await
|
|
10838
|
+
await rm4(runDir, { recursive: true, force: true });
|
|
10839
|
+
await rm4(cwd2, { recursive: true, force: true });
|
|
10766
10840
|
}
|
|
10767
10841
|
}
|
|
10768
|
-
const capabilities = capCapabilities(skill.manifest.capabilities, autonomy2);
|
|
10842
|
+
const capabilities = capCapabilities(skill.manifest.capabilities, autonomy2, { asked: skill.manifest.asked && job.requested_by === ctx.userId });
|
|
10769
10843
|
const harnessWeb = await claudeSearches(searchProvider);
|
|
10770
|
-
|
|
10844
|
+
let cwd = await resolveCwd(ctx, job, params, scratchDir(job.id), capabilities.includes("write_repo"));
|
|
10845
|
+
const prepared = job.skill === BUILD_SKILL ? await prepareBuild(ctx.client, job, cwd, adapter) : null;
|
|
10846
|
+
if (prepared && !prepared.ok) {
|
|
10847
|
+
clearTimeout(timer);
|
|
10848
|
+
clearInterval(heartbeat2);
|
|
10849
|
+
clearInterval(cancelPoll);
|
|
10850
|
+
return prepared.outcome;
|
|
10851
|
+
}
|
|
10852
|
+
const build2 = prepared?.build;
|
|
10853
|
+
if (build2) cwd = build2.dir;
|
|
10771
10854
|
const definitionsRead = job.skill === "onboard" && !job.resume_of ? readDefinitions({
|
|
10772
10855
|
adapter,
|
|
10773
10856
|
spec: { cwd, runDir, systemPrompt: "", prompt: "", capabilities: [], mcp: [], maxTurns: 1, sessionId, env: harnessEnv(timeoutMs), signal: abort.signal },
|
|
@@ -10791,29 +10874,6 @@ async function execute(job, skill, ctx, events) {
|
|
|
10791
10874
|
const read6 = site.pages.length ? `read ${site.pages.map((p) => new URL(p.url).pathname).join(", ")} on ${site.host}` : `could not read ${site.host}`;
|
|
10792
10875
|
await logActivity(ctx, job, "scan.site", site.missed.length ? `${read6} (${site.missed.length} not read)` : read6);
|
|
10793
10876
|
}
|
|
10794
|
-
let item;
|
|
10795
|
-
let session2;
|
|
10796
|
-
if (capabilities.includes("write_repo")) {
|
|
10797
|
-
const solutionId = params.solution_id;
|
|
10798
|
-
if (typeof solutionId !== "string" || !solutionId) {
|
|
10799
|
-
clearTimeout(timer);
|
|
10800
|
-
clearInterval(heartbeat2);
|
|
10801
|
-
clearInterval(cancelPoll);
|
|
10802
|
-
return { status: "failed", error: { code: "harness_error", message: "this job names no solution to build" } };
|
|
10803
|
-
}
|
|
10804
|
-
try {
|
|
10805
|
-
item = await loadAcceptedSolution(ctx.client, job.project_id, solutionId);
|
|
10806
|
-
session2 = await openRepoSession(cwd, item.ref);
|
|
10807
|
-
await startSolution(ctx.client, job.project_id, item.ref, job.id);
|
|
10808
|
-
params.item = renderSolution(item);
|
|
10809
|
-
await logActivity(ctx, job, "repo.branch", `working on ${session2.branch}`);
|
|
10810
|
-
} catch (err) {
|
|
10811
|
-
clearTimeout(timer);
|
|
10812
|
-
clearInterval(heartbeat2);
|
|
10813
|
-
clearInterval(cancelPoll);
|
|
10814
|
-
return { status: "failed", error: { code: "harness_error", message: errorMessage(err) } };
|
|
10815
|
-
}
|
|
10816
|
-
}
|
|
10817
10877
|
const spec2 = {
|
|
10818
10878
|
cwd,
|
|
10819
10879
|
runDir,
|
|
@@ -10833,20 +10893,21 @@ async function execute(job, skill, ctx, events) {
|
|
|
10833
10893
|
].filter(Boolean).join("\n\n"),
|
|
10834
10894
|
capabilities,
|
|
10835
10895
|
harnessWeb,
|
|
10896
|
+
network: build2?.network,
|
|
10836
10897
|
withoutTools: skill.manifest.withoutTools,
|
|
10837
10898
|
// The workspace reaches the memory server only for a job that changes files; that is
|
|
10838
|
-
// what lets
|
|
10899
|
+
// what lets the build's tools refuse everywhere else.
|
|
10839
10900
|
mcp: ctx.mcpServers?.(job, timeoutMs, capabilities.includes("write_repo") ? cwd : void 0) ?? [],
|
|
10840
10901
|
model: skill.manifest.model,
|
|
10841
10902
|
maxTurns: skill.manifest.maxTurns,
|
|
10842
10903
|
sessionId,
|
|
10843
10904
|
resumeSessionId,
|
|
10844
|
-
|
|
10905
|
+
// Print mode kills a background task when the turn ends; a build waits on its commands (§9).
|
|
10906
|
+
env: build2 ? { ...harnessEnv(timeoutMs), CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" } : harnessEnv(timeoutMs),
|
|
10845
10907
|
signal: abort.signal
|
|
10846
10908
|
};
|
|
10847
10909
|
const verifyCommand2 = project?.preferences?.verify_command?.trim() || null;
|
|
10848
10910
|
let outcome;
|
|
10849
|
-
let verified;
|
|
10850
10911
|
let webCalls = 0;
|
|
10851
10912
|
const onHarnessEvent = async (event) => {
|
|
10852
10913
|
await events.harness(event);
|
|
@@ -10864,6 +10925,24 @@ async function execute(job, skill, ctx, events) {
|
|
|
10864
10925
|
return result;
|
|
10865
10926
|
};
|
|
10866
10927
|
try {
|
|
10928
|
+
if (build2) {
|
|
10929
|
+
try {
|
|
10930
|
+
return await runBuildJob({
|
|
10931
|
+
client: ctx.client,
|
|
10932
|
+
job,
|
|
10933
|
+
adapter,
|
|
10934
|
+
build: build2,
|
|
10935
|
+
spec: spec2,
|
|
10936
|
+
verifyCommand: verifyCommand2,
|
|
10937
|
+
events,
|
|
10938
|
+
onEvent: onHarnessEvent,
|
|
10939
|
+
log: (step, summary, result) => logActivity(ctx, job, step, summary, result ? { outcome: result } : {})
|
|
10940
|
+
});
|
|
10941
|
+
} catch (error) {
|
|
10942
|
+
if (cancelled) return { status: "cancelled" };
|
|
10943
|
+
return { status: "failed", error: { code: timedOut ? "timeout" : "harness_error", message: errorMessage(error) } };
|
|
10944
|
+
}
|
|
10945
|
+
}
|
|
10867
10946
|
if (job.skill === "personas") {
|
|
10868
10947
|
try {
|
|
10869
10948
|
return await runPersonaJob({ client: ctx.client, job, adapter, spec: spec2, onEvent: onHarnessEvent });
|
|
@@ -10944,83 +11023,23 @@ async function execute(job, skill, ctx, events) {
|
|
|
10944
11023
|
return { status: "failed", error: { code: timedOut ? "timeout" : "harness_error", message: errorMessage(error) } };
|
|
10945
11024
|
}
|
|
10946
11025
|
}
|
|
10947
|
-
for (
|
|
10948
|
-
|
|
10949
|
-
|
|
10950
|
-
|
|
10951
|
-
|
|
10952
|
-
|
|
10953
|
-
if (event.type === "finished") {
|
|
10954
|
-
attemptOutcome = { status: "succeeded", result: event.result, usage: event.usage };
|
|
10955
|
-
} else if (event.type === "failed") {
|
|
10956
|
-
attemptOutcome = { status: "failed", error: { code: event.code, message: event.message } };
|
|
10957
|
-
}
|
|
11026
|
+
for await (const event of adapter.run(spec2)) {
|
|
11027
|
+
await onHarnessEvent(event);
|
|
11028
|
+
if (event.type === "finished") {
|
|
11029
|
+
outcome = { status: "succeeded", result: event.result, usage: event.usage };
|
|
11030
|
+
} else if (event.type === "failed") {
|
|
11031
|
+
outcome = { status: "failed", error: { code: event.code, message: event.message } };
|
|
10958
11032
|
}
|
|
10959
|
-
if (attemptOutcome) await noteIfItNeverSearched(attemptOutcome, capabilities.includes("read_web"));
|
|
10960
|
-
outcome = mergeAttempts(outcome, attemptOutcome);
|
|
10961
|
-
if (attemptOutcome?.status !== "succeeded" || !session2 || !verifyCommand2) break;
|
|
10962
|
-
verified = await verify({
|
|
10963
|
-
cwd,
|
|
10964
|
-
command: verifyCommand2,
|
|
10965
|
-
env: harnessEnv(timeoutMs),
|
|
10966
|
-
changed: await changedFiles(cwd),
|
|
10967
|
-
signal: abort.signal
|
|
10968
|
-
});
|
|
10969
|
-
await logActivity(ctx, job, "repo.verify", renderVerifyResult(verified, verifyCommand2), {
|
|
10970
|
-
outcome: verified.passed ? "ok" : "error",
|
|
10971
|
-
usage: { duration_ms: verified.durationMs }
|
|
10972
|
-
});
|
|
10973
|
-
await events.write("text", { text: renderVerifyResult(verified, verifyCommand2) });
|
|
10974
|
-
if (verified.passed || !verified.ran || attempt7 === 2) break;
|
|
10975
11033
|
}
|
|
11034
|
+
if (outcome) await noteIfItNeverSearched(outcome, capabilities.includes("read_web"));
|
|
10976
11035
|
} finally {
|
|
10977
11036
|
clearTimeout(timer);
|
|
10978
11037
|
clearInterval(heartbeat2);
|
|
10979
11038
|
clearInterval(cancelPoll);
|
|
10980
11039
|
await definitionsRead;
|
|
10981
|
-
await
|
|
10982
|
-
}
|
|
10983
|
-
if (outcome?.status === "succeeded" && verified && verified.ran && !verified.passed) {
|
|
10984
|
-
outcome = {
|
|
10985
|
-
...outcome,
|
|
10986
|
-
status: "failed",
|
|
10987
|
-
error: {
|
|
10988
|
-
code: "harness_error",
|
|
10989
|
-
message: `${renderVerifyResult(verified, verifyCommand2)}. The work is on the branch; the solution stays accepted.`
|
|
10990
|
-
}
|
|
10991
|
-
};
|
|
10992
|
-
}
|
|
10993
|
-
if (outcome?.status === "succeeded") {
|
|
10994
|
-
if (!session2 || !item) return outcome;
|
|
10995
|
-
try {
|
|
10996
|
-
const repo = await closeRepoSession(cwd, session2, item);
|
|
10997
|
-
await logActivity(ctx, job, "repo.commit", renderRepoResult(repo).split("\n")[0]);
|
|
10998
|
-
if (!repo.commit) {
|
|
10999
|
-
await unstartSolution(ctx.client, job.project_id, item.ref).catch(() => void 0);
|
|
11000
|
-
return {
|
|
11001
|
-
...outcome,
|
|
11002
|
-
status: "failed",
|
|
11003
|
-
error: { code: "harness_error", message: "the skill finished without changing anything" }
|
|
11004
|
-
};
|
|
11005
|
-
}
|
|
11006
|
-
await finishSolution(ctx.client, job, item, repo);
|
|
11007
|
-
return { ...outcome, result: [outcome.result, "", renderRepoResult(repo)].filter((l) => l !== void 0).join("\n") };
|
|
11008
|
-
} catch (err) {
|
|
11009
|
-
return {
|
|
11010
|
-
...outcome,
|
|
11011
|
-
status: "failed",
|
|
11012
|
-
error: { code: "harness_error", message: errorMessage(err) }
|
|
11013
|
-
};
|
|
11014
|
-
}
|
|
11015
|
-
}
|
|
11016
|
-
if (session2) {
|
|
11017
|
-
const kept = await abandonRepoSession(cwd, session2, verified?.ran && !verified.passed ? item : void 0).catch(() => void 0);
|
|
11018
|
-
if (item) await unstartSolution(ctx.client, job.project_id, item.ref).catch(() => void 0);
|
|
11019
|
-
if (kept?.commit) {
|
|
11020
|
-
await logActivity(ctx, job, "repo.commit", renderRepoResult(kept).split("\n")[0], { outcome: "error" });
|
|
11021
|
-
if (outcome) outcome = { ...outcome, result: [outcome.result, "", renderRepoResult(kept)].filter(Boolean).join("\n") };
|
|
11022
|
-
}
|
|
11040
|
+
await rm4(runDir, { recursive: true, force: true });
|
|
11023
11041
|
}
|
|
11042
|
+
if (outcome?.status === "succeeded") return outcome;
|
|
11024
11043
|
if (cancelled) return { status: "cancelled" };
|
|
11025
11044
|
if (timedOut) {
|
|
11026
11045
|
return { status: "failed", error: { code: "timeout", message: `exceeded the ${skill.manifest.timeout} cap` } };
|
|
@@ -11031,10 +11050,6 @@ async function sessionOf(client, jobId) {
|
|
|
11031
11050
|
const { data } = await client.from("jobs").select("harness_session_id").eq("id", jobId).maybeSingle();
|
|
11032
11051
|
return data?.harness_session_id ?? void 0;
|
|
11033
11052
|
}
|
|
11034
|
-
function mergeAttempts(first, second) {
|
|
11035
|
-
if (!first?.usage || !second?.usage) return second ?? first;
|
|
11036
|
-
return { ...second, usage: sumUsage(first.usage, second.usage) };
|
|
11037
|
-
}
|
|
11038
11053
|
async function askForTheSite(ctx, job, project, events) {
|
|
11039
11054
|
const started = Date.now();
|
|
11040
11055
|
const search = await searchForSite(project.name);
|
|
@@ -11060,7 +11075,7 @@ function harnessWebFor(searchProvider, machineCanSearch) {
|
|
|
11060
11075
|
}
|
|
11061
11076
|
async function machineWebSearch() {
|
|
11062
11077
|
try {
|
|
11063
|
-
const parsed = JSON.parse(await
|
|
11078
|
+
const parsed = JSON.parse(await readFile8(join12(nanopmHome(), "daemon.json"), "utf8"));
|
|
11064
11079
|
return parsed.webSearch === true;
|
|
11065
11080
|
} catch {
|
|
11066
11081
|
return false;
|
|
@@ -11102,7 +11117,7 @@ function buildHeader(job, project, cwd, harnessWeb = false, memory = false) {
|
|
|
11102
11117
|
return lines.join("\n");
|
|
11103
11118
|
}
|
|
11104
11119
|
function scratchDir(jobId) {
|
|
11105
|
-
return
|
|
11120
|
+
return join12(tmpdir(), `nanopm-run-${jobId}`);
|
|
11106
11121
|
}
|
|
11107
11122
|
async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
|
|
11108
11123
|
const refuse2 = (why) => {
|
|
@@ -11111,12 +11126,12 @@ async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
|
|
|
11111
11126
|
const requested = params.repo_path;
|
|
11112
11127
|
if (typeof requested !== "string" || !requested) {
|
|
11113
11128
|
if (mustBeTheClone) refuse2("the job named no repository");
|
|
11114
|
-
await
|
|
11129
|
+
await mkdir5(fallback, { recursive: true, mode: 448 });
|
|
11115
11130
|
return fallback;
|
|
11116
11131
|
}
|
|
11117
11132
|
if (!job.project_id) {
|
|
11118
11133
|
if (mustBeTheClone) refuse2("the job belongs to no project");
|
|
11119
|
-
await
|
|
11134
|
+
await mkdir5(fallback, { recursive: true, mode: 448 });
|
|
11120
11135
|
return fallback;
|
|
11121
11136
|
}
|
|
11122
11137
|
const { data: clone } = await ctx.client.from("clones").select("repo_path").eq("project_id", job.project_id).eq("daemon_id", ctx.daemonId).maybeSingle();
|
|
@@ -11124,12 +11139,12 @@ async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
|
|
|
11124
11139
|
if (!allowed || resolve4(allowed) !== resolve4(requested)) {
|
|
11125
11140
|
if (mustBeTheClone) refuse2(`${requested} is not the clone recorded for this project on this machine. Run any \`nanopm\` command inside your clone here to record it`);
|
|
11126
11141
|
console.error(`job ${job.id}: repo_path ${requested} is not this machine's clone; running in a scratch directory instead`);
|
|
11127
|
-
await
|
|
11142
|
+
await mkdir5(fallback, { recursive: true, mode: 448 });
|
|
11128
11143
|
return fallback;
|
|
11129
11144
|
}
|
|
11130
11145
|
if (existsSync4(allowed)) return resolve4(allowed);
|
|
11131
11146
|
if (mustBeTheClone) refuse2(`${allowed} is not on this machine any more`);
|
|
11132
|
-
await
|
|
11147
|
+
await mkdir5(fallback, { recursive: true, mode: 448 });
|
|
11133
11148
|
return fallback;
|
|
11134
11149
|
}
|
|
11135
11150
|
function asData8(label, body) {
|
|
@@ -11138,10 +11153,10 @@ ${body}
|
|
|
11138
11153
|
</${label}>`;
|
|
11139
11154
|
}
|
|
11140
11155
|
function harnessEnv(timeoutMs, source = process.env) {
|
|
11141
|
-
const home = source.HOME ??
|
|
11156
|
+
const home = source.HOME ?? homedir4();
|
|
11142
11157
|
const env = {
|
|
11143
11158
|
HOME: home,
|
|
11144
|
-
PATH: [source.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
11159
|
+
PATH: [source.PATH ?? "/usr/local/bin:/usr/bin:/bin", join12(home, ".local", "bin")].join(":"),
|
|
11145
11160
|
MCP_TOOL_TIMEOUT: String(timeoutMs)
|
|
11146
11161
|
};
|
|
11147
11162
|
for (const key of ["USER", "LANG", "TMPDIR", "SHELL", "ANTHROPIC_API_KEY", "CLAUDE_CONFIG_DIR"]) {
|
|
@@ -11318,12 +11333,12 @@ async function chainAfter(auth, job, outcome, log) {
|
|
|
11318
11333
|
const parent = job.parent_job_id ? await parentOf(auth, job.parent_job_id) : null;
|
|
11319
11334
|
const own = job.params ?? {};
|
|
11320
11335
|
const deep = own.depth === "deep" && job.trigger === "hook";
|
|
11321
|
-
const
|
|
11322
|
-
if (!
|
|
11336
|
+
const next = daemonQueues(job.skill, 0, parent?.skill ?? null, deep);
|
|
11337
|
+
if (!next.length) return;
|
|
11323
11338
|
if (await chainDepth(auth, job) >= CHAIN_DEPTH_MAX) return;
|
|
11324
11339
|
const from = deep ? own.repo_path : parent?.params?.repo_path;
|
|
11325
11340
|
const repo = typeof from === "string" ? { repo_path: from } : null;
|
|
11326
|
-
for (const skill of
|
|
11341
|
+
for (const skill of next) {
|
|
11327
11342
|
const params = { ...repo ?? {}, ...deep ? { depth: "deep", background: true } : {} };
|
|
11328
11343
|
const { data, error } = await auth.client.from("jobs").insert({
|
|
11329
11344
|
project_id: job.project_id,
|
|
@@ -11406,7 +11421,7 @@ var heldInThisProcess = false;
|
|
|
11406
11421
|
function acquireLock() {
|
|
11407
11422
|
if (heldInThisProcess) throw new Error("a nanopm daemon is already running in this process");
|
|
11408
11423
|
mkdirSync(nanopmHome(), { recursive: true, mode: 448 });
|
|
11409
|
-
const path =
|
|
11424
|
+
const path = join13(nanopmHome(), "daemon.pid");
|
|
11410
11425
|
for (; ; ) {
|
|
11411
11426
|
try {
|
|
11412
11427
|
const fd = openSync(path, "wx");
|
|
@@ -11493,13 +11508,13 @@ async function reclaimOwnJobs(client, daemonId) {
|
|
|
11493
11508
|
async function harnessPids(runDir) {
|
|
11494
11509
|
const dirs = [runDir];
|
|
11495
11510
|
try {
|
|
11496
|
-
for (const d of await
|
|
11511
|
+
for (const d of await readdir3(runDir, { withFileTypes: true })) if (d.isDirectory()) dirs.push(join13(runDir, d.name));
|
|
11497
11512
|
} catch {
|
|
11498
11513
|
}
|
|
11499
11514
|
const pids = [];
|
|
11500
11515
|
for (const dir of dirs) {
|
|
11501
11516
|
try {
|
|
11502
|
-
const pid = Number((await
|
|
11517
|
+
const pid = Number((await readFile9(join13(dir, "harness.pid"), "utf8")).trim());
|
|
11503
11518
|
if (pid > 1) pids.push(pid);
|
|
11504
11519
|
} catch {
|
|
11505
11520
|
}
|
|
@@ -11507,20 +11522,20 @@ async function harnessPids(runDir) {
|
|
|
11507
11522
|
return pids;
|
|
11508
11523
|
}
|
|
11509
11524
|
async function killOrphanedHarness(jobId) {
|
|
11510
|
-
const runDir =
|
|
11525
|
+
const runDir = join13(nanopmHome(), "run", jobId);
|
|
11511
11526
|
for (const pid of await harnessPids(runDir)) {
|
|
11512
11527
|
try {
|
|
11513
11528
|
process.kill(pid, "SIGTERM");
|
|
11514
11529
|
} catch {
|
|
11515
11530
|
}
|
|
11516
11531
|
}
|
|
11517
|
-
await
|
|
11532
|
+
await rm5(runDir, { recursive: true, force: true });
|
|
11518
11533
|
}
|
|
11519
11534
|
async function registerDaemon(auth, version, opts = {}) {
|
|
11520
|
-
const path =
|
|
11535
|
+
const path = join13(nanopmHome(), "daemon.json");
|
|
11521
11536
|
let identity = null;
|
|
11522
11537
|
try {
|
|
11523
|
-
identity = JSON.parse(await
|
|
11538
|
+
identity = JSON.parse(await readFile9(path, "utf8"));
|
|
11524
11539
|
} catch {
|
|
11525
11540
|
identity = null;
|
|
11526
11541
|
}
|
|
@@ -11558,8 +11573,8 @@ async function registerDaemon(auth, version, opts = {}) {
|
|
|
11558
11573
|
return data.id;
|
|
11559
11574
|
}
|
|
11560
11575
|
async function rememberDaemon(id, shared = false) {
|
|
11561
|
-
await
|
|
11562
|
-
await
|
|
11576
|
+
await mkdir6(nanopmHome(), { recursive: true, mode: 448 });
|
|
11577
|
+
await writeFile6(join13(nanopmHome(), "daemon.json"), JSON.stringify({ id, shared }, null, 2) + "\n", { mode: 384 });
|
|
11563
11578
|
}
|
|
11564
11579
|
async function heartbeat(client, daemonId) {
|
|
11565
11580
|
await client.from("daemons").update({ status: "online", last_seen_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", daemonId);
|
|
@@ -11910,48 +11925,6 @@ function refuseFetch(url, allowlist, open = false) {
|
|
|
11910
11925
|
return `${host} is not on this project's allowlist, so it was not read. The founder can allow it: if it is a competitor, \`nanopm market confirm <ref>\` on the entry that names them, or \`nanopm market add <name> ${host}\`. Until then, search results and the sites already allowed are what there is.`;
|
|
11911
11926
|
}
|
|
11912
11927
|
|
|
11913
|
-
// ../memory-mcp/src/repo-rules.ts
|
|
11914
|
-
import { lstat, unlink } from "fs/promises";
|
|
11915
|
-
import { relative as relative3, resolve as resolve5, sep as sep2 } from "path";
|
|
11916
|
-
var REMOVE_MAX = 50;
|
|
11917
|
-
function refuseRemove(workspace, requested) {
|
|
11918
|
-
const root2 = resolve5(workspace);
|
|
11919
|
-
const target = resolve5(root2, requested);
|
|
11920
|
-
const rel = relative3(root2, target);
|
|
11921
|
-
if (!rel || rel.startsWith("..") || rel.startsWith(`..${sep2}`)) return "outside the workspace";
|
|
11922
|
-
if (rel === ".git" || rel.startsWith(`.git${sep2}`)) return "inside .git";
|
|
11923
|
-
if (looksLikeASecret(rel)) return "looks like a secret, which the agent may neither read nor remove";
|
|
11924
|
-
return null;
|
|
11925
|
-
}
|
|
11926
|
-
async function removeFiles(workspace, paths) {
|
|
11927
|
-
const out = [];
|
|
11928
|
-
for (const requested of paths.slice(0, REMOVE_MAX)) {
|
|
11929
|
-
const refusal = refuseRemove(workspace, requested);
|
|
11930
|
-
if (refusal) {
|
|
11931
|
-
out.push({ path: requested, removed: false, reason: refusal });
|
|
11932
|
-
continue;
|
|
11933
|
-
}
|
|
11934
|
-
const target = resolve5(workspace, requested);
|
|
11935
|
-
try {
|
|
11936
|
-
const stat2 = await lstat(target);
|
|
11937
|
-
if (stat2.isSymbolicLink()) {
|
|
11938
|
-
out.push({ path: requested, removed: false, reason: "a symlink; what it points at is not touched" });
|
|
11939
|
-
continue;
|
|
11940
|
-
}
|
|
11941
|
-
if (stat2.isDirectory()) {
|
|
11942
|
-
out.push({ path: requested, removed: false, reason: "a directory; name the files in it" });
|
|
11943
|
-
continue;
|
|
11944
|
-
}
|
|
11945
|
-
await unlink(target);
|
|
11946
|
-
out.push({ path: requested, removed: true });
|
|
11947
|
-
} catch (err) {
|
|
11948
|
-
const code = err.code;
|
|
11949
|
-
out.push({ path: requested, removed: false, reason: code === "ENOENT" ? "not there" : err instanceof Error ? err.message : String(err) });
|
|
11950
|
-
}
|
|
11951
|
-
}
|
|
11952
|
-
return out;
|
|
11953
|
-
}
|
|
11954
|
-
|
|
11955
11928
|
// ../memory-mcp/src/problem-rules.ts
|
|
11956
11929
|
import { z as z20 } from "zod";
|
|
11957
11930
|
var STATEMENT_MAX = 300;
|
|
@@ -12326,7 +12299,7 @@ ${renderMap()}`,
|
|
|
12326
12299
|
}
|
|
12327
12300
|
},
|
|
12328
12301
|
async ({ ref, outcome, note }) => {
|
|
12329
|
-
return json(await
|
|
12302
|
+
return json(await finishSolution(ctx, ref, outcome, note));
|
|
12330
12303
|
}
|
|
12331
12304
|
);
|
|
12332
12305
|
server.registerTool(
|
|
@@ -12426,20 +12399,20 @@ ${renderMap()}`,
|
|
|
12426
12399
|
}
|
|
12427
12400
|
);
|
|
12428
12401
|
server.registerTool(
|
|
12429
|
-
"
|
|
12402
|
+
"build_plan",
|
|
12430
12403
|
{
|
|
12431
|
-
description:
|
|
12432
|
-
inputSchema: {
|
|
12404
|
+
description: `Say the plan before you change anything: ${BUILD_STEPS_MIN} to ${BUILD_STEPS_MAX} steps, in order, each one change the founder can recognise, in their words (at most ${BUILD_STEP_MAX} characters). The last step is your own check of the whole change. Call it once; the founder sees it as a checklist.`,
|
|
12405
|
+
inputSchema: { steps: z21.array(z21.string().min(3).max(BUILD_STEP_MAX)).min(BUILD_STEPS_MIN).max(BUILD_STEPS_MAX) }
|
|
12433
12406
|
},
|
|
12434
|
-
async ({
|
|
12435
|
-
|
|
12436
|
-
|
|
12437
|
-
|
|
12438
|
-
|
|
12439
|
-
|
|
12440
|
-
}
|
|
12441
|
-
|
|
12442
|
-
}
|
|
12407
|
+
async ({ steps }) => ctx.workspace ? json({ recorded: steps.length, note: "Recorded. Build step 1 now; call build_step when it is done." }) : failure("this job builds nothing")
|
|
12408
|
+
);
|
|
12409
|
+
server.registerTool(
|
|
12410
|
+
"build_step",
|
|
12411
|
+
{
|
|
12412
|
+
description: "A step of your plan is done and its checks pass: NanoPM commits the worktree now, with the step as the message, and ticks it for the founder. Call it once per step, in order, after the step works. Do not run git commit yourself.",
|
|
12413
|
+
inputSchema: { step: z21.number().int().min(1).max(BUILD_STEPS_MAX), summary: z21.string().min(3).max(300) }
|
|
12414
|
+
},
|
|
12415
|
+
async ({ step }) => ctx.workspace ? json({ step, note: "Committed by NanoPM. Go on with the next step, or stop when the plan is done." }) : failure("this job builds nothing")
|
|
12443
12416
|
);
|
|
12444
12417
|
server.registerTool(
|
|
12445
12418
|
"web_search",
|
|
@@ -13226,7 +13199,7 @@ async function markFast(ctx, problemRef) {
|
|
|
13226
13199
|
await logActivity2(ctx, "problems.fast", `${problem.ref} is one small change`);
|
|
13227
13200
|
return { problem: problem.ref, fast: true };
|
|
13228
13201
|
}
|
|
13229
|
-
async function
|
|
13202
|
+
async function finishSolution(ctx, ref, outcome, note) {
|
|
13230
13203
|
if (ctx.actor === "mapping-review") throw new Error("A targeted collection cannot decide solution outcomes.");
|
|
13231
13204
|
const upper = ref.toUpperCase();
|
|
13232
13205
|
const { data: current } = await ctx.client.from("solutions").select("status").eq("project_id", ctx.projectId).eq("ref", upper).is("superseded_by", null).maybeSingle();
|
|
@@ -13524,7 +13497,8 @@ var MEMORY_TOOL_NAMES = [
|
|
|
13524
13497
|
"play_store_search",
|
|
13525
13498
|
"list_market",
|
|
13526
13499
|
"write_market",
|
|
13527
|
-
"
|
|
13500
|
+
"build_plan",
|
|
13501
|
+
"build_step",
|
|
13528
13502
|
// Talk to Nano (docs/✅nanopm-68-talk-to-nano.md): the map read, and changed as the founder asked.
|
|
13529
13503
|
...TALK_TOOLS
|
|
13530
13504
|
];
|
|
@@ -13535,7 +13509,7 @@ async function startMemoryServer(env = process.env) {
|
|
|
13535
13509
|
}
|
|
13536
13510
|
|
|
13537
13511
|
// src/commands/add.ts
|
|
13538
|
-
import { readFile as
|
|
13512
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
13539
13513
|
|
|
13540
13514
|
// src/project.ts
|
|
13541
13515
|
import { execFile as execFile7 } from "child_process";
|
|
@@ -13543,11 +13517,11 @@ import { basename } from "path";
|
|
|
13543
13517
|
import { promisify as promisify7 } from "util";
|
|
13544
13518
|
|
|
13545
13519
|
// src/local.ts
|
|
13546
|
-
import { readFile as
|
|
13547
|
-
import { join as
|
|
13520
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
13521
|
+
import { join as join14 } from "path";
|
|
13548
13522
|
async function localDaemonId() {
|
|
13549
13523
|
try {
|
|
13550
|
-
const parsed = JSON.parse(await
|
|
13524
|
+
const parsed = JSON.parse(await readFile10(join14(nanopmHome(), "daemon.json"), "utf8"));
|
|
13551
13525
|
return parsed.id ?? null;
|
|
13552
13526
|
} catch {
|
|
13553
13527
|
return null;
|
|
@@ -13682,7 +13656,7 @@ async function add(file, opts) {
|
|
|
13682
13656
|
text2 = await readStdin();
|
|
13683
13657
|
} else {
|
|
13684
13658
|
try {
|
|
13685
|
-
text2 = await
|
|
13659
|
+
text2 = await readFile11(file, "utf8");
|
|
13686
13660
|
} catch {
|
|
13687
13661
|
throw new Error(`could not read ${file}`);
|
|
13688
13662
|
}
|
|
@@ -13697,7 +13671,7 @@ async function add(file, opts) {
|
|
|
13697
13671
|
}
|
|
13698
13672
|
|
|
13699
13673
|
// src/commands/run.ts
|
|
13700
|
-
import { readFile as
|
|
13674
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
13701
13675
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
13702
13676
|
import { setTimeout as sleep3 } from "timers/promises";
|
|
13703
13677
|
|
|
@@ -13864,7 +13838,7 @@ function phaseOf(tool) {
|
|
|
13864
13838
|
if (name === "query") return "data";
|
|
13865
13839
|
if (name === "web_search" || name === "web_fetch") return "web";
|
|
13866
13840
|
if (REVIEW_WRITES.has(name)) return "review";
|
|
13867
|
-
if (name === "
|
|
13841
|
+
if (name === "build_plan" || name === "build_step") return "build";
|
|
13868
13842
|
return null;
|
|
13869
13843
|
}
|
|
13870
13844
|
function touched(tool, input) {
|
|
@@ -14029,7 +14003,7 @@ function questionBlock(p) {
|
|
|
14029
14003
|
|
|
14030
14004
|
// src/commands/run.ts
|
|
14031
14005
|
async function answersFromFile(path) {
|
|
14032
|
-
const raw = JSON.parse(await
|
|
14006
|
+
const raw = JSON.parse(await readFile12(path, "utf8"));
|
|
14033
14007
|
const entries = raw.map((a) => typeof a === "string" ? { answer: a } : a);
|
|
14034
14008
|
return scriptedAnswerer(entries);
|
|
14035
14009
|
}
|
|
@@ -14252,15 +14226,15 @@ async function askOne(auth, projectId, question, daemonId, opts, out) {
|
|
|
14252
14226
|
// src/commands/daemon-service.ts
|
|
14253
14227
|
import { execFile as execFile8 } from "child_process";
|
|
14254
14228
|
import { existsSync as existsSync5 } from "fs";
|
|
14255
|
-
import { mkdir as
|
|
14256
|
-
import { homedir as
|
|
14257
|
-
import { join as
|
|
14229
|
+
import { mkdir as mkdir7, readFile as readFile13, rm as rm6, writeFile as writeFile7 } from "fs/promises";
|
|
14230
|
+
import { homedir as homedir5 } from "os";
|
|
14231
|
+
import { join as join15 } from "path";
|
|
14258
14232
|
import { promisify as promisify8 } from "util";
|
|
14259
14233
|
|
|
14260
14234
|
// package.json
|
|
14261
14235
|
var package_default = {
|
|
14262
14236
|
name: "@nanopm/cli",
|
|
14263
|
-
version: "0.1.
|
|
14237
|
+
version: "0.1.11",
|
|
14264
14238
|
description: "An autonomous product manager for builders who run several small products. Runs on your machine, drives your own coding harness.",
|
|
14265
14239
|
type: "module",
|
|
14266
14240
|
license: "MIT",
|
|
@@ -14312,10 +14286,10 @@ var VERSION = package_default.version;
|
|
|
14312
14286
|
var execFileAsync8 = promisify8(execFile8);
|
|
14313
14287
|
var LAUNCHD_LABEL = "pm.nano.daemon";
|
|
14314
14288
|
function plistPath() {
|
|
14315
|
-
return
|
|
14289
|
+
return join15(homedir5(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
14316
14290
|
}
|
|
14317
14291
|
function daemonLogPath() {
|
|
14318
|
-
return
|
|
14292
|
+
return join15(nanopmHome(), "daemon.log");
|
|
14319
14293
|
}
|
|
14320
14294
|
function renderPlist(opts) {
|
|
14321
14295
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -14374,13 +14348,13 @@ async function installDaemon(out = (l) => console.log(l), opts = {}) {
|
|
|
14374
14348
|
out(web.ok ? "Claude's own search answers; searches go through your subscription." : `Claude's search is not available on this machine (${web.message}); searches use DuckDuckGo.`);
|
|
14375
14349
|
const bin = process.argv[1];
|
|
14376
14350
|
if (!bin) throw new Error("cannot locate the nanopm binary");
|
|
14377
|
-
const home =
|
|
14378
|
-
const path = [process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
14379
|
-
await
|
|
14380
|
-
await
|
|
14351
|
+
const home = homedir5();
|
|
14352
|
+
const path = [process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", join15(home, ".local", "bin")].join(":");
|
|
14353
|
+
await mkdir7(join15(home, "Library", "LaunchAgents"), { recursive: true });
|
|
14354
|
+
await mkdir7(nanopmHome(), { recursive: true, mode: 448 });
|
|
14381
14355
|
if (opts.shared !== void 0) await rememberShared(opts.shared);
|
|
14382
14356
|
await found({ ...detection, authenticated: true });
|
|
14383
|
-
await
|
|
14357
|
+
await writeFile7(plistPath(), renderPlist({ node: process.execPath, bin, home, path, logPath: daemonLogPath(), nanopmHome: nanopmHome() }), { mode: 420 });
|
|
14384
14358
|
const target = guiTarget();
|
|
14385
14359
|
await execFileAsync8("launchctl", ["bootout", target, plistPath()]).catch(() => void 0);
|
|
14386
14360
|
await execFileAsync8("launchctl", ["bootstrap", target, plistPath()]);
|
|
@@ -14401,7 +14375,7 @@ async function uninstallDaemon(out = (l) => console.log(l)) {
|
|
|
14401
14375
|
requireMac();
|
|
14402
14376
|
const target = guiTarget();
|
|
14403
14377
|
await execFileAsync8("launchctl", ["bootout", target, plistPath()]).catch(() => void 0);
|
|
14404
|
-
await
|
|
14378
|
+
await rm6(plistPath(), { force: true });
|
|
14405
14379
|
out(`Removed ${LAUNCHD_LABEL}.`);
|
|
14406
14380
|
}
|
|
14407
14381
|
async function daemonStatus(out = (l) => console.log(l)) {
|
|
@@ -14428,7 +14402,7 @@ async function daemonStatus(out = (l) => console.log(l)) {
|
|
|
14428
14402
|
}
|
|
14429
14403
|
async function daemonLogs(lines = 50) {
|
|
14430
14404
|
try {
|
|
14431
|
-
const text2 = await
|
|
14405
|
+
const text2 = await readFile13(daemonLogPath(), "utf8");
|
|
14432
14406
|
return text2.trimEnd().split("\n").slice(-lines).join("\n");
|
|
14433
14407
|
} catch {
|
|
14434
14408
|
return `no log at ${daemonLogPath()} yet`;
|
|
@@ -14436,41 +14410,41 @@ async function daemonLogs(lines = 50) {
|
|
|
14436
14410
|
}
|
|
14437
14411
|
async function localWebSearch() {
|
|
14438
14412
|
try {
|
|
14439
|
-
const parsed = JSON.parse(await
|
|
14413
|
+
const parsed = JSON.parse(await readFile13(join15(nanopmHome(), "daemon.json"), "utf8"));
|
|
14440
14414
|
return parsed.webSearch === true;
|
|
14441
14415
|
} catch {
|
|
14442
14416
|
return false;
|
|
14443
14417
|
}
|
|
14444
14418
|
}
|
|
14445
14419
|
async function rememberWebSearch(webSearch2) {
|
|
14446
|
-
const path =
|
|
14420
|
+
const path = join15(nanopmHome(), "daemon.json");
|
|
14447
14421
|
let parsed = {};
|
|
14448
14422
|
try {
|
|
14449
|
-
parsed = JSON.parse(await
|
|
14423
|
+
parsed = JSON.parse(await readFile13(path, "utf8"));
|
|
14450
14424
|
} catch {
|
|
14451
14425
|
parsed = {};
|
|
14452
14426
|
}
|
|
14453
|
-
await
|
|
14454
|
-
await
|
|
14427
|
+
await mkdir7(nanopmHome(), { recursive: true, mode: 448 });
|
|
14428
|
+
await writeFile7(path, JSON.stringify({ ...parsed, webSearch: webSearch2 }, null, 2) + "\n", { mode: 384 });
|
|
14455
14429
|
}
|
|
14456
14430
|
async function localShared() {
|
|
14457
14431
|
try {
|
|
14458
|
-
const parsed = JSON.parse(await
|
|
14432
|
+
const parsed = JSON.parse(await readFile13(join15(nanopmHome(), "daemon.json"), "utf8"));
|
|
14459
14433
|
return Boolean(parsed.shared);
|
|
14460
14434
|
} catch {
|
|
14461
14435
|
return false;
|
|
14462
14436
|
}
|
|
14463
14437
|
}
|
|
14464
14438
|
async function rememberShared(shared) {
|
|
14465
|
-
const path =
|
|
14439
|
+
const path = join15(nanopmHome(), "daemon.json");
|
|
14466
14440
|
let parsed = {};
|
|
14467
14441
|
try {
|
|
14468
|
-
parsed = JSON.parse(await
|
|
14442
|
+
parsed = JSON.parse(await readFile13(path, "utf8"));
|
|
14469
14443
|
} catch {
|
|
14470
14444
|
parsed = {};
|
|
14471
14445
|
}
|
|
14472
|
-
await
|
|
14473
|
-
await
|
|
14446
|
+
await mkdir7(nanopmHome(), { recursive: true, mode: 448 });
|
|
14447
|
+
await writeFile7(path, JSON.stringify({ ...parsed, shared }, null, 2) + "\n", { mode: 384 });
|
|
14474
14448
|
}
|
|
14475
14449
|
|
|
14476
14450
|
// src/format/log.ts
|
|
@@ -14713,702 +14687,766 @@ The install line does the same: curl -fsSL https://nanopm.app/install | sh`);
|
|
|
14713
14687
|
out(await restartDaemon() ? `Updated ${VERSION} \u2192 ${latest}; the daemon restarted on it.` : `Updated ${VERSION} \u2192 ${latest}.`);
|
|
14714
14688
|
}
|
|
14715
14689
|
|
|
14716
|
-
// src/
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
building: { mark: "\u25D4", colour: ANSI.blue, label: "building" },
|
|
14721
|
-
rejected: { mark: "\u2298", colour: ANSI.red, label: "rejected" },
|
|
14722
|
-
done: { mark: "\u25CF", colour: ANSI.green, label: "done" },
|
|
14723
|
-
dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
|
|
14724
|
-
};
|
|
14725
|
-
var WORK_KINDS = /* @__PURE__ */ new Set(["branch", "git", "url", "job"]);
|
|
14726
|
-
function renderRoadmapItem(item, opts, problem, builtBy) {
|
|
14727
|
-
const paint = painter(opts.color);
|
|
14728
|
-
const shape2 = SHAPE[item.status] ?? { mark: "\xB7", colour: "", label: item.status };
|
|
14729
|
-
const out = [`${paint(shape2.colour, shape2.mark)} ${paint(ANSI.bold, item.ref)} ${item.title}`];
|
|
14730
|
-
if (problem) out.push(paint(ANSI.dim, ` answers ${problem.ref} ${clip(problem.statement, Math.max(30, columns() - 20))}`));
|
|
14731
|
-
out.push("");
|
|
14732
|
-
for (const line of wrap(item.reasoning, Math.max(40, columns() - 4))) out.push(` ${line}`);
|
|
14733
|
-
out.push("");
|
|
14734
|
-
const facts = [
|
|
14735
|
-
["status", shape2.label + (item.outcome ? ` \xB7 ${item.outcome}${item.outcome_note ? `: ${item.outcome_note}` : ""}` : "")],
|
|
14736
|
-
["rank", String(item.rank)],
|
|
14737
|
-
["who", item.actor === "founder" ? "you" : "NanoPM"],
|
|
14738
|
-
["bets on", item.assumption ?? "\u2014"],
|
|
14739
|
-
["we'll know", item.eval ?? "\u2014"]
|
|
14740
|
-
];
|
|
14741
|
-
for (const [k, v] of facts) out.push(paint(ANSI.dim, ` ${k.padEnd(9)} ${v}`));
|
|
14742
|
-
const sources2 = item.sources ?? [];
|
|
14743
|
-
const restsOn = sources2.filter((s) => !WORK_KINDS.has(s.kind));
|
|
14744
|
-
if (restsOn.length) {
|
|
14745
|
-
out.push(paint(ANSI.dim, ` ${"rests on".padEnd(9)} ${restsOn.map((s) => `${s.kind}:${s.ref}`).join(", ")}`));
|
|
14746
|
-
}
|
|
14747
|
-
const where = renderWhere(sources2);
|
|
14748
|
-
if (where) out.push(paint(ANSI.dim, ` ${"where".padEnd(9)} ${where}${builtBy ? ` \xB7 built by ${builtBy}` : ""}`));
|
|
14749
|
-
return out.join("\n");
|
|
14750
|
-
}
|
|
14751
|
-
function renderWhere(sources2) {
|
|
14752
|
-
const branch = sources2.find((s) => s.kind === "branch")?.ref;
|
|
14753
|
-
const commit = sources2.find((s) => s.kind === "git")?.ref;
|
|
14754
|
-
const url = sources2.find((s) => s.kind === "url")?.ref;
|
|
14755
|
-
return [branch, commit, url].filter(Boolean).join(" \xB7 ");
|
|
14690
|
+
// src/commands/autonomy.ts
|
|
14691
|
+
async function readAutonomy(client, projectId) {
|
|
14692
|
+
const { data } = await client.from("project_preferences").select("autonomy").eq("project_id", projectId).maybeSingle();
|
|
14693
|
+
return data?.autonomy ?? "propose";
|
|
14756
14694
|
}
|
|
14757
|
-
var
|
|
14758
|
-
|
|
14759
|
-
|
|
14760
|
-
|
|
14761
|
-
merge: { text: "merged into", colour: ANSI.blue },
|
|
14762
|
-
separate: { text: "kept apart", colour: ANSI.blue },
|
|
14763
|
-
weight: { text: "weighed", colour: ANSI.blue },
|
|
14764
|
-
area: { text: "filed", colour: ANSI.blue },
|
|
14765
|
-
accept: { text: "accepted", colour: ANSI.green },
|
|
14766
|
-
reject: { text: "rejected", colour: ANSI.red },
|
|
14767
|
-
first: { text: "put first", colour: ANSI.blue },
|
|
14768
|
-
done: { text: "did", colour: ANSI.green },
|
|
14769
|
-
dismiss: { text: "dismissed", colour: ANSI.red }
|
|
14695
|
+
var EXPLAINED = {
|
|
14696
|
+
propose: "NanoPM reads and proposes. It never changes a file.",
|
|
14697
|
+
execute_after_validation: "NanoPM changes files, on its own branch, after showing you what it will do and waiting for a yes.",
|
|
14698
|
+
execute_then_report: "NanoPM acts first and tells you after. Not available yet."
|
|
14770
14699
|
};
|
|
14771
|
-
function
|
|
14772
|
-
const
|
|
14773
|
-
|
|
14774
|
-
const
|
|
14775
|
-
|
|
14776
|
-
const
|
|
14777
|
-
const
|
|
14778
|
-
|
|
14779
|
-
const by = row.by ? paint(ANSI.dim, ` \xB7 by ${row.by}`) : "";
|
|
14780
|
-
out.push(`${paint(ANSI.dim, when)} ${paint(verb.colour, verb.text)} ${about}${by}`.trimEnd());
|
|
14781
|
-
if (row.body) {
|
|
14782
|
-
for (const line of wrap(row.body, 72)) out.push(paint(ANSI.dim, ` ${line}`));
|
|
14700
|
+
async function autonomy(level, opts) {
|
|
14701
|
+
const auth = await authenticate();
|
|
14702
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
14703
|
+
const current = await readAutonomy(auth.client, project.id);
|
|
14704
|
+
if (!level) {
|
|
14705
|
+
const lines = [`${project.name} is on "${current}".`, ""];
|
|
14706
|
+
for (const l of AUTONOMY_LEVELS) {
|
|
14707
|
+
lines.push(` ${l === current ? "\u25CF" : "\u25CB"} ${l.padEnd(26)} ${EXPLAINED[l]}`);
|
|
14783
14708
|
}
|
|
14709
|
+
lines.push("", " nanopm autonomy <level>");
|
|
14710
|
+
return lines.join("\n");
|
|
14784
14711
|
}
|
|
14785
|
-
|
|
14786
|
-
|
|
14787
|
-
|
|
14788
|
-
|
|
14789
|
-
|
|
14712
|
+
const wanted = level.trim();
|
|
14713
|
+
if (!AUTONOMY_LEVELS.includes(wanted)) {
|
|
14714
|
+
throw new Error(`"${level}" is not a level. Pick one of: ${AUTONOMY_LEVELS.join(", ")}.`);
|
|
14715
|
+
}
|
|
14716
|
+
if (wanted === "execute_then_report") {
|
|
14717
|
+
throw new Error(
|
|
14718
|
+
"execute_then_report is not available yet. NanoPM asks before it changes anything until the review loop has been used in anger."
|
|
14719
|
+
);
|
|
14720
|
+
}
|
|
14721
|
+
if (wanted === current) return `${project.name} is already on "${current}".`;
|
|
14722
|
+
const { error } = await auth.client.from("project_preferences").update({ autonomy: wanted }).eq("project_id", project.id);
|
|
14723
|
+
if (error) {
|
|
14724
|
+
throw new Error(`could not change the dial: ${error.message}`);
|
|
14725
|
+
}
|
|
14726
|
+
return [`${project.name} is now on "${wanted}".`, EXPLAINED[wanted]].join("\n");
|
|
14790
14727
|
}
|
|
14791
|
-
function
|
|
14792
|
-
|
|
14728
|
+
async function verifyCommand(command, opts) {
|
|
14729
|
+
const auth = await authenticate();
|
|
14730
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
14731
|
+
const { data } = await auth.client.from("project_preferences").select("verify_command").eq("project_id", project.id).maybeSingle();
|
|
14732
|
+
const current = data?.verify_command ?? null;
|
|
14733
|
+
if (!command && !opts.clear) {
|
|
14734
|
+
if (!current) {
|
|
14735
|
+
return [
|
|
14736
|
+
`${project.name} has no check, so nothing verifies what NanoPM writes.`,
|
|
14737
|
+
"",
|
|
14738
|
+
' nanopm verify "npm run lint && npm test"',
|
|
14739
|
+
"",
|
|
14740
|
+
"A build runs it in its sandboxed copy of the repository before its last step,",
|
|
14741
|
+
"fixes what it reports, and says the result in the pull request."
|
|
14742
|
+
].join("\n");
|
|
14743
|
+
}
|
|
14744
|
+
return [`${project.name} verifies with:`, "", ` ${current}`, "", ' nanopm verify "<command>" \xB7 nanopm verify --clear'].join("\n");
|
|
14745
|
+
}
|
|
14746
|
+
const next = opts.clear ? null : command.trim();
|
|
14747
|
+
if (next !== null && !next) throw new Error('a check needs a command: nanopm verify "npm test"');
|
|
14748
|
+
const { error } = await auth.client.from("project_preferences").update({ verify_command: next }).eq("project_id", project.id);
|
|
14749
|
+
if (error) throw new Error(`could not save the check: ${error.message}`);
|
|
14750
|
+
if (next === null) return `${project.name} no longer runs a check. Nothing will verify what NanoPM writes.`;
|
|
14751
|
+
return [`${project.name} will verify with:`, "", ` ${next}`, "", "A build runs it in its sandboxed copy of the repository before its last step."].join("\n");
|
|
14793
14752
|
}
|
|
14794
|
-
|
|
14753
|
+
|
|
14754
|
+
// src/format/opportunities.ts
|
|
14755
|
+
function renderOpportunities(rows2, lens, opts) {
|
|
14795
14756
|
const paint = painter(opts.color);
|
|
14796
14757
|
const width = Math.max(40, columns());
|
|
14797
|
-
const
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14803
|
-
|
|
14804
|
-
|
|
14805
|
-
const last = lines.at(-1);
|
|
14806
|
-
if (last !== void 0 && visibleLength(last) + 3 + visibleLength(chip) <= width) lines[lines.length - 1] = `${last} ${chip}`;
|
|
14807
|
-
else lines.push(chip);
|
|
14808
|
-
return lines;
|
|
14809
|
-
}, []);
|
|
14810
|
-
out.push("", ...legend);
|
|
14811
|
-
if (!board.cards.some((c) => c.pick.placed_by === "founder")) out.push(paint(ANSI.dim, "Nano placed these in the order of your opportunity map. Move them where you want them."));
|
|
14812
|
-
const refWidth = Math.max(5, ...board.cards.map((c) => c.pick.ref.length));
|
|
14813
|
-
const gutter = " ".repeat(refWidth + 4);
|
|
14814
|
-
const titleWidth = Math.min(Math.max(...board.cards.map((c) => c.pick.title.length)), width - gutter.length - 8);
|
|
14815
|
-
for (const h of HORIZONS) {
|
|
14816
|
-
const column = board.cards.filter((c) => c.horizon === h);
|
|
14817
|
-
out.push("", `${paint(ANSI.bold, HORIZON_WORD[h])} ${paint(ANSI.dim, String(column.length))}`);
|
|
14818
|
-
if (!column.length) out.push(paint(ANSI.dim, ` Move a card here: nanopm roadmap move <ref> ${h}`));
|
|
14819
|
-
for (const c of column) {
|
|
14820
|
-
const t = themes.get(c.branch.id);
|
|
14821
|
-
out.push(` ${paint(ANSI.dim, c.pick.ref.padEnd(refWidth))} ${paint(t.colour, "\u25CF")} ${paint(ANSI.dim, t.name)}`);
|
|
14822
|
-
out.push(`${gutter}${clip(c.pick.title, titleWidth).padEnd(titleWidth)} ${sizeWord(c.pick.size, paint)}`.trimEnd());
|
|
14823
|
-
const depth = board.working.get(c.leaf.id);
|
|
14824
|
-
if (depth) out.push(`${gutter}${paint(ANSI.blue, "\u25CF")} ${paint(ANSI.dim, WORK_WORD[depth])}`);
|
|
14825
|
-
}
|
|
14758
|
+
const out = [];
|
|
14759
|
+
if (!lens) return "No objective yet: the map is drawn for it. Say what matters most right now on Your business, then `nanopm opportunities run`.";
|
|
14760
|
+
out.push(`${paint(ANSI.bold, "Opportunities")} ${paint(ANSI.dim, `${lens.lever} \u2014 ${lens.sentence}`)}`);
|
|
14761
|
+
out.push(paint(ANSI.dim, lens.definition ? `Counts: ${lens.definition}` : "What it counts is not defined yet: drawn against the lever and your sentence."));
|
|
14762
|
+
if (opts.previous) out.push(paint(ANSI.amber, "Drawn for your previous objective. `nanopm opportunities run` draws it for this one."));
|
|
14763
|
+
if (!rows2.length) {
|
|
14764
|
+
out.push("", "No map yet. `nanopm opportunities run` draws it from what NanoPM knows; add --deep to check it against your users, code and data.");
|
|
14765
|
+
return out.join("\n");
|
|
14826
14766
|
}
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
)
|
|
14767
|
+
const tree = liveTree(rows2);
|
|
14768
|
+
const live = rows2.filter(isLive);
|
|
14769
|
+
const evidenced = live.filter((r) => r.evidence_status === "evidence_backed").length;
|
|
14770
|
+
out.push(paint(ANSI.dim, `${live.length} on the map: ${evidenced} evidence-backed, ${live.length - evidenced} hypotheses \u2014 what users or their behaviour have not shown yet.`));
|
|
14771
|
+
const who = (r) => r.personas.map((p) => opts.names?.get(p) ?? p).join(", ");
|
|
14772
|
+
const one = (r, place) => {
|
|
14773
|
+
const indent = " ".repeat(r.depth - 1);
|
|
14774
|
+
const yours = r.status === "confirmed" ? paint(ANSI.green, " yours") : "";
|
|
14775
|
+
out.push("", `${indent}${paint(ANSI.bold, place.padStart(2))} ${r.depth === 1 ? paint(ANSI.bold, r.problem) : r.problem}${yours} ${paint(ANSI.dim, r.ref)}`);
|
|
14776
|
+
const pad = `${indent} `;
|
|
14777
|
+
const facts = [PRIORITY_WORD[r.priority], EVIDENCE_STATUS_WORD[r.evidence_status], NEXT_ACTION_WORD[r.next_action], who(r)].filter(Boolean).join(" \xB7 ");
|
|
14778
|
+
out.push(`${pad}${paint(ANSI.dim, facts)}`);
|
|
14779
|
+
const gist = r.description ? paragraphs(r.description)[0] : null;
|
|
14780
|
+
if (gist) for (const l of wrap(gist, width - pad.length)) out.push(`${pad}${l}`);
|
|
14781
|
+
if (r.learn) for (const l of wrap(`To learn more: ${r.learn}`, width - pad.length)) out.push(`${pad}${paint(ANSI.dim, l)}`);
|
|
14782
|
+
if (r.founder_rank !== null && r.now_rank) out.push(`${pad}${paint(ANSI.dim, `Its place is yours; NanoPM would now put it at #${r.now_rank}.`)}`);
|
|
14783
|
+
if (r.reconsider) for (const l of wrap(`Worth reconsidering: ${r.reconsider}`, width - pad.length)) out.push(`${pad}${paint(ANSI.amber, l)}`);
|
|
14784
|
+
for (const c of r.children) one(c, `${place}.${c.rank}`);
|
|
14785
|
+
};
|
|
14786
|
+
for (const r of tree) one(r, String(r.rank));
|
|
14787
|
+
const aside = rows2.filter((r) => r.status === "set_aside");
|
|
14788
|
+
if (aside.length) {
|
|
14789
|
+
out.push("", paint(ANSI.bold, `Set aside (${aside.length})`));
|
|
14790
|
+
for (const r of aside) out.push(` ${paint(ANSI.dim, r.ref.padEnd(8))} ${r.problem}${r.set_aside_reason ? paint(ANSI.dim, ` \u2014 ${r.set_aside_reason}`) : ""}`);
|
|
14791
|
+
}
|
|
14792
|
+
out.push("", paint(ANSI.dim, '`nanopm opportunities confirm <ref>`, `set-aside <ref> "why"`, `move <ref> <place>`, `reword <ref> "\u2026"`, `brainstorm <ref> [--deep]`, `run [--deep] [--note "\u2026"] [--use signals,data,repo,market] [--skip \u2026]`.'));
|
|
14833
14793
|
return out.join("\n");
|
|
14834
14794
|
}
|
|
14835
|
-
|
|
14795
|
+
|
|
14796
|
+
// src/format/needs.ts
|
|
14797
|
+
var MARK = { observed: "\u25CF", stated: "\u2013", inferred: "\u25CB" };
|
|
14798
|
+
function renderNeeds(persona, tree, opts) {
|
|
14836
14799
|
const paint = painter(opts.color);
|
|
14837
14800
|
const width = Math.max(40, columns());
|
|
14838
|
-
const theme = at.branch ? themesOf(at.tree).get(at.branch.id) : void 0;
|
|
14839
14801
|
const out = [];
|
|
14840
|
-
|
|
14841
|
-
|
|
14842
|
-
|
|
14843
|
-
|
|
14844
|
-
|
|
14845
|
-
heading("Problem we are solving");
|
|
14846
|
-
if (at.leaf) {
|
|
14847
|
-
const solves = wrap(at.leaf.problem, width - 6 - at.leaf.ref.length - 2);
|
|
14848
|
-
solves[solves.length - 1] += ` ${paint(ANSI.dim, at.leaf.ref)}`;
|
|
14849
|
-
for (const line of solves) out.push(` ${line}`);
|
|
14850
|
-
if (at.branch && at.branch.id !== at.leaf.id) {
|
|
14851
|
-
for (const line of wrap(`Part of: ${at.branch.problem} ${at.branch.ref}`, width - 6)) out.push(` ${paint(ANSI.dim, line)}`);
|
|
14852
|
-
}
|
|
14853
|
-
} else {
|
|
14854
|
-
out.push(paint(ANSI.dim, " The problem it answered is no longer on the map."));
|
|
14855
|
-
}
|
|
14856
|
-
const described = paragraphs(s.description);
|
|
14857
|
-
if (described.length) {
|
|
14858
|
-
heading("Proposed solution");
|
|
14859
|
-
described.forEach((p, i) => {
|
|
14860
|
-
if (i) out.push("");
|
|
14861
|
-
for (const line of wrap(p, width - 6)) out.push(` ${line}`);
|
|
14862
|
-
});
|
|
14863
|
-
}
|
|
14864
|
-
const changelog = parseChangelog(s.changelog);
|
|
14865
|
-
if (changelog) {
|
|
14866
|
-
heading("Future changelog");
|
|
14867
|
-
for (const line of wrap(changelog.headline, width - 6)) out.push(` ${paint(ANSI.bold, line)}`);
|
|
14868
|
-
for (const point of changelog.points) wrap(point, width - 8).forEach((line, i) => out.push(` ${i ? " " : "\u2022"} ${line}`));
|
|
14869
|
-
}
|
|
14870
|
-
if (s.steps.length) {
|
|
14871
|
-
heading("Plan");
|
|
14872
|
-
s.steps.forEach((step, i) => {
|
|
14873
|
-
const who = STEP_WHO_WORD[step.who] ?? step.who;
|
|
14874
|
-
wrap(step.do, width - pad.length - 8).forEach((line, j) => out.push(`${j ? pad : ` \u2610 ${who.padEnd(6)}`}${line}`));
|
|
14875
|
-
if (step.checks) out.push(pad + paint(ANSI.amber, i === 0 ? "checks the problem first" : "checks the problem"));
|
|
14876
|
-
});
|
|
14802
|
+
const counts = needCounts(tree);
|
|
14803
|
+
out.push(`${paint(ANSI.bold, `${persona.name}\u2019s needs`)} ${paint(ANSI.dim, `${counts.needs} needs \xB7 ${counts.difficulties + counts.precise} difficulties`)}`);
|
|
14804
|
+
if (!tree.length) {
|
|
14805
|
+
out.push("", `Nothing mapped yet. \`nanopm needs --refresh ${persona.slug}\` maps it in depth; add --short for a quick pass.`);
|
|
14806
|
+
return out.join("\n");
|
|
14877
14807
|
}
|
|
14878
|
-
const
|
|
14879
|
-
|
|
14880
|
-
|
|
14881
|
-
|
|
14882
|
-
|
|
14883
|
-
const
|
|
14884
|
-
const
|
|
14885
|
-
out.push(paint(ANSI.dim,
|
|
14886
|
-
|
|
14887
|
-
|
|
14888
|
-
|
|
14889
|
-
|
|
14890
|
-
out.push(
|
|
14808
|
+
const line = (n, depth) => {
|
|
14809
|
+
const indent = " ".repeat(depth);
|
|
14810
|
+
const lead = n.level === 1 ? `${n.emoji ?? " "} ` : `${MARK[n.provenance]} `;
|
|
14811
|
+
const where = n.provenance === "inferred" ? paint(ANSI.dim, ` guess ${n.confidence ?? 0}%`) : n.sources.length ? paint(ANSI.dim, ` ${n.sources.length} source${n.sources.length === 1 ? "" : "s"}`) : "";
|
|
14812
|
+
const state = n.status === "dropped" ? paint(ANSI.dim, " dropped") : n.status === "confirmed" || n.author_kind === "user" ? "" : paint(ANSI.amber, " to review");
|
|
14813
|
+
const label = n.status === "dropped" ? paint(ANSI.dim, n.label) : n.level === 1 ? paint(ANSI.bold, n.label) : n.label;
|
|
14814
|
+
const body = wrap(`${label}${where}${state}${n.proposal ? paint(ANSI.amber, " \xB7 NanoPM suggests a new wording") : ""}`, width - indent.length - 12);
|
|
14815
|
+
out.push(`${indent}${lead}${paint(ANSI.dim, n.ref.padEnd(8))} ${body[0] ?? ""}`);
|
|
14816
|
+
for (const more of body.slice(1)) out.push(`${indent} ${more}`);
|
|
14817
|
+
for (const c of n.children) line(c, depth + 1);
|
|
14818
|
+
};
|
|
14819
|
+
for (const need of tree) {
|
|
14820
|
+
out.push("");
|
|
14821
|
+
line(need, 0);
|
|
14891
14822
|
}
|
|
14823
|
+
out.push("", paint(ANSI.dim, `\u25CF observed \u2013 stated by you \u25CB NanoPM\u2019s guess. \`nanopm needs confirm <ref>\`, \`nanopm needs drop <ref> "why"\`, \`nanopm needs dig <ref>\`.`));
|
|
14824
|
+
if (opts.others?.length) out.push(paint(ANSI.dim, `Other personas: ${opts.others.join(", ")}.`));
|
|
14892
14825
|
return out.join("\n");
|
|
14893
14826
|
}
|
|
14894
14827
|
|
|
14895
|
-
// src/
|
|
14896
|
-
|
|
14897
|
-
|
|
14898
|
-
|
|
14899
|
-
|
|
14900
|
-
|
|
14901
|
-
};
|
|
14902
|
-
|
|
14903
|
-
const paint = painter(opts.color);
|
|
14904
|
-
if (problem.importance_basis === "evidence") {
|
|
14905
|
-
return paint(ANSI.bold, String(Math.round(problem.importance)).padStart(3));
|
|
14906
|
-
}
|
|
14907
|
-
const value = problem.importance;
|
|
14908
|
-
const word = value >= 67 ? "high" : value >= 34 ? "med" : value > 0 ? "low" : "\u2014";
|
|
14909
|
-
return paint(ANSI.dim, `~${word}`.padStart(5));
|
|
14828
|
+
// src/commands/needs.ts
|
|
14829
|
+
async function people(client, projectId) {
|
|
14830
|
+
const { data, error } = await client.from("memory").select("key, title, content").eq("project_id", projectId).eq("topic", PERSONA_TOPIC).is("superseded_by", null);
|
|
14831
|
+
if (error) throw new Error(`could not read the personas: ${error.message}`);
|
|
14832
|
+
return (data ?? []).filter((r) => PERSONA_KEY.test(r.key)).map((r) => {
|
|
14833
|
+
const p = parsePersona(r.key, r.content);
|
|
14834
|
+
return { slug: p.slug, name: personaName(r.title, p.slug), primary: p.primary };
|
|
14835
|
+
}).sort((a, b) => Number(b.primary) - Number(a.primary));
|
|
14910
14836
|
}
|
|
14911
|
-
function
|
|
14912
|
-
|
|
14913
|
-
if (!
|
|
14914
|
-
|
|
14915
|
-
|
|
14916
|
-
|
|
14917
|
-
paint(ANSI.dim, " nanopm review reads your data, then works out what they say about your users"),
|
|
14918
|
-
paint(ANSI.dim, " nanopm opportunities what to work on, for your objective")
|
|
14919
|
-
].join("\n");
|
|
14920
|
-
}
|
|
14921
|
-
const refWidth = Math.max(...problems2.map((p) => p.ref.length));
|
|
14922
|
-
const out = [];
|
|
14923
|
-
const map = groupProblemMap(problems2.map((p) => ({ ...p, parentRef: p.parent_ref })));
|
|
14924
|
-
const ordered = [...map.branches.flatMap((b) => [b.parent, ...b.children]), ...map.unplaced];
|
|
14925
|
-
for (const problem of ordered) {
|
|
14926
|
-
const shape2 = SHAPE2[problem.status] ?? { mark: "\xB7", colour: ANSI.dim, label: problem.status };
|
|
14927
|
-
const score = renderImportance(problem, opts);
|
|
14928
|
-
const head = `${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, problem.ref.padEnd(refWidth))} ${score} ${problem.parent_ref ? paint(ANSI.dim, "\u21B3 ") : ""}${clip(firstSentence(problem.statement), Math.max(40, columns() - refWidth - 14))}`;
|
|
14929
|
-
out.push(head);
|
|
14930
|
-
if (problem.mapping) out.push(paint(ANSI.dim, ` ${problem.mapping.level === 1 ? "Need" : "Difficulty"} \xB7 ${problem.mapping.knowledge} \xB7 ${problem.mapping.origin === "expert_inference" ? "expert inference" : "source analysis"}`));
|
|
14931
|
-
const indent = ` ${" ".repeat(refWidth)} `;
|
|
14932
|
-
const tail2 = problem.proofs > 0 ? `${problem.proofs} proof${problem.proofs === 1 ? "" : "s"}${problem.proof_kinds?.length ? ` \xB7 ${problem.proof_kinds.join(", ")}` : ""}` : problem.importance_note ? `no proof yet \xB7 ${problem.importance_note}` : "no proof yet";
|
|
14933
|
-
out.push(paint(ANSI.dim, indent + clip(tail2, Math.max(40, columns() - indent.length))));
|
|
14934
|
-
}
|
|
14935
|
-
const unproven = problems2.filter((p) => p.proofs === 0).length;
|
|
14936
|
-
if (unproven) {
|
|
14937
|
-
out.push("");
|
|
14938
|
-
out.push(
|
|
14939
|
-
paint(
|
|
14940
|
-
ANSI.dim,
|
|
14941
|
-
`${unproven} of ${problems2.length} rest on judgement, not proof. A tilde marks those.`
|
|
14942
|
-
)
|
|
14943
|
-
);
|
|
14944
|
-
}
|
|
14945
|
-
if (duplicates.length) {
|
|
14946
|
-
out.push("");
|
|
14947
|
-
for (const pair of duplicates.slice(0, 3)) {
|
|
14948
|
-
const why = pair.flagged ? `NanoPM is not sure ${pair.a} and ${pair.b} are different problems` : `${pair.a} and ${pair.b} read alike`;
|
|
14949
|
-
out.push(paint(ANSI.amber, `? ${why}`));
|
|
14950
|
-
}
|
|
14951
|
-
out.push(paint(ANSI.dim, ` nanopm decisions why \xB7 nanopm problems merge ${duplicates[0].b} ${duplicates[0].a}`));
|
|
14952
|
-
}
|
|
14953
|
-
if (opts.manualMerges !== void 0) {
|
|
14954
|
-
out.push("");
|
|
14955
|
-
out.push(
|
|
14956
|
-
paint(
|
|
14957
|
-
ANSI.dim,
|
|
14958
|
-
`${opts.manualMerges} merge${opts.manualMerges === 1 ? "" : "s"} you have had to make by hand.`
|
|
14959
|
-
)
|
|
14960
|
-
);
|
|
14961
|
-
}
|
|
14962
|
-
out.push("");
|
|
14963
|
-
out.push(paint(ANSI.dim, " nanopm problems <ref> \xB7 nanopm problems merge <from> <into> \xB7 nanopm problems weight <ref> <0-100>"));
|
|
14964
|
-
return out.join("\n");
|
|
14837
|
+
function pick2(list, named) {
|
|
14838
|
+
if (!list.length) throw new Error("there is no persona on the business card yet. `nanopm personas --refresh` works out who this is for.");
|
|
14839
|
+
if (!named) return list[0];
|
|
14840
|
+
const one = list.find((p) => p.slug === named.toLowerCase() || p.name.toLowerCase() === named.toLowerCase());
|
|
14841
|
+
if (!one) throw new Error(`no persona "${named}". The personas are: ${list.map((p) => p.slug).join(", ")}.`);
|
|
14842
|
+
return one;
|
|
14965
14843
|
}
|
|
14966
|
-
function
|
|
14967
|
-
|
|
14968
|
-
|
|
14969
|
-
const
|
|
14970
|
-
|
|
14971
|
-
|
|
14972
|
-
|
|
14973
|
-
|
|
14974
|
-
|
|
14975
|
-
const
|
|
14976
|
-
const
|
|
14977
|
-
|
|
14978
|
-
|
|
14979
|
-
|
|
14980
|
-
|
|
14981
|
-
|
|
14982
|
-
|
|
14983
|
-
|
|
14984
|
-
|
|
14985
|
-
|
|
14986
|
-
|
|
14987
|
-
|
|
14988
|
-
|
|
14989
|
-
|
|
14990
|
-
|
|
14991
|
-
|
|
14844
|
+
async function rows(client, projectId, persona) {
|
|
14845
|
+
let q = client.from("need_nodes").select(NEED_NODE_COLUMNS).eq("project_id", projectId);
|
|
14846
|
+
if (persona) q = q.eq("persona", persona);
|
|
14847
|
+
const { data, error } = await q.order("created_at");
|
|
14848
|
+
if (error) throw new Error(`could not read the needs map: ${error.message}`);
|
|
14849
|
+
return data ?? [];
|
|
14850
|
+
}
|
|
14851
|
+
async function needs(persona, opts) {
|
|
14852
|
+
const auth = await authenticate();
|
|
14853
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
14854
|
+
const list = await people(auth.client, project.id);
|
|
14855
|
+
const who = pick2(list, persona);
|
|
14856
|
+
const tree = needTree(await rows(auth.client, project.id, who.slug));
|
|
14857
|
+
return renderNeeds(who, tree, { color: opts.color ?? useColor(), others: list.filter((p) => p.slug !== who.slug).map((p) => p.slug) });
|
|
14858
|
+
}
|
|
14859
|
+
async function mapNeeds(target, opts) {
|
|
14860
|
+
const auth = await authenticate();
|
|
14861
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
14862
|
+
const list = await people(auth.client, project.id);
|
|
14863
|
+
let persona;
|
|
14864
|
+
let scope = "map";
|
|
14865
|
+
if (target.ref) {
|
|
14866
|
+
const ref = target.ref.toUpperCase();
|
|
14867
|
+
const node = (await rows(auth.client, project.id)).find((r) => r.ref === ref);
|
|
14868
|
+
if (!node) throw new Error(`no node ${target.ref} on any needs map. \`nanopm needs\` shows the refs.`);
|
|
14869
|
+
persona = pick2(list, node.persona);
|
|
14870
|
+
scope = ref;
|
|
14871
|
+
} else persona = pick2(list, target.persona);
|
|
14872
|
+
const [{ data: active }, tree] = await Promise.all([
|
|
14873
|
+
auth.client.from("jobs").select("params").eq("project_id", project.id).eq("skill", "needs").in("status", [...ACTIVE_STATUSES]),
|
|
14874
|
+
rows(auth.client, project.id, persona.slug).then(needTree)
|
|
14992
14875
|
]);
|
|
14993
|
-
|
|
14994
|
-
|
|
14995
|
-
|
|
14996
|
-
|
|
14997
|
-
|
|
14998
|
-
|
|
14999
|
-
|
|
15000
|
-
|
|
15001
|
-
|
|
15002
|
-
|
|
15003
|
-
|
|
15004
|
-
|
|
15005
|
-
|
|
15006
|
-
|
|
15007
|
-
|
|
15008
|
-
|
|
15009
|
-
|
|
15010
|
-
|
|
15011
|
-
|
|
15012
|
-
|
|
15013
|
-
|
|
15014
|
-
|
|
15015
|
-
|
|
14876
|
+
const lineage = (ref) => {
|
|
14877
|
+
const walk = (l) => {
|
|
14878
|
+
for (const n of l) {
|
|
14879
|
+
if (n.ref === ref.toUpperCase()) return n.path.map((p) => p.ref);
|
|
14880
|
+
const deeper = walk(n.children);
|
|
14881
|
+
if (deeper) return deeper;
|
|
14882
|
+
}
|
|
14883
|
+
return null;
|
|
14884
|
+
};
|
|
14885
|
+
return walk(tree) ?? [ref];
|
|
14886
|
+
};
|
|
14887
|
+
const clash = needsRunConflict((active ?? []).map((j) => runScopeOf(j.params)), { persona: persona.slug, scope }, { primary: list.find((p) => p.primary)?.slug ?? list[0]?.slug ?? null, lineage, name: (s) => list.find((p) => p.slug === s)?.name ?? s });
|
|
14888
|
+
if (clash) throw new Error(clash);
|
|
14889
|
+
const repo = await detectRepo(opts.cwd);
|
|
14890
|
+
if (repo) await rememberClone(auth.client, project.id, repo.root);
|
|
14891
|
+
const params = {
|
|
14892
|
+
persona: persona.slug,
|
|
14893
|
+
...scope === "map" ? {} : { scope },
|
|
14894
|
+
depth: opts.short ? "short" : "deep",
|
|
14895
|
+
...opts.note?.trim() ? { note: opts.note.trim().slice(0, 600) } : {},
|
|
14896
|
+
...repo ? { repo_path: repo.root } : {}
|
|
14897
|
+
};
|
|
14898
|
+
return runSkill("needs", { project: project.id, params, cancelSignal: opts.cancelSignal });
|
|
14899
|
+
}
|
|
14900
|
+
async function confirmNeed(ref, opts) {
|
|
14901
|
+
const auth = await authenticate();
|
|
14902
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
14903
|
+
const { data, error } = await auth.client.from("need_nodes").update({ status: "confirmed" }).eq("project_id", project.id).eq("ref", ref.toUpperCase()).eq("status", "proposed").select("ref, label");
|
|
14904
|
+
if (error) throw new Error(pgMessage(error));
|
|
14905
|
+
if (!data?.length) throw new Error(`${ref} is not a node waiting on your word. \`nanopm needs\` shows which are.`);
|
|
14906
|
+
return `Confirmed ${data[0].ref}: ${data[0].label}. A run proposes beside it from now on, never over it.`;
|
|
14907
|
+
}
|
|
14908
|
+
async function dropNeed(ref, why, opts) {
|
|
14909
|
+
const auth = await authenticate();
|
|
14910
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
14911
|
+
const { data, error } = await auth.client.from("need_nodes").update({ status: "dropped", drop_reason: why?.trim().slice(0, 400) || "not true of them" }).eq("project_id", project.id).eq("ref", ref.toUpperCase()).in("status", ["proposed", "confirmed"]).select("ref, label");
|
|
14912
|
+
if (error) throw new Error(pgMessage(error));
|
|
14913
|
+
if (!data?.length) throw new Error(`${ref} is not on the map any more.`);
|
|
14914
|
+
return `Dropped ${data[0].ref}: ${data[0].label}. It stays on the record, and NanoPM will not propose it again in the same place.`;
|
|
14915
|
+
}
|
|
14916
|
+
|
|
14917
|
+
// src/commands/opportunities.ts
|
|
14918
|
+
async function lensOf2(client, projectId) {
|
|
14919
|
+
const { data } = await client.from("memory").select("id, key, content").eq("project_id", projectId).eq("topic", GOALS_TOPIC).in("key", [OBJECTIVE_KEY, DEFINITION_KEY]).is("superseded_by", null);
|
|
14920
|
+
const rows2 = data ?? [];
|
|
14921
|
+
const aim = rows2.find((r) => r.key === OBJECTIVE_KEY);
|
|
14922
|
+
const m = aim ? /^Lever:\s*([a-z]+)\.\s*(.*)$/s.exec(aim.content.trim()) : null;
|
|
14923
|
+
const lever = m?.[1];
|
|
14924
|
+
if (!aim || !lever || !OUTCOME_LEVERS.includes(lever)) return null;
|
|
14925
|
+
const def = rows2.find((r) => r.key === DEFINITION_KEY);
|
|
14926
|
+
const parsed = def ? parseDefinition(def.content) : null;
|
|
14927
|
+
const sentence2 = m[2].trim();
|
|
14928
|
+
const definition = parsed && parsed.lever === lever ? parsed.sentence : null;
|
|
14929
|
+
return { id: aim.id, lever, sentence: sentence2, definition, key: lensKey(lever, sentence2, definition) };
|
|
14930
|
+
}
|
|
14931
|
+
async function loadOpportunities(client, projectId) {
|
|
14932
|
+
const [standing, replaced] = await Promise.all([
|
|
14933
|
+
client.from("opportunities").select(OPPORTUNITY_COLUMNS).eq("project_id", projectId).neq("status", "replaced"),
|
|
14934
|
+
client.from("opportunities").select(REPLACED_COLUMNS).eq("project_id", projectId).eq("status", "replaced")
|
|
14935
|
+
]);
|
|
14936
|
+
const error = standing.error ?? replaced.error;
|
|
14937
|
+
if (error) throw new Error(`could not read the opportunities: ${error.message}`);
|
|
14938
|
+
return [...standing.data ?? [], ...replaced.data ?? []];
|
|
14939
|
+
}
|
|
14940
|
+
async function workingOn(client, projectId) {
|
|
14941
|
+
const { data } = await client.from("jobs").select("params").eq("project_id", projectId).eq("skill", SOLUTIONS_SKILL).in("status", [...ACTIVE_STATUSES]);
|
|
14942
|
+
return leavesAtWork((data ?? []).map((j) => j.params));
|
|
14943
|
+
}
|
|
14944
|
+
async function opportunities(opts) {
|
|
14945
|
+
const auth = await authenticate();
|
|
14946
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
14947
|
+
const [lens, all, personas2] = await Promise.all([lensOf2(auth.client, project.id), loadOpportunities(auth.client, project.id), people(auth.client, project.id)]);
|
|
14948
|
+
const { shown, drawnFor } = shownMap(all, lens?.key ?? null);
|
|
14949
|
+
const names = new Map(personas2.map((p) => [p.slug, p.name]));
|
|
14950
|
+
return renderOpportunities(shown, lens, { color: opts.color ?? useColor(), previous: drawnFor === "previous", names });
|
|
14951
|
+
}
|
|
14952
|
+
async function rankOpportunities(opts) {
|
|
14953
|
+
const auth = await authenticate();
|
|
14954
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
14955
|
+
const list = (s) => s ? s.split(",").map((x) => x.trim()).filter(Boolean) : void 0;
|
|
14956
|
+
const sources2 = opts.use || opts.skip ? { ...opts.use ? { use: list(opts.use) } : {}, ...opts.skip ? { skip: list(opts.skip) } : {} } : void 0;
|
|
14957
|
+
const params = {
|
|
14958
|
+
depth: opts.deep ? "deep" : "short",
|
|
14959
|
+
...opts.note?.trim() ? { note: opts.note.trim().slice(0, OPPORTUNITIES_NOTE_MAX) } : {},
|
|
14960
|
+
...sources2 ? { sources: sources2 } : {}
|
|
14961
|
+
};
|
|
14962
|
+
const checked = opportunitiesAsk(params);
|
|
14963
|
+
if (!checked.ok) throw new Error(checked.why);
|
|
14964
|
+
const { data: active } = await auth.client.from("jobs").select("id").eq("project_id", project.id).eq("skill", "opportunities").in("status", [...ACTIVE_STATUSES]);
|
|
14965
|
+
const clash = opportunitiesRunConflict(active ?? []);
|
|
14966
|
+
if (clash) throw new Error(clash);
|
|
14967
|
+
const repo = await detectRepo(opts.cwd);
|
|
14968
|
+
if (repo) await rememberClone(auth.client, project.id, repo.root);
|
|
14969
|
+
return runSkill("opportunities", { project: project.id, params: { ...params, ...repo ? { repo_path: repo.root } : {} }, cancelSignal: opts.cancelSignal });
|
|
14970
|
+
}
|
|
14971
|
+
async function rpc2(name, args, opts) {
|
|
14972
|
+
const auth = await authenticate();
|
|
14973
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
14974
|
+
const { error } = await auth.client.rpc(name, { p_project_id: project.id, ...args });
|
|
14975
|
+
if (error) throw new Error(pgMessage(error));
|
|
14976
|
+
}
|
|
14977
|
+
async function confirmOpportunity(ref, opts) {
|
|
14978
|
+
await rpc2("confirm_opportunity", { p_ref: ref }, opts);
|
|
14979
|
+
return `${ref.toUpperCase()} is yours. A later run ranks around it and never replaces it.`;
|
|
14980
|
+
}
|
|
14981
|
+
async function setAsideOpportunity(ref, why, opts) {
|
|
14982
|
+
await rpc2("set_aside_opportunity", { p_ref: ref, p_reason: why ?? "not now" }, opts);
|
|
14983
|
+
return `${ref.toUpperCase()} is set aside for this objective, with the narrower problems under it. It comes back if the objective changes.`;
|
|
14984
|
+
}
|
|
14985
|
+
async function moveOpportunity(ref, place, opts) {
|
|
14986
|
+
const rank2 = Number(place);
|
|
14987
|
+
if (!Number.isInteger(rank2) || rank2 < 1) throw new Error("say where: nanopm opportunities move <ref> <place>, a place from 1");
|
|
14988
|
+
await rpc2("move_opportunity", { p_ref: ref, p_rank: rank2 }, opts);
|
|
14989
|
+
return `${ref.toUpperCase()} is at #${rank2} among its siblings, and the place is yours. Moved above a higher priority, it takes that priority.`;
|
|
14990
|
+
}
|
|
14991
|
+
async function rewordOpportunity(ref, problem, opts) {
|
|
14992
|
+
const text2 = problem?.trim().replace(/\s+/g, " ") ?? "";
|
|
14993
|
+
if (!text2) throw new Error('say it: nanopm opportunities reword <ref> "the problem, in your words"');
|
|
14994
|
+
if (text2.length > PROBLEM_MAX) throw new Error(`one line, under ${PROBLEM_MAX} characters`);
|
|
14995
|
+
await rpc2("restate_opportunity", { p_ref: ref, p_problem: text2 }, opts);
|
|
14996
|
+
return `Kept in your words: ${text2}`;
|
|
14997
|
+
}
|
|
14998
|
+
var AWAKE_MS = 12e4;
|
|
14999
|
+
async function awakeDaemon(client, userId) {
|
|
15000
|
+
const since = new Date(Date.now() - AWAKE_MS).toISOString();
|
|
15001
|
+
const [{ data }, local] = await Promise.all([
|
|
15002
|
+
client.from("daemons").select("id, hostname").eq("user_id", userId).in("status", ["online", "running"]).gte("last_seen_at", since).order("last_seen_at", { ascending: false }),
|
|
15003
|
+
localDaemonId()
|
|
15004
|
+
]);
|
|
15005
|
+
return data?.find((d) => d.id === local) ?? data?.[0] ?? null;
|
|
15006
|
+
}
|
|
15007
|
+
async function brainstorm(ref, opts) {
|
|
15008
|
+
const auth = await authenticate();
|
|
15009
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15010
|
+
const [lens, all, busy] = await Promise.all([lensOf2(auth.client, project.id), loadOpportunities(auth.client, project.id), workingOn(auth.client, project.id)]);
|
|
15011
|
+
const deep = Boolean(opts.deep);
|
|
15012
|
+
const plan = brainstormPlan(shownMap(all, lens?.key ?? null).shown, ref, deep, new Set(busy.keys()));
|
|
15013
|
+
const daemon = await awakeDaemon(auth.client, auth.userId);
|
|
15014
|
+
if (!daemon) throw new Error("Your machine is asleep: none of your daemons has been seen in the last two minutes, so nothing was queued. `nanopm daemon install` starts it; then brainstorm again.");
|
|
15015
|
+
const repo = deep ? await detectRepo(opts.cwd) : null;
|
|
15016
|
+
if (repo) await rememberClone(auth.client, project.id, repo.root);
|
|
15017
|
+
for (const params of plan.jobs) {
|
|
15018
|
+
const { error } = await auth.client.from("jobs").insert({ project_id: project.id, skill: SOLUTIONS_SKILL, params: { ...params, ...repo ? { repo_path: repo.root } : {} }, requested_by: auth.userId, trigger: "user", target_daemon_id: daemon.id });
|
|
15019
|
+
if (error) throw new Error(`could not queue the brainstorm: ${error.message}`);
|
|
15016
15020
|
}
|
|
15017
|
-
|
|
15018
|
-
|
|
15021
|
+
const n = plan.leaves.length;
|
|
15022
|
+
const minutes = deep ? DEEP_MINUTES * n : FLASH_MINUTES;
|
|
15023
|
+
const what = plan.problem.children.length ? `the ${n} narrower problem${n === 1 ? "" : "s"} under ${plan.problem.ref}` : `${plan.problem.ref} \u201C${clip(plan.problem.problem, 60)}\u201D`;
|
|
15024
|
+
const how = deep ? `${DEPTH_WORD.deep}${n > 1 ? ", one after the other" : ""}` : `${DEPTH_WORD.quick}, a first pass`;
|
|
15025
|
+
const out = [`${deep ? "Going deeper on" : "Brainstorming"} ${what}: ${how}, about ${minutes} minutes on ${daemon.hostname}.`];
|
|
15026
|
+
if (plan.problem.children.length) for (const l of plan.leaves) out.push(` ${l.ref.padEnd(8)} ${clip(l.problem, Math.max(30, columns() - 11))}`);
|
|
15027
|
+
if (plan.busy) out.push(`${plan.busy} left out: Nano is already finding solutions to ${plan.busy === 1 ? "it" : "them"}.`);
|
|
15028
|
+
out.push("The solutions land on `nanopm roadmap`.");
|
|
15019
15029
|
return out.join("\n");
|
|
15020
15030
|
}
|
|
15021
15031
|
|
|
15022
|
-
// src/format/
|
|
15023
|
-
var
|
|
15032
|
+
// src/format/roadmap.ts
|
|
15033
|
+
var SHAPE = {
|
|
15024
15034
|
proposed: { mark: "\u25CB", colour: ANSI.amber, label: "proposed" },
|
|
15025
15035
|
accepted: { mark: "\u25D0", colour: ANSI.green, label: "accepted" },
|
|
15026
15036
|
building: { mark: "\u25D4", colour: ANSI.blue, label: "building" },
|
|
15027
|
-
done: { mark: "\u25CF", colour: ANSI.green, label: "done" },
|
|
15028
15037
|
rejected: { mark: "\u2298", colour: ANSI.red, label: "rejected" },
|
|
15038
|
+
done: { mark: "\u25CF", colour: ANSI.green, label: "done" },
|
|
15029
15039
|
dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
|
|
15030
15040
|
};
|
|
15031
|
-
var
|
|
15032
|
-
|
|
15033
|
-
|
|
15034
|
-
|
|
15035
|
-
|
|
15041
|
+
var WORK_KINDS = /* @__PURE__ */ new Set(["branch", "git", "url", "job"]);
|
|
15042
|
+
function renderRoadmapItem(item, opts, problem, builtBy) {
|
|
15043
|
+
const paint = painter(opts.color);
|
|
15044
|
+
const shape2 = SHAPE[item.status] ?? { mark: "\xB7", colour: "", label: item.status };
|
|
15045
|
+
const out = [`${paint(shape2.colour, shape2.mark)} ${paint(ANSI.bold, item.ref)} ${item.title}`];
|
|
15046
|
+
if (problem) out.push(paint(ANSI.dim, ` answers ${problem.ref} ${clip(problem.statement, Math.max(30, columns() - 20))}`));
|
|
15047
|
+
out.push("");
|
|
15048
|
+
for (const line of wrap(item.reasoning, Math.max(40, columns() - 4))) out.push(` ${line}`);
|
|
15049
|
+
out.push("");
|
|
15050
|
+
const facts = [
|
|
15051
|
+
["status", shape2.label + (item.outcome ? ` \xB7 ${item.outcome}${item.outcome_note ? `: ${item.outcome_note}` : ""}` : "")],
|
|
15052
|
+
["rank", String(item.rank)],
|
|
15053
|
+
["who", item.actor === "founder" ? "you" : "NanoPM"],
|
|
15054
|
+
["bets on", item.assumption ?? "\u2014"],
|
|
15055
|
+
["we'll know", item.eval ?? "\u2014"]
|
|
15056
|
+
];
|
|
15057
|
+
for (const [k, v] of facts) out.push(paint(ANSI.dim, ` ${k.padEnd(9)} ${v}`));
|
|
15058
|
+
const sources2 = item.sources ?? [];
|
|
15059
|
+
const restsOn = sources2.filter((s) => !WORK_KINDS.has(s.kind));
|
|
15060
|
+
if (restsOn.length) {
|
|
15061
|
+
out.push(paint(ANSI.dim, ` ${"rests on".padEnd(9)} ${restsOn.map((s) => `${s.kind}:${s.ref}`).join(", ")}`));
|
|
15062
|
+
}
|
|
15063
|
+
const where = renderWhere(sources2);
|
|
15064
|
+
if (where) out.push(paint(ANSI.dim, ` ${"where".padEnd(9)} ${where}${builtBy ? ` \xB7 built by ${builtBy}` : ""}`));
|
|
15065
|
+
return out.join("\n");
|
|
15066
|
+
}
|
|
15067
|
+
function renderWhere(sources2) {
|
|
15068
|
+
const branch = sources2.find((s) => s.kind === "branch")?.ref;
|
|
15069
|
+
const commit = sources2.find((s) => s.kind === "git")?.ref;
|
|
15070
|
+
const url = sources2.find((s) => s.kind === "url")?.ref;
|
|
15071
|
+
return [branch, commit, url].filter(Boolean).join(" \xB7 ");
|
|
15072
|
+
}
|
|
15073
|
+
var VERB = {
|
|
15074
|
+
want: { text: "wanted", colour: ANSI.amber },
|
|
15075
|
+
confirm: { text: "confirmed", colour: ANSI.green },
|
|
15076
|
+
drop: { text: "dropped", colour: ANSI.red },
|
|
15077
|
+
merge: { text: "merged into", colour: ANSI.blue },
|
|
15078
|
+
separate: { text: "kept apart", colour: ANSI.blue },
|
|
15079
|
+
weight: { text: "weighed", colour: ANSI.blue },
|
|
15080
|
+
area: { text: "filed", colour: ANSI.blue },
|
|
15081
|
+
accept: { text: "accepted", colour: ANSI.green },
|
|
15082
|
+
reject: { text: "rejected", colour: ANSI.red },
|
|
15083
|
+
first: { text: "put first", colour: ANSI.blue },
|
|
15084
|
+
done: { text: "did", colour: ANSI.green },
|
|
15085
|
+
dismiss: { text: "dismissed", colour: ANSI.red }
|
|
15036
15086
|
};
|
|
15037
|
-
|
|
15038
|
-
var SHOWN = /* @__PURE__ */ new Set(["proposed", "accepted", "building", "done"]);
|
|
15039
|
-
function renderTree(problems2, solutions, strategy, opts) {
|
|
15087
|
+
function renderDecisions(rows2, opts) {
|
|
15040
15088
|
const paint = painter(opts.color);
|
|
15089
|
+
if (!rows2.length) return "No decisions yet.";
|
|
15041
15090
|
const out = [];
|
|
15042
|
-
const
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
}
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
if (opts.alongside && !open.length) return "";
|
|
15051
|
-
if (!open.length && !shown.length) {
|
|
15052
|
-
out.push("No problems yet. `nanopm opportunities` shows what to work on.");
|
|
15053
|
-
return out.join("\n");
|
|
15054
|
-
}
|
|
15055
|
-
const byProblem = /* @__PURE__ */ new Map();
|
|
15056
|
-
for (const s of shown) byProblem.set(s.problem_id, [...byProblem.get(s.problem_id) ?? [], s]);
|
|
15057
|
-
const refWidth = Math.max(...[...open.map((p) => p.ref), ...shown.map((s) => s.ref)].map((r) => r.length), 5);
|
|
15058
|
-
for (const [area, rows2] of groupByArea(open)) {
|
|
15059
|
-
out.push(paint(ANSI.bold, area));
|
|
15060
|
-
for (const p of rows2) {
|
|
15061
|
-
const shape2 = PROBLEM_SHAPE[p.status] ?? { mark: "\xB7", colour: "" };
|
|
15062
|
-
const kind = p.kind === "founder" ? paint(ANSI.dim, " (founder)") : "";
|
|
15063
|
-
const proof = p.proofs ? `${p.proofs} proof${p.proofs === 1 ? "" : "s"}` : "no proof";
|
|
15064
|
-
const tail2 = `${renderImportance(p, opts)} ${paint(ANSI.dim, proof)}`;
|
|
15065
|
-
const head = ` ${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, p.ref.padEnd(refWidth))} `;
|
|
15066
|
-
out.push(head + clip(p.statement, Math.max(30, width - visibleLength(head) - visibleLength(tail2) - 2)) + kind + " " + tail2);
|
|
15067
|
-
const under = byProblem.get(p.id) ?? [];
|
|
15068
|
-
if (!under.length) {
|
|
15069
|
-
out.push(paint(ANSI.dim, ` ${" ".repeat(refWidth + 2)} no solution yet`));
|
|
15070
|
-
continue;
|
|
15071
|
-
}
|
|
15072
|
-
for (const s of under) out.push(...solutionLines(s, refWidth, width, paint));
|
|
15091
|
+
for (const row of rows2) {
|
|
15092
|
+
const when = row.created_at.slice(0, 10);
|
|
15093
|
+
const verb = VERB[row.kind] ?? { text: row.kind, colour: ANSI.blue };
|
|
15094
|
+
const about = row.solutions ? `${paint(ANSI.dim, row.solutions.ref)} ${row.solutions.title}` : row.problems ? paint(ANSI.dim, row.problems.ref) : row.market_entries ? `${paint(ANSI.dim, row.market_entries.ref)} ${row.market_entries.competitor ?? ""}`.trimEnd() : "";
|
|
15095
|
+
const by = row.by ? paint(ANSI.dim, ` \xB7 by ${row.by}`) : "";
|
|
15096
|
+
out.push(`${paint(ANSI.dim, when)} ${paint(verb.colour, verb.text)} ${about}${by}`.trimEnd());
|
|
15097
|
+
if (row.body) {
|
|
15098
|
+
for (const line of wrap(row.body, 72)) out.push(paint(ANSI.dim, ` ${line}`));
|
|
15073
15099
|
}
|
|
15074
|
-
out.push("");
|
|
15075
15100
|
}
|
|
15076
|
-
|
|
15077
|
-
if (waiting) {
|
|
15078
|
-
out.push(`${waiting} waiting on you.`);
|
|
15079
|
-
out.push(paint(ANSI.dim, ' nanopm accept <ref> \xB7 nanopm reject <ref> "why" \xB7 nanopm roadmap <ref> \xB7 nanopm roadmap --board'));
|
|
15080
|
-
} else if (!opts.alongside) {
|
|
15081
|
-
out.push(paint(ANSI.dim, "Nothing waiting on you."));
|
|
15082
|
-
}
|
|
15083
|
-
return out.join("\n").trimEnd();
|
|
15101
|
+
return out.join("\n");
|
|
15084
15102
|
}
|
|
15085
|
-
|
|
15086
|
-
|
|
15087
|
-
|
|
15088
|
-
const STATE = { accepted: "chosen", building: "building", done: "done" };
|
|
15089
|
-
const tail2 = s.actor === "founder" ? "yours to do" : s.outcome ? s.outcome : s.status === "proposed" ? s.assumption ? clip(s.assumption, 40) : null : STATE[s.status] ?? null;
|
|
15090
|
-
const badge = tail2 ? paint(ANSI.dim, ` ${tail2}`) : "";
|
|
15091
|
-
const head = `${pad}${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, s.ref.padEnd(refWidth))} `;
|
|
15092
|
-
const lines = [head + clip(s.title, Math.max(30, width - visibleLength(head) - visibleLength(badge))) + badge];
|
|
15093
|
-
const reason2 = `${pad} ${" ".repeat(refWidth)} `;
|
|
15094
|
-
lines.push(paint(ANSI.dim, reason2 + clip(firstSentence(s.reasoning), Math.max(30, width - reason2.length))));
|
|
15095
|
-
return lines;
|
|
15103
|
+
var HUE = ["\x1B[34m", "\x1B[35m", "\x1B[36m", "\x1B[32m", "\x1B[95m"];
|
|
15104
|
+
function themesOf(tree) {
|
|
15105
|
+
return new Map(tree.map((b) => [b.id, { name: themeName(b), colour: HUE[themeHue(tree, b) - 1] }]));
|
|
15096
15106
|
}
|
|
15097
|
-
function
|
|
15098
|
-
|
|
15099
|
-
for (const p of problems2) {
|
|
15100
|
-
const area = p.area ?? NO_AREA;
|
|
15101
|
-
groups.set(area, [...groups.get(area) ?? [], p]);
|
|
15102
|
-
}
|
|
15103
|
-
const named = [...groups.entries()].filter(([a]) => a !== NO_AREA);
|
|
15104
|
-
const rest = groups.get(NO_AREA);
|
|
15105
|
-
return rest ? [...named, [NO_AREA, rest]] : named;
|
|
15107
|
+
function sizeWord(size, paint) {
|
|
15108
|
+
return size === "big" ? `${paint(ANSI.bold, "\u25AA")} ${SIZE_WORD.big}` : size === "small" ? paint(ANSI.dim, SIZE_WORD.small) : "";
|
|
15106
15109
|
}
|
|
15107
|
-
|
|
15108
|
-
{ status: "proposed", label: "Proposed" },
|
|
15109
|
-
{ status: "accepted", label: "Chosen" },
|
|
15110
|
-
{ status: "building", label: "Building" },
|
|
15111
|
-
{ status: "done", label: "Done" }
|
|
15112
|
-
];
|
|
15113
|
-
function renderBoard(problems2, solutions, opts) {
|
|
15110
|
+
function renderSolutionsBoard(board, opts) {
|
|
15114
15111
|
const paint = painter(opts.color);
|
|
15115
|
-
const out = [];
|
|
15116
|
-
const byId = new Map(problems2.map((p) => [p.id, p]));
|
|
15117
15112
|
const width = Math.max(40, columns());
|
|
15118
|
-
const
|
|
15119
|
-
|
|
15120
|
-
|
|
15121
|
-
|
|
15122
|
-
|
|
15123
|
-
|
|
15124
|
-
|
|
15125
|
-
|
|
15126
|
-
|
|
15127
|
-
|
|
15128
|
-
|
|
15129
|
-
|
|
15113
|
+
const themes = themesOf(board.tree);
|
|
15114
|
+
const out = [`${paint(ANSI.bold, "Roadmap")}${board.objective ? ` ${paint(ANSI.dim, `${board.objective.lever} \u2014 ${board.objective.sentence}`)}` : ""}`];
|
|
15115
|
+
const chips = board.tree.flatMap((b) => {
|
|
15116
|
+
const n = board.cards.filter((c) => c.branch.id === b.id).length;
|
|
15117
|
+
const t = themes.get(b.id);
|
|
15118
|
+
return n ? [`${paint(t.colour, "\u25CF")} ${t.name} ${paint(ANSI.dim, String(n))}`] : [];
|
|
15119
|
+
});
|
|
15120
|
+
const legend = chips.reduce((lines, chip) => {
|
|
15121
|
+
const last = lines.at(-1);
|
|
15122
|
+
if (last !== void 0 && visibleLength(last) + 3 + visibleLength(chip) <= width) lines[lines.length - 1] = `${last} ${chip}`;
|
|
15123
|
+
else lines.push(chip);
|
|
15124
|
+
return lines;
|
|
15125
|
+
}, []);
|
|
15126
|
+
out.push("", ...legend);
|
|
15127
|
+
if (!board.cards.some((c) => c.pick.placed_by === "founder")) out.push(paint(ANSI.dim, "Nano placed these in the order of your opportunity map. Move them where you want them."));
|
|
15128
|
+
const refWidth = Math.max(5, ...board.cards.map((c) => c.pick.ref.length));
|
|
15129
|
+
const gutter = " ".repeat(refWidth + 4);
|
|
15130
|
+
const titleWidth = Math.min(Math.max(...board.cards.map((c) => c.pick.title.length)), width - gutter.length - 8);
|
|
15131
|
+
for (const h of HORIZONS) {
|
|
15132
|
+
const column = board.cards.filter((c) => c.horizon === h);
|
|
15133
|
+
out.push("", `${paint(ANSI.bold, HORIZON_WORD[h])} ${paint(ANSI.dim, String(column.length))}`);
|
|
15134
|
+
if (!column.length) out.push(paint(ANSI.dim, ` Move a card here: nanopm roadmap move <ref> ${h}`));
|
|
15135
|
+
for (const c of column) {
|
|
15136
|
+
const t = themes.get(c.branch.id);
|
|
15137
|
+
out.push(` ${paint(ANSI.dim, c.pick.ref.padEnd(refWidth))} ${paint(t.colour, "\u25CF")} ${paint(ANSI.dim, t.name)}`);
|
|
15138
|
+
out.push(`${gutter}${clip(c.pick.title, titleWidth).padEnd(titleWidth)} ${sizeWord(c.pick.size, paint)}`.trimEnd());
|
|
15139
|
+
const depth = board.working.get(c.leaf.id);
|
|
15140
|
+
if (depth) out.push(`${gutter}${paint(ANSI.blue, "\u25CF")} ${paint(ANSI.dim, WORK_WORD[depth])}`);
|
|
15130
15141
|
}
|
|
15131
|
-
out.push("");
|
|
15132
15142
|
}
|
|
15133
|
-
|
|
15143
|
+
if (board.earlier) out.push("", paint(ANSI.bold, "On the problem list"), "", board.earlier);
|
|
15144
|
+
out.push(
|
|
15145
|
+
"",
|
|
15146
|
+
paint(ANSI.dim, 'nanopm roadmap <ref> \xB7 nanopm roadmap move <ref> now|next|later [position] \xB7 nanopm reject <ref> ["why"]'),
|
|
15147
|
+
paint(ANSI.dim, "nanopm opportunities brainstorm <ref> [--deep]: solutions to another problem, or deeper ones")
|
|
15148
|
+
);
|
|
15149
|
+
return out.join("\n");
|
|
15134
15150
|
}
|
|
15135
|
-
|
|
15136
|
-
// src/format/opportunities.ts
|
|
15137
|
-
function renderOpportunities(rows2, lens, opts) {
|
|
15151
|
+
function renderSolutionSheet(s, at, opts) {
|
|
15138
15152
|
const paint = painter(opts.color);
|
|
15139
15153
|
const width = Math.max(40, columns());
|
|
15154
|
+
const theme = at.branch ? themesOf(at.tree).get(at.branch.id) : void 0;
|
|
15140
15155
|
const out = [];
|
|
15141
|
-
if (
|
|
15142
|
-
|
|
15143
|
-
out.push(paint(ANSI.dim,
|
|
15144
|
-
|
|
15145
|
-
|
|
15146
|
-
|
|
15147
|
-
|
|
15156
|
+
if (theme) out.push(`${paint(theme.colour, "\u25CF")} ${paint(ANSI.dim, theme.name)}`);
|
|
15157
|
+
const size = sizeWord(s.size, paint);
|
|
15158
|
+
out.push(`${paint(ANSI.dim, s.ref)} ${paint(ANSI.bold, s.title)}${size ? ` ${size}` : ""}`);
|
|
15159
|
+
const pad = " ".repeat(12);
|
|
15160
|
+
const heading = (text2) => out.push("", ` ${paint(ANSI.bold, text2)}`);
|
|
15161
|
+
heading("Problem we are solving");
|
|
15162
|
+
if (at.leaf) {
|
|
15163
|
+
const solves = wrap(at.leaf.problem, width - 6 - at.leaf.ref.length - 2);
|
|
15164
|
+
solves[solves.length - 1] += ` ${paint(ANSI.dim, at.leaf.ref)}`;
|
|
15165
|
+
for (const line of solves) out.push(` ${line}`);
|
|
15166
|
+
if (at.branch && at.branch.id !== at.leaf.id) {
|
|
15167
|
+
for (const line of wrap(`Part of: ${at.branch.problem} ${at.branch.ref}`, width - 6)) out.push(` ${paint(ANSI.dim, line)}`);
|
|
15168
|
+
}
|
|
15169
|
+
} else {
|
|
15170
|
+
out.push(paint(ANSI.dim, " The problem it answered is no longer on the map."));
|
|
15148
15171
|
}
|
|
15149
|
-
const
|
|
15150
|
-
|
|
15151
|
-
|
|
15152
|
-
|
|
15153
|
-
|
|
15154
|
-
|
|
15155
|
-
|
|
15156
|
-
|
|
15157
|
-
|
|
15158
|
-
|
|
15159
|
-
|
|
15160
|
-
out.push(
|
|
15161
|
-
const
|
|
15162
|
-
|
|
15163
|
-
|
|
15164
|
-
|
|
15165
|
-
|
|
15166
|
-
|
|
15167
|
-
|
|
15168
|
-
|
|
15169
|
-
|
|
15170
|
-
|
|
15171
|
-
|
|
15172
|
-
|
|
15172
|
+
const described = paragraphs(s.description);
|
|
15173
|
+
if (described.length) {
|
|
15174
|
+
heading("Proposed solution");
|
|
15175
|
+
described.forEach((p, i) => {
|
|
15176
|
+
if (i) out.push("");
|
|
15177
|
+
for (const line of wrap(p, width - 6)) out.push(` ${line}`);
|
|
15178
|
+
});
|
|
15179
|
+
}
|
|
15180
|
+
const changelog = parseChangelog(s.changelog);
|
|
15181
|
+
if (changelog) {
|
|
15182
|
+
heading("Future changelog");
|
|
15183
|
+
for (const line of wrap(changelog.headline, width - 6)) out.push(` ${paint(ANSI.bold, line)}`);
|
|
15184
|
+
for (const point of changelog.points) wrap(point, width - 8).forEach((line, i) => out.push(` ${i ? " " : "\u2022"} ${line}`));
|
|
15185
|
+
}
|
|
15186
|
+
if (s.steps.length) {
|
|
15187
|
+
heading("Plan");
|
|
15188
|
+
s.steps.forEach((step, i) => {
|
|
15189
|
+
const who = STEP_WHO_WORD[step.who] ?? step.who;
|
|
15190
|
+
wrap(step.do, width - pad.length - 8).forEach((line, j) => out.push(`${j ? pad : ` \u2610 ${who.padEnd(6)}`}${line}`));
|
|
15191
|
+
if (step.checks) out.push(pad + paint(ANSI.amber, i === 0 ? "checks the problem first" : "checks the problem"));
|
|
15192
|
+
});
|
|
15193
|
+
}
|
|
15194
|
+
const card = at.leaf ? at.cards.find((c) => c.leaf.id === at.leaf.id) : void 0;
|
|
15195
|
+
const others = card ? card.others.filter((o) => o.id !== s.id) : [];
|
|
15196
|
+
out.push("");
|
|
15197
|
+
if (s.status === "rejected") out.push(paint(ANSI.dim, " Thrown out."));
|
|
15198
|
+
else if (card?.pick.id === s.id) {
|
|
15199
|
+
const column = at.cards.filter((c) => c.horizon === card.horizon);
|
|
15200
|
+
const yours = s.placed_by === "founder" ? ", where you put it" : "";
|
|
15201
|
+
out.push(paint(ANSI.dim, ` On the board: ${HORIZON_WORD[card.horizon]}, ${column.indexOf(card) + 1} of ${column.length}${yours}.`));
|
|
15202
|
+
} else out.push(paint(ANSI.dim, card ? ` Not on the board: ${card.pick.ref} is this problem's card.` : " Not on the board."));
|
|
15203
|
+
if (others.length) out.push(paint(ANSI.dim, ` Other solutions to this problem: ${others.map((o) => o.ref).join(", ")}.`));
|
|
15204
|
+
if (s.status === "proposed") {
|
|
15205
|
+
const move = card?.pick.id === s.id ? `nanopm roadmap move ${s.ref} now|next|later [position] \xB7 ` : "";
|
|
15206
|
+
out.push(paint(ANSI.dim, ` ${move}nanopm reject ${s.ref} ["why"]`));
|
|
15207
|
+
}
|
|
15208
|
+
return out.join("\n");
|
|
15209
|
+
}
|
|
15210
|
+
|
|
15211
|
+
// src/format/problems.ts
|
|
15212
|
+
var SHAPE2 = {
|
|
15213
|
+
candidate: { mark: "\u25CB", colour: ANSI.dim, label: "candidate" },
|
|
15214
|
+
confirmed: { mark: "\u25C9", colour: ANSI.amber, label: "confirmed" },
|
|
15215
|
+
addressed: { mark: "\u25CF", colour: ANSI.green, label: "addressed" },
|
|
15216
|
+
dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
|
|
15217
|
+
};
|
|
15218
|
+
function renderImportance(problem, opts) {
|
|
15219
|
+
const paint = painter(opts.color);
|
|
15220
|
+
if (problem.importance_basis === "evidence") {
|
|
15221
|
+
return paint(ANSI.bold, String(Math.round(problem.importance)).padStart(3));
|
|
15173
15222
|
}
|
|
15174
|
-
|
|
15175
|
-
|
|
15223
|
+
const value = problem.importance;
|
|
15224
|
+
const word = value >= 67 ? "high" : value >= 34 ? "med" : value > 0 ? "low" : "\u2014";
|
|
15225
|
+
return paint(ANSI.dim, `~${word}`.padStart(5));
|
|
15176
15226
|
}
|
|
15177
|
-
|
|
15178
|
-
// src/format/needs.ts
|
|
15179
|
-
var MARK = { observed: "\u25CF", stated: "\u2013", inferred: "\u25CB" };
|
|
15180
|
-
function renderNeeds(persona, tree, opts) {
|
|
15227
|
+
function renderProblems(problems2, duplicates, opts) {
|
|
15181
15228
|
const paint = painter(opts.color);
|
|
15182
|
-
|
|
15229
|
+
if (!problems2.length) {
|
|
15230
|
+
return [
|
|
15231
|
+
"No problems on the list yet.",
|
|
15232
|
+
"",
|
|
15233
|
+
paint(ANSI.dim, " nanopm review reads your data, then works out what they say about your users"),
|
|
15234
|
+
paint(ANSI.dim, " nanopm opportunities what to work on, for your objective")
|
|
15235
|
+
].join("\n");
|
|
15236
|
+
}
|
|
15237
|
+
const refWidth = Math.max(...problems2.map((p) => p.ref.length));
|
|
15183
15238
|
const out = [];
|
|
15184
|
-
const
|
|
15185
|
-
|
|
15186
|
-
|
|
15187
|
-
|
|
15188
|
-
|
|
15239
|
+
const map = groupProblemMap(problems2.map((p) => ({ ...p, parentRef: p.parent_ref })));
|
|
15240
|
+
const ordered = [...map.branches.flatMap((b) => [b.parent, ...b.children]), ...map.unplaced];
|
|
15241
|
+
for (const problem of ordered) {
|
|
15242
|
+
const shape2 = SHAPE2[problem.status] ?? { mark: "\xB7", colour: ANSI.dim, label: problem.status };
|
|
15243
|
+
const score = renderImportance(problem, opts);
|
|
15244
|
+
const head = `${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, problem.ref.padEnd(refWidth))} ${score} ${problem.parent_ref ? paint(ANSI.dim, "\u21B3 ") : ""}${clip(firstSentence(problem.statement), Math.max(40, columns() - refWidth - 14))}`;
|
|
15245
|
+
out.push(head);
|
|
15246
|
+
if (problem.mapping) out.push(paint(ANSI.dim, ` ${problem.mapping.level === 1 ? "Need" : "Difficulty"} \xB7 ${problem.mapping.knowledge} \xB7 ${problem.mapping.origin === "expert_inference" ? "expert inference" : "source analysis"}`));
|
|
15247
|
+
const indent = ` ${" ".repeat(refWidth)} `;
|
|
15248
|
+
const tail = problem.proofs > 0 ? `${problem.proofs} proof${problem.proofs === 1 ? "" : "s"}${problem.proof_kinds?.length ? ` \xB7 ${problem.proof_kinds.join(", ")}` : ""}` : problem.importance_note ? `no proof yet \xB7 ${problem.importance_note}` : "no proof yet";
|
|
15249
|
+
out.push(paint(ANSI.dim, indent + clip(tail, Math.max(40, columns() - indent.length))));
|
|
15189
15250
|
}
|
|
15190
|
-
const
|
|
15191
|
-
|
|
15192
|
-
const lead = n.level === 1 ? `${n.emoji ?? " "} ` : `${MARK[n.provenance]} `;
|
|
15193
|
-
const where = n.provenance === "inferred" ? paint(ANSI.dim, ` guess ${n.confidence ?? 0}%`) : n.sources.length ? paint(ANSI.dim, ` ${n.sources.length} source${n.sources.length === 1 ? "" : "s"}`) : "";
|
|
15194
|
-
const state = n.status === "dropped" ? paint(ANSI.dim, " dropped") : n.status === "confirmed" || n.author_kind === "user" ? "" : paint(ANSI.amber, " to review");
|
|
15195
|
-
const label = n.status === "dropped" ? paint(ANSI.dim, n.label) : n.level === 1 ? paint(ANSI.bold, n.label) : n.label;
|
|
15196
|
-
const body = wrap(`${label}${where}${state}${n.proposal ? paint(ANSI.amber, " \xB7 NanoPM suggests a new wording") : ""}`, width - indent.length - 12);
|
|
15197
|
-
out.push(`${indent}${lead}${paint(ANSI.dim, n.ref.padEnd(8))} ${body[0] ?? ""}`);
|
|
15198
|
-
for (const more of body.slice(1)) out.push(`${indent} ${more}`);
|
|
15199
|
-
for (const c of n.children) line(c, depth + 1);
|
|
15200
|
-
};
|
|
15201
|
-
for (const need of tree) {
|
|
15251
|
+
const unproven = problems2.filter((p) => p.proofs === 0).length;
|
|
15252
|
+
if (unproven) {
|
|
15202
15253
|
out.push("");
|
|
15203
|
-
|
|
15254
|
+
out.push(
|
|
15255
|
+
paint(
|
|
15256
|
+
ANSI.dim,
|
|
15257
|
+
`${unproven} of ${problems2.length} rest on judgement, not proof. A tilde marks those.`
|
|
15258
|
+
)
|
|
15259
|
+
);
|
|
15204
15260
|
}
|
|
15205
|
-
|
|
15206
|
-
|
|
15261
|
+
if (duplicates.length) {
|
|
15262
|
+
out.push("");
|
|
15263
|
+
for (const pair of duplicates.slice(0, 3)) {
|
|
15264
|
+
const why = pair.flagged ? `NanoPM is not sure ${pair.a} and ${pair.b} are different problems` : `${pair.a} and ${pair.b} read alike`;
|
|
15265
|
+
out.push(paint(ANSI.amber, `? ${why}`));
|
|
15266
|
+
}
|
|
15267
|
+
out.push(paint(ANSI.dim, ` nanopm decisions why \xB7 nanopm problems merge ${duplicates[0].b} ${duplicates[0].a}`));
|
|
15268
|
+
}
|
|
15269
|
+
if (opts.manualMerges !== void 0) {
|
|
15270
|
+
out.push("");
|
|
15271
|
+
out.push(
|
|
15272
|
+
paint(
|
|
15273
|
+
ANSI.dim,
|
|
15274
|
+
`${opts.manualMerges} merge${opts.manualMerges === 1 ? "" : "s"} you have had to make by hand.`
|
|
15275
|
+
)
|
|
15276
|
+
);
|
|
15277
|
+
}
|
|
15278
|
+
out.push("");
|
|
15279
|
+
out.push(paint(ANSI.dim, " nanopm problems <ref> \xB7 nanopm problems merge <from> <into> \xB7 nanopm problems weight <ref> <0-100>"));
|
|
15207
15280
|
return out.join("\n");
|
|
15208
15281
|
}
|
|
15209
|
-
|
|
15210
|
-
|
|
15211
|
-
|
|
15212
|
-
const
|
|
15213
|
-
|
|
15214
|
-
|
|
15215
|
-
|
|
15216
|
-
|
|
15217
|
-
|
|
15218
|
-
|
|
15219
|
-
|
|
15220
|
-
if (
|
|
15221
|
-
|
|
15222
|
-
|
|
15223
|
-
|
|
15224
|
-
|
|
15225
|
-
}
|
|
15226
|
-
|
|
15227
|
-
|
|
15228
|
-
|
|
15229
|
-
|
|
15230
|
-
|
|
15231
|
-
|
|
15232
|
-
|
|
15233
|
-
|
|
15234
|
-
|
|
15235
|
-
|
|
15236
|
-
|
|
15237
|
-
const
|
|
15238
|
-
|
|
15239
|
-
|
|
15240
|
-
|
|
15241
|
-
|
|
15242
|
-
|
|
15243
|
-
|
|
15244
|
-
|
|
15245
|
-
|
|
15246
|
-
|
|
15247
|
-
|
|
15248
|
-
|
|
15249
|
-
|
|
15250
|
-
|
|
15251
|
-
|
|
15252
|
-
|
|
15253
|
-
|
|
15254
|
-
|
|
15255
|
-
|
|
15256
|
-
|
|
15257
|
-
|
|
15258
|
-
|
|
15259
|
-
|
|
15260
|
-
|
|
15261
|
-
|
|
15262
|
-
|
|
15263
|
-
if (deeper) return deeper;
|
|
15264
|
-
}
|
|
15265
|
-
return null;
|
|
15266
|
-
};
|
|
15267
|
-
return walk(tree) ?? [ref];
|
|
15268
|
-
};
|
|
15269
|
-
const clash = needsRunConflict((active ?? []).map((j) => runScopeOf(j.params)), { persona: persona.slug, scope }, { primary: list.find((p) => p.primary)?.slug ?? list[0]?.slug ?? null, lineage, name: (s) => list.find((p) => p.slug === s)?.name ?? s });
|
|
15270
|
-
if (clash) throw new Error(clash);
|
|
15271
|
-
const repo = await detectRepo(opts.cwd);
|
|
15272
|
-
if (repo) await rememberClone(auth.client, project.id, repo.root);
|
|
15273
|
-
const params = {
|
|
15274
|
-
persona: persona.slug,
|
|
15275
|
-
...scope === "map" ? {} : { scope },
|
|
15276
|
-
depth: opts.short ? "short" : "deep",
|
|
15277
|
-
...opts.note?.trim() ? { note: opts.note.trim().slice(0, 600) } : {},
|
|
15278
|
-
...repo ? { repo_path: repo.root } : {}
|
|
15279
|
-
};
|
|
15280
|
-
return runSkill("needs", { project: project.id, params, cancelSignal: opts.cancelSignal });
|
|
15281
|
-
}
|
|
15282
|
-
async function confirmNeed(ref, opts) {
|
|
15283
|
-
const auth = await authenticate();
|
|
15284
|
-
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15285
|
-
const { data, error } = await auth.client.from("need_nodes").update({ status: "confirmed" }).eq("project_id", project.id).eq("ref", ref.toUpperCase()).eq("status", "proposed").select("ref, label");
|
|
15286
|
-
if (error) throw new Error(pgMessage(error));
|
|
15287
|
-
if (!data?.length) throw new Error(`${ref} is not a node waiting on your word. \`nanopm needs\` shows which are.`);
|
|
15288
|
-
return `Confirmed ${data[0].ref}: ${data[0].label}. A run proposes beside it from now on, never over it.`;
|
|
15289
|
-
}
|
|
15290
|
-
async function dropNeed(ref, why, opts) {
|
|
15291
|
-
const auth = await authenticate();
|
|
15292
|
-
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15293
|
-
const { data, error } = await auth.client.from("need_nodes").update({ status: "dropped", drop_reason: why?.trim().slice(0, 400) || "not true of them" }).eq("project_id", project.id).eq("ref", ref.toUpperCase()).in("status", ["proposed", "confirmed"]).select("ref, label");
|
|
15294
|
-
if (error) throw new Error(pgMessage(error));
|
|
15295
|
-
if (!data?.length) throw new Error(`${ref} is not on the map any more.`);
|
|
15296
|
-
return `Dropped ${data[0].ref}: ${data[0].label}. It stays on the record, and NanoPM will not propose it again in the same place.`;
|
|
15297
|
-
}
|
|
15298
|
-
|
|
15299
|
-
// src/commands/opportunities.ts
|
|
15300
|
-
async function lensOf2(client, projectId) {
|
|
15301
|
-
const { data } = await client.from("memory").select("id, key, content").eq("project_id", projectId).eq("topic", GOALS_TOPIC).in("key", [OBJECTIVE_KEY, DEFINITION_KEY]).is("superseded_by", null);
|
|
15302
|
-
const rows2 = data ?? [];
|
|
15303
|
-
const aim = rows2.find((r) => r.key === OBJECTIVE_KEY);
|
|
15304
|
-
const m = aim ? /^Lever:\s*([a-z]+)\.\s*(.*)$/s.exec(aim.content.trim()) : null;
|
|
15305
|
-
const lever = m?.[1];
|
|
15306
|
-
if (!aim || !lever || !OUTCOME_LEVERS.includes(lever)) return null;
|
|
15307
|
-
const def = rows2.find((r) => r.key === DEFINITION_KEY);
|
|
15308
|
-
const parsed = def ? parseDefinition(def.content) : null;
|
|
15309
|
-
const sentence2 = m[2].trim();
|
|
15310
|
-
const definition = parsed && parsed.lever === lever ? parsed.sentence : null;
|
|
15311
|
-
return { id: aim.id, lever, sentence: sentence2, definition, key: lensKey(lever, sentence2, definition) };
|
|
15312
|
-
}
|
|
15313
|
-
async function loadOpportunities(client, projectId) {
|
|
15314
|
-
const [standing, replaced] = await Promise.all([
|
|
15315
|
-
client.from("opportunities").select(OPPORTUNITY_COLUMNS).eq("project_id", projectId).neq("status", "replaced"),
|
|
15316
|
-
client.from("opportunities").select(REPLACED_COLUMNS).eq("project_id", projectId).eq("status", "replaced")
|
|
15317
|
-
]);
|
|
15318
|
-
const error = standing.error ?? replaced.error;
|
|
15319
|
-
if (error) throw new Error(`could not read the opportunities: ${error.message}`);
|
|
15320
|
-
return [...standing.data ?? [], ...replaced.data ?? []];
|
|
15321
|
-
}
|
|
15322
|
-
async function workingOn(client, projectId) {
|
|
15323
|
-
const { data } = await client.from("jobs").select("params").eq("project_id", projectId).eq("skill", SOLUTIONS_SKILL).in("status", [...ACTIVE_STATUSES]);
|
|
15324
|
-
return leavesAtWork((data ?? []).map((j) => j.params));
|
|
15325
|
-
}
|
|
15326
|
-
async function opportunities(opts) {
|
|
15327
|
-
const auth = await authenticate();
|
|
15328
|
-
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15329
|
-
const [lens, all, personas2] = await Promise.all([lensOf2(auth.client, project.id), loadOpportunities(auth.client, project.id), people(auth.client, project.id)]);
|
|
15330
|
-
const { shown, drawnFor } = shownMap(all, lens?.key ?? null);
|
|
15331
|
-
const names = new Map(personas2.map((p) => [p.slug, p.name]));
|
|
15332
|
-
return renderOpportunities(shown, lens, { color: opts.color ?? useColor(), previous: drawnFor === "previous", names });
|
|
15333
|
-
}
|
|
15334
|
-
async function rankOpportunities(opts) {
|
|
15335
|
-
const auth = await authenticate();
|
|
15336
|
-
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15337
|
-
const list = (s) => s ? s.split(",").map((x) => x.trim()).filter(Boolean) : void 0;
|
|
15338
|
-
const sources2 = opts.use || opts.skip ? { ...opts.use ? { use: list(opts.use) } : {}, ...opts.skip ? { skip: list(opts.skip) } : {} } : void 0;
|
|
15339
|
-
const params = {
|
|
15340
|
-
depth: opts.deep ? "deep" : "short",
|
|
15341
|
-
...opts.note?.trim() ? { note: opts.note.trim().slice(0, OPPORTUNITIES_NOTE_MAX) } : {},
|
|
15342
|
-
...sources2 ? { sources: sources2 } : {}
|
|
15343
|
-
};
|
|
15344
|
-
const checked = opportunitiesAsk(params);
|
|
15345
|
-
if (!checked.ok) throw new Error(checked.why);
|
|
15346
|
-
const { data: active } = await auth.client.from("jobs").select("id").eq("project_id", project.id).eq("skill", "opportunities").in("status", [...ACTIVE_STATUSES]);
|
|
15347
|
-
const clash = opportunitiesRunConflict(active ?? []);
|
|
15348
|
-
if (clash) throw new Error(clash);
|
|
15349
|
-
const repo = await detectRepo(opts.cwd);
|
|
15350
|
-
if (repo) await rememberClone(auth.client, project.id, repo.root);
|
|
15351
|
-
return runSkill("opportunities", { project: project.id, params: { ...params, ...repo ? { repo_path: repo.root } : {} }, cancelSignal: opts.cancelSignal });
|
|
15352
|
-
}
|
|
15353
|
-
async function rpc2(name, args, opts) {
|
|
15354
|
-
const auth = await authenticate();
|
|
15355
|
-
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15356
|
-
const { error } = await auth.client.rpc(name, { p_project_id: project.id, ...args });
|
|
15357
|
-
if (error) throw new Error(pgMessage(error));
|
|
15358
|
-
}
|
|
15359
|
-
async function confirmOpportunity(ref, opts) {
|
|
15360
|
-
await rpc2("confirm_opportunity", { p_ref: ref }, opts);
|
|
15361
|
-
return `${ref.toUpperCase()} is yours. A later run ranks around it and never replaces it.`;
|
|
15362
|
-
}
|
|
15363
|
-
async function setAsideOpportunity(ref, why, opts) {
|
|
15364
|
-
await rpc2("set_aside_opportunity", { p_ref: ref, p_reason: why ?? "not now" }, opts);
|
|
15365
|
-
return `${ref.toUpperCase()} is set aside for this objective, with the narrower problems under it. It comes back if the objective changes.`;
|
|
15282
|
+
function renderProblem(problem, evidence2, addressedBy, opts) {
|
|
15283
|
+
const paint = painter(opts.color);
|
|
15284
|
+
const shape2 = SHAPE2[problem.status] ?? { mark: "\xB7", colour: ANSI.dim, label: problem.status };
|
|
15285
|
+
const width = columns();
|
|
15286
|
+
const out = [];
|
|
15287
|
+
out.push(`${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, problem.ref)} ${paint(ANSI.bold, shape2.label)}`);
|
|
15288
|
+
out.push("");
|
|
15289
|
+
for (const line of wrap(problem.statement, width)) out.push(line);
|
|
15290
|
+
out.push("");
|
|
15291
|
+
const facts = [];
|
|
15292
|
+
const m = problem.mapping ?? null;
|
|
15293
|
+
if (m) {
|
|
15294
|
+
const story = problemStory(m, { proofs: problem.proofs, kinds: problem.proof_kinds ?? [], provenance: problem.provenance ?? "inferred", authorKind: problem.author_kind ?? "nanopm", createdAt: problem.created_at ?? "" });
|
|
15295
|
+
out.push(...wrap(story.story, width));
|
|
15296
|
+
out.push("");
|
|
15297
|
+
out.push(...wrap(story.doubt, width).map((l) => paint(ANSI.dim, l)));
|
|
15298
|
+
out.push(...wrap(story.assumes ? `${story.test} ${story.assumes}` : story.test, width).map((l) => paint(ANSI.dim, l)));
|
|
15299
|
+
out.push("");
|
|
15300
|
+
out.push(paint(ANSI.dim, "how I got here"));
|
|
15301
|
+
facts.push(["kind", `${m.level === 1 ? "a need" : "a difficulty"} \xB7 ${m.knowledge} \xB7 ${m.origin === "expert_inference" ? "expert inference" : "source analysis"}`]);
|
|
15302
|
+
if (m.parent_reason) facts.push(["grouping", m.parent_reason]);
|
|
15303
|
+
}
|
|
15304
|
+
if (problem.segment) facts.push(["who", problem.segment]);
|
|
15305
|
+
facts.push([
|
|
15306
|
+
"weight",
|
|
15307
|
+
problem.importance_basis === "evidence" ? `${Math.round(problem.importance)} from ${problem.proofs} piece${problem.proofs === 1 ? "" : "s"} of proof` : `${Math.round(problem.importance)}, a judgement${problem.importance_note ? ` \u2014 ${problem.importance_note}` : ""}`
|
|
15308
|
+
]);
|
|
15309
|
+
if (problem.parent_ref) facts.push(["part of", problem.parent_ref]);
|
|
15310
|
+
for (const [label, value] of facts) {
|
|
15311
|
+
out.push(`${paint(ANSI.dim, label.padEnd(9))}${clip(value, width - 9)}`);
|
|
15312
|
+
}
|
|
15313
|
+
if (evidence2.length) {
|
|
15314
|
+
out.push("");
|
|
15315
|
+
out.push(paint(ANSI.dim, "sources"));
|
|
15316
|
+
for (const item of evidence2) {
|
|
15317
|
+
const when = (item.occurred_at ?? item.created_at).slice(0, 10);
|
|
15318
|
+
const where = item.metric_key ?? item.source_ref ?? item.kind;
|
|
15319
|
+
for (const line of wrap(item.body, width - 2)) out.push(` ${line}`);
|
|
15320
|
+
out.push(paint(ANSI.dim, ` \u2014 ${item.relation ?? "supports"} \xB7 ${item.kind} \xB7 ${where} \xB7 ${when}`));
|
|
15321
|
+
}
|
|
15322
|
+
} else {
|
|
15323
|
+
out.push("");
|
|
15324
|
+
out.push(paint(ANSI.dim, "No proof yet. This ranking is a judgement, and the next review may settle it."));
|
|
15325
|
+
}
|
|
15326
|
+
if (addressedBy.length) {
|
|
15327
|
+
out.push("");
|
|
15328
|
+
out.push(paint(ANSI.dim, "addressed by"));
|
|
15329
|
+
for (const item of addressedBy) {
|
|
15330
|
+
out.push(` ${paint(ANSI.dim, item.ref)} ${clip(item.title, width - 12)} ${paint(ANSI.dim, `(${item.status})`)}`);
|
|
15331
|
+
}
|
|
15332
|
+
}
|
|
15333
|
+
out.push("");
|
|
15334
|
+
out.push(paint(ANSI.dim, " nanopm problems \xB7 nanopm roadmap"));
|
|
15335
|
+
return out.join("\n");
|
|
15366
15336
|
}
|
|
15367
|
-
|
|
15368
|
-
|
|
15369
|
-
|
|
15370
|
-
|
|
15371
|
-
|
|
15337
|
+
|
|
15338
|
+
// src/format/tree.ts
|
|
15339
|
+
var SOLUTION_SHAPE = {
|
|
15340
|
+
proposed: { mark: "\u25CB", colour: ANSI.amber, label: "proposed" },
|
|
15341
|
+
accepted: { mark: "\u25D0", colour: ANSI.green, label: "accepted" },
|
|
15342
|
+
building: { mark: "\u25D4", colour: ANSI.blue, label: "building" },
|
|
15343
|
+
done: { mark: "\u25CF", colour: ANSI.green, label: "done" },
|
|
15344
|
+
rejected: { mark: "\u2298", colour: ANSI.red, label: "rejected" },
|
|
15345
|
+
dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
|
|
15346
|
+
};
|
|
15347
|
+
var PROBLEM_SHAPE = {
|
|
15348
|
+
candidate: { mark: "\u25CB", colour: ANSI.dim },
|
|
15349
|
+
confirmed: { mark: "\u25C9", colour: ANSI.amber },
|
|
15350
|
+
addressed: { mark: "\u25CF", colour: ANSI.green },
|
|
15351
|
+
dropped: { mark: "\u2298", colour: ANSI.dim }
|
|
15352
|
+
};
|
|
15353
|
+
var NO_AREA = "no area yet";
|
|
15354
|
+
var SHOWN = /* @__PURE__ */ new Set(["proposed", "accepted", "building", "done"]);
|
|
15355
|
+
function renderTree(problems2, solutions, strategy, opts) {
|
|
15356
|
+
const paint = painter(opts.color);
|
|
15357
|
+
const out = [];
|
|
15358
|
+
const width = Math.max(40, columns());
|
|
15359
|
+
if (strategy) {
|
|
15360
|
+
out.push(paint(ANSI.bold, "Strategy"));
|
|
15361
|
+
for (const line of wrap(strategy, width - 4)) out.push(` ${line}`);
|
|
15362
|
+
out.push("");
|
|
15363
|
+
}
|
|
15364
|
+
const shown = solutions.filter((s) => SHOWN.has(s.status));
|
|
15365
|
+
const open = problems2.filter((p) => p.status !== "dropped" && (!opts.alongside || shown.some((s) => s.problem_id === p.id)));
|
|
15366
|
+
if (opts.alongside && !open.length) return "";
|
|
15367
|
+
if (!open.length && !shown.length) {
|
|
15368
|
+
out.push("No problems yet. `nanopm opportunities` shows what to work on.");
|
|
15369
|
+
return out.join("\n");
|
|
15370
|
+
}
|
|
15371
|
+
const byProblem = /* @__PURE__ */ new Map();
|
|
15372
|
+
for (const s of shown) byProblem.set(s.problem_id, [...byProblem.get(s.problem_id) ?? [], s]);
|
|
15373
|
+
const refWidth = Math.max(...[...open.map((p) => p.ref), ...shown.map((s) => s.ref)].map((r) => r.length), 5);
|
|
15374
|
+
for (const [area, rows2] of groupByArea(open)) {
|
|
15375
|
+
out.push(paint(ANSI.bold, area));
|
|
15376
|
+
for (const p of rows2) {
|
|
15377
|
+
const shape2 = PROBLEM_SHAPE[p.status] ?? { mark: "\xB7", colour: "" };
|
|
15378
|
+
const kind = p.kind === "founder" ? paint(ANSI.dim, " (founder)") : "";
|
|
15379
|
+
const proof = p.proofs ? `${p.proofs} proof${p.proofs === 1 ? "" : "s"}` : "no proof";
|
|
15380
|
+
const tail = `${renderImportance(p, opts)} ${paint(ANSI.dim, proof)}`;
|
|
15381
|
+
const head = ` ${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, p.ref.padEnd(refWidth))} `;
|
|
15382
|
+
out.push(head + clip(p.statement, Math.max(30, width - visibleLength(head) - visibleLength(tail) - 2)) + kind + " " + tail);
|
|
15383
|
+
const under = byProblem.get(p.id) ?? [];
|
|
15384
|
+
if (!under.length) {
|
|
15385
|
+
out.push(paint(ANSI.dim, ` ${" ".repeat(refWidth + 2)} no solution yet`));
|
|
15386
|
+
continue;
|
|
15387
|
+
}
|
|
15388
|
+
for (const s of under) out.push(...solutionLines(s, refWidth, width, paint));
|
|
15389
|
+
}
|
|
15390
|
+
out.push("");
|
|
15391
|
+
}
|
|
15392
|
+
const waiting = shown.filter((s) => s.status === "proposed").length;
|
|
15393
|
+
if (waiting) {
|
|
15394
|
+
out.push(`${waiting} waiting on you.`);
|
|
15395
|
+
out.push(paint(ANSI.dim, ' nanopm accept <ref> \xB7 nanopm reject <ref> "why" \xB7 nanopm roadmap <ref> \xB7 nanopm roadmap --board'));
|
|
15396
|
+
} else if (!opts.alongside) {
|
|
15397
|
+
out.push(paint(ANSI.dim, "Nothing waiting on you."));
|
|
15398
|
+
}
|
|
15399
|
+
return out.join("\n").trimEnd();
|
|
15372
15400
|
}
|
|
15373
|
-
|
|
15374
|
-
const
|
|
15375
|
-
|
|
15376
|
-
|
|
15377
|
-
|
|
15378
|
-
|
|
15401
|
+
function solutionLines(s, refWidth, width, paint, indent = 6) {
|
|
15402
|
+
const shape2 = SOLUTION_SHAPE[s.status] ?? { mark: "\xB7", colour: "", label: s.status };
|
|
15403
|
+
const pad = " ".repeat(indent);
|
|
15404
|
+
const STATE = { accepted: "chosen", building: "building", done: "done" };
|
|
15405
|
+
const tail = s.actor === "founder" ? "yours to do" : s.outcome ? s.outcome : s.status === "proposed" ? s.assumption ? clip(s.assumption, 40) : null : STATE[s.status] ?? null;
|
|
15406
|
+
const badge = tail ? paint(ANSI.dim, ` ${tail}`) : "";
|
|
15407
|
+
const head = `${pad}${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, s.ref.padEnd(refWidth))} `;
|
|
15408
|
+
const lines = [head + clip(s.title, Math.max(30, width - visibleLength(head) - visibleLength(badge))) + badge];
|
|
15409
|
+
const reason2 = `${pad} ${" ".repeat(refWidth)} `;
|
|
15410
|
+
lines.push(paint(ANSI.dim, reason2 + clip(firstSentence(s.reasoning), Math.max(30, width - reason2.length))));
|
|
15411
|
+
return lines;
|
|
15379
15412
|
}
|
|
15380
|
-
|
|
15381
|
-
|
|
15382
|
-
const
|
|
15383
|
-
|
|
15384
|
-
|
|
15385
|
-
|
|
15386
|
-
]);
|
|
15387
|
-
|
|
15413
|
+
function groupByArea(problems2) {
|
|
15414
|
+
const groups = /* @__PURE__ */ new Map();
|
|
15415
|
+
for (const p of problems2) {
|
|
15416
|
+
const area = p.area ?? NO_AREA;
|
|
15417
|
+
groups.set(area, [...groups.get(area) ?? [], p]);
|
|
15418
|
+
}
|
|
15419
|
+
const named = [...groups.entries()].filter(([a]) => a !== NO_AREA);
|
|
15420
|
+
const rest = groups.get(NO_AREA);
|
|
15421
|
+
return rest ? [...named, [NO_AREA, rest]] : named;
|
|
15388
15422
|
}
|
|
15389
|
-
|
|
15390
|
-
|
|
15391
|
-
|
|
15392
|
-
|
|
15393
|
-
|
|
15394
|
-
|
|
15395
|
-
|
|
15396
|
-
|
|
15397
|
-
const
|
|
15398
|
-
|
|
15399
|
-
|
|
15400
|
-
|
|
15401
|
-
|
|
15423
|
+
var BOARD_COLUMNS = [
|
|
15424
|
+
{ status: "proposed", label: "Proposed" },
|
|
15425
|
+
{ status: "accepted", label: "Chosen" },
|
|
15426
|
+
{ status: "building", label: "Building" },
|
|
15427
|
+
{ status: "done", label: "Done" }
|
|
15428
|
+
];
|
|
15429
|
+
function renderBoard(problems2, solutions, opts) {
|
|
15430
|
+
const paint = painter(opts.color);
|
|
15431
|
+
const out = [];
|
|
15432
|
+
const byId = new Map(problems2.map((p) => [p.id, p]));
|
|
15433
|
+
const width = Math.max(40, columns());
|
|
15434
|
+
const refWidth = Math.max(...solutions.map((s) => s.ref.length), 5);
|
|
15435
|
+
for (const col of BOARD_COLUMNS) {
|
|
15436
|
+
const rows2 = solutions.filter((s) => s.status === col.status);
|
|
15437
|
+
out.push(`${paint(ANSI.bold, col.label)} ${paint(ANSI.dim, String(rows2.length))}`);
|
|
15438
|
+
if (!rows2.length) out.push(paint(ANSI.dim, " \u2014"));
|
|
15439
|
+
for (const s of rows2) {
|
|
15440
|
+
const shape2 = SOLUTION_SHAPE[s.status];
|
|
15441
|
+
const problem = byId.get(s.problem_id);
|
|
15442
|
+
const head = ` ${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, s.ref.padEnd(refWidth))} `;
|
|
15443
|
+
out.push(head + clip(s.title, Math.max(30, width - visibleLength(head))));
|
|
15444
|
+
const under = [problem ? `${problem.ref} ${clip(problem.statement, 50)}` : "its problem was dropped", s.assumption ? clip(s.assumption, 40) : null].filter(Boolean).join(" \xB7 ");
|
|
15445
|
+
out.push(paint(ANSI.dim, ` ${" ".repeat(refWidth + 2)} ${under}`));
|
|
15446
|
+
}
|
|
15447
|
+
out.push("");
|
|
15402
15448
|
}
|
|
15403
|
-
|
|
15404
|
-
const minutes = deep ? DEEP_MINUTES * n : FLASH_MINUTES;
|
|
15405
|
-
const what = plan.problem.children.length ? `the ${n} narrower problem${n === 1 ? "" : "s"} under ${plan.problem.ref}` : `${plan.problem.ref} \u201C${clip(plan.problem.problem, 60)}\u201D`;
|
|
15406
|
-
const how = deep ? `${DEPTH_WORD.deep}${n > 1 ? ", one after the other" : ""}` : `${DEPTH_WORD.quick}, a first pass`;
|
|
15407
|
-
const out = [`${deep ? "Going deeper on" : "Brainstorming"} ${what}: ${how}, about ${minutes} minutes on ${daemon.hostname}.`];
|
|
15408
|
-
if (plan.problem.children.length) for (const l of plan.leaves) out.push(` ${l.ref.padEnd(8)} ${clip(l.problem, Math.max(30, columns() - 11))}`);
|
|
15409
|
-
if (plan.busy) out.push(`${plan.busy} left out: Nano is already finding solutions to ${plan.busy === 1 ? "it" : "them"}.`);
|
|
15410
|
-
out.push("The solutions land on `nanopm roadmap`.");
|
|
15411
|
-
return out.join("\n");
|
|
15449
|
+
return out.join("\n").trimEnd();
|
|
15412
15450
|
}
|
|
15413
15451
|
|
|
15414
15452
|
// src/commands/roadmap.ts
|
|
@@ -15568,8 +15606,8 @@ async function decide(kind, ref, body, opts) {
|
|
|
15568
15606
|
const row = Array.isArray(data) ? data[0] : void 0;
|
|
15569
15607
|
if (!row) throw new Error(`nothing changed for ${ref}`);
|
|
15570
15608
|
if (onMap) {
|
|
15571
|
-
const
|
|
15572
|
-
const after = !card ? "" :
|
|
15609
|
+
const next = card?.others[0];
|
|
15610
|
+
const after = !card ? "" : next ? ` ${next.ref} \u201C${next.title}\u201D takes its card in ${HORIZON_WORD[card.horizon]}.` : ` Its card leaves the board: nothing else answers ${card.leaf.ref} yet. \`nanopm opportunities brainstorm ${card.leaf.ref}\` finds more.`;
|
|
15573
15611
|
return `Thrown out: ${row.ref}${body?.trim() ? ` \u2014 ${body.trim()}` : ""}.${after}`;
|
|
15574
15612
|
}
|
|
15575
15613
|
const verb = kind === "accept" ? "Accepted" : "Rejected";
|
|
@@ -15586,123 +15624,36 @@ function normalizeRef(input, items) {
|
|
|
15586
15624
|
return given;
|
|
15587
15625
|
}
|
|
15588
15626
|
|
|
15589
|
-
// src/commands/
|
|
15590
|
-
async function
|
|
15627
|
+
// src/commands/build.ts
|
|
15628
|
+
async function build(ref, opts) {
|
|
15591
15629
|
const { renderer, out } = rendererFor(opts.out);
|
|
15592
15630
|
const auth = await authenticate();
|
|
15593
15631
|
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15594
|
-
const autonomy2 = await readAutonomy(auth.client, project.id);
|
|
15595
|
-
if (autonomy2 === "propose") {
|
|
15596
|
-
throw new Error(
|
|
15597
|
-
`${project.name} is on "propose", so NanoPM may look but not touch.
|
|
15598
|
-
nanopm autonomy execute_after_validation to let it change files, asking you first`
|
|
15599
|
-
);
|
|
15600
|
-
}
|
|
15601
|
-
const { data: items, error } = await auth.client.from("solutions").select("id, ref, title, rank, status").eq("project_id", project.id).is("superseded_by", null).eq("status", "accepted").eq("actor", "nanopm").order("rank");
|
|
15602
|
-
if (error) throw new Error(`could not read the solutions: ${error.message}`);
|
|
15603
|
-
const accepted = items ?? [];
|
|
15604
|
-
if (!accepted.length) {
|
|
15605
|
-
throw new Error("nothing is accepted yet. Run `nanopm roadmap`, then `nanopm accept <ref>`.");
|
|
15606
|
-
}
|
|
15607
|
-
let item = accepted[0];
|
|
15608
|
-
if (opts.ref) {
|
|
15609
|
-
const wanted = normalizeRef(opts.ref, accepted);
|
|
15610
|
-
const found = accepted.find((i) => i.ref === wanted);
|
|
15611
|
-
if (!found) {
|
|
15612
|
-
throw new Error(`${opts.ref} is not an accepted solution NanoPM can build. Accepted: ${accepted.map((i) => i.ref).join(", ")}.`);
|
|
15613
|
-
}
|
|
15614
|
-
item = found;
|
|
15615
|
-
}
|
|
15616
15632
|
const repo = await detectRepo(opts.cwd);
|
|
15617
|
-
if (!repo) throw new Error("run this inside the repository, so NanoPM
|
|
15633
|
+
if (!repo) throw new Error("run this inside the repository, so NanoPM builds from the clone you are looking at");
|
|
15634
|
+
const [daemon, local, { data: refs }] = await Promise.all([
|
|
15635
|
+
awakeDaemon(auth.client, auth.userId),
|
|
15636
|
+
localDaemonId(),
|
|
15637
|
+
auth.client.from("solutions").select("ref").eq("project_id", project.id).is("superseded_by", null)
|
|
15638
|
+
]);
|
|
15639
|
+
if (!local || daemon?.id !== local) throw new Error("This machine\u2019s daemon is asleep, so nothing was queued. `nanopm daemon install` starts it; then build again.");
|
|
15618
15640
|
await rememberClone(auth.client, project.id, repo.root);
|
|
15619
|
-
const
|
|
15620
|
-
|
|
15621
|
-
|
|
15622
|
-
|
|
15623
|
-
|
|
15624
|
-
|
|
15625
|
-
|
|
15626
|
-
})
|
|
15627
|
-
|
|
15628
|
-
out(`${item.ref} ${item.title}`);
|
|
15641
|
+
const { jobId, solution } = await queueBuild(auth.client, {
|
|
15642
|
+
projectId: project.id,
|
|
15643
|
+
userId: auth.userId,
|
|
15644
|
+
daemonId: local,
|
|
15645
|
+
ref: normalizeRef(ref, refs ?? []),
|
|
15646
|
+
repoPath: repo.root,
|
|
15647
|
+
anyway: opts.anyway
|
|
15648
|
+
});
|
|
15649
|
+
out(`${solution.ref} ${solution.title}`);
|
|
15629
15650
|
const answerer = opts.answersFile ? await answersFromFile(opts.answersFile) : answersFromTerminal(out);
|
|
15630
|
-
return streamJob(auth.client,
|
|
15631
|
-
}
|
|
15632
|
-
async function readAutonomy(client, projectId) {
|
|
15633
|
-
const { data } = await client.from("project_preferences").select("autonomy").eq("project_id", projectId).maybeSingle();
|
|
15634
|
-
return data?.autonomy ?? "propose";
|
|
15635
|
-
}
|
|
15636
|
-
var EXPLAINED = {
|
|
15637
|
-
propose: "NanoPM reads and proposes. It never changes a file.",
|
|
15638
|
-
execute_after_validation: "NanoPM changes files, on its own branch, after showing you what it will do and waiting for a yes.",
|
|
15639
|
-
execute_then_report: "NanoPM acts first and tells you after. Not available yet."
|
|
15640
|
-
};
|
|
15641
|
-
async function autonomy(level, opts) {
|
|
15642
|
-
const auth = await authenticate();
|
|
15643
|
-
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15644
|
-
const current = await readAutonomy(auth.client, project.id);
|
|
15645
|
-
if (!level) {
|
|
15646
|
-
const lines = [`${project.name} is on "${current}".`, ""];
|
|
15647
|
-
for (const l of AUTONOMY_LEVELS) {
|
|
15648
|
-
lines.push(` ${l === current ? "\u25CF" : "\u25CB"} ${l.padEnd(26)} ${EXPLAINED[l]}`);
|
|
15649
|
-
}
|
|
15650
|
-
lines.push("", " nanopm autonomy <level>");
|
|
15651
|
-
return lines.join("\n");
|
|
15652
|
-
}
|
|
15653
|
-
const wanted = level.trim();
|
|
15654
|
-
if (!AUTONOMY_LEVELS.includes(wanted)) {
|
|
15655
|
-
throw new Error(`"${level}" is not a level. Pick one of: ${AUTONOMY_LEVELS.join(", ")}.`);
|
|
15656
|
-
}
|
|
15657
|
-
if (wanted === "execute_then_report") {
|
|
15658
|
-
throw new Error(
|
|
15659
|
-
"execute_then_report is not available yet. NanoPM asks before it changes anything until the review loop has been used in anger."
|
|
15660
|
-
);
|
|
15661
|
-
}
|
|
15662
|
-
if (wanted === current) return `${project.name} is already on "${current}".`;
|
|
15663
|
-
const { error } = await auth.client.from("project_preferences").update({ autonomy: wanted }).eq("project_id", project.id);
|
|
15664
|
-
if (error) {
|
|
15665
|
-
throw new Error(`could not change the dial: ${error.message}`);
|
|
15666
|
-
}
|
|
15667
|
-
return [`${project.name} is now on "${wanted}".`, EXPLAINED[wanted]].join("\n");
|
|
15668
|
-
}
|
|
15669
|
-
async function verifyCommand(command, opts) {
|
|
15670
|
-
const auth = await authenticate();
|
|
15671
|
-
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15672
|
-
const { data } = await auth.client.from("project_preferences").select("verify_command").eq("project_id", project.id).maybeSingle();
|
|
15673
|
-
const current = data?.verify_command ?? null;
|
|
15674
|
-
if (!command && !opts.clear) {
|
|
15675
|
-
if (!current) {
|
|
15676
|
-
return [
|
|
15677
|
-
`${project.name} has no check, so nothing verifies what NanoPM writes.`,
|
|
15678
|
-
"",
|
|
15679
|
-
' nanopm verify "npm run lint && npm test"',
|
|
15680
|
-
"",
|
|
15681
|
-
"The daemon runs it in the repository root after a change and before the solution is",
|
|
15682
|
-
"marked done. A failure goes back to NanoPM once, to fix."
|
|
15683
|
-
].join("\n");
|
|
15684
|
-
}
|
|
15685
|
-
return [`${project.name} verifies with:`, "", ` ${current}`, "", ' nanopm verify "<command>" \xB7 nanopm verify --clear'].join("\n");
|
|
15686
|
-
}
|
|
15687
|
-
const next2 = opts.clear ? null : command.trim();
|
|
15688
|
-
if (next2 !== null && !next2) throw new Error('a check needs a command: nanopm verify "npm test"');
|
|
15689
|
-
const { error } = await auth.client.from("project_preferences").update({ verify_command: next2 }).eq("project_id", project.id);
|
|
15690
|
-
if (error) throw new Error(`could not save the check: ${error.message}`);
|
|
15691
|
-
if (next2 === null) return `${project.name} no longer runs a check. Nothing will verify what NanoPM writes.`;
|
|
15692
|
-
return [
|
|
15693
|
-
`${project.name} will verify with:`,
|
|
15694
|
-
"",
|
|
15695
|
-
` ${next2}`,
|
|
15696
|
-
"",
|
|
15697
|
-
"It runs in the repository root. If your change touches what the command itself runs \u2014",
|
|
15698
|
-
"package.json, a lockfile, the script it calls \u2014 the check is skipped and you are told,",
|
|
15699
|
-
"because an agent that can rewrite its own examiner is not being examined."
|
|
15700
|
-
].join("\n");
|
|
15651
|
+
return streamJob(auth.client, jobId, { pollMs: 700, out, renderer, cancelSignal: opts.cancelSignal, answerer, verbose: verboseWanted() });
|
|
15701
15652
|
}
|
|
15702
15653
|
|
|
15703
15654
|
// src/detect-sources.ts
|
|
15704
|
-
import { readFile as
|
|
15705
|
-
import { join as
|
|
15655
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
15656
|
+
import { join as join16 } from "path";
|
|
15706
15657
|
var ENV_FILES = [".env", ".env.local", ".env.development.local", ".env.development", ".env.example"];
|
|
15707
15658
|
var POSTGRES_KEYS = [
|
|
15708
15659
|
"DATABASE_URL",
|
|
@@ -15752,7 +15703,7 @@ async function envFiles(repoRoot) {
|
|
|
15752
15703
|
const out = [];
|
|
15753
15704
|
for (const file of ENV_FILES) {
|
|
15754
15705
|
try {
|
|
15755
|
-
out.push({ file, env: parseEnv(await
|
|
15706
|
+
out.push({ file, env: parseEnv(await readFile14(join16(repoRoot, file), "utf8")) });
|
|
15756
15707
|
} catch {
|
|
15757
15708
|
continue;
|
|
15758
15709
|
}
|
|
@@ -15812,7 +15763,7 @@ function describeUrl(url) {
|
|
|
15812
15763
|
// src/format/closing.ts
|
|
15813
15764
|
function closingLines(project, exit, env = process.env, mode = "onboard") {
|
|
15814
15765
|
if (exit !== 0) return [];
|
|
15815
|
-
const
|
|
15766
|
+
const build2 = mode === "start" ? `nanopm accept <ref> takes an option; when one needs code, in its repo: nanopm onboard --project ${project.id ?? "<id>"} attaches it, then nanopm build <ref> builds it` : "nanopm spec <ref> --write writes its specs, nanopm build <ref> builds it";
|
|
15816
15767
|
const chain = chainedAfter(mode).map((skill) => skillSentence(skill));
|
|
15817
15768
|
return [
|
|
15818
15769
|
"",
|
|
@@ -15820,7 +15771,7 @@ function closingLines(project, exit, env = process.env, mode = "onboard") {
|
|
|
15820
15771
|
...chain.length ? [`NanoPM is now ${joinAnd(chain)}; the Roadmap fills in as it goes. Watch it at ${webUrl(env)}/p/${project.slug}/journal`] : [],
|
|
15821
15772
|
// The map is no longer chained after onboarding, so the first three clauses are what
|
|
15822
15773
|
// builds it: what users said, where they leave traces, then the map that reads both.
|
|
15823
|
-
`Next: nanopm add <file> adds what users said \xB7 nanopm connect adds where they leave traces \xB7 nanopm opportunities run ranks what to work on \xB7 ${
|
|
15774
|
+
`Next: nanopm add <file> adds what users said \xB7 nanopm connect adds where they leave traces \xB7 nanopm opportunities run ranks what to work on \xB7 ${build2} \xB7 nanopm schedule weekly monday 7 sets the review`,
|
|
15824
15775
|
"With a partner: nanopm invite <email> prints a link; they open it, or in their clone: nanopm login, then nanopm join <code>"
|
|
15825
15776
|
];
|
|
15826
15777
|
}
|
|
@@ -16521,7 +16472,7 @@ async function seedWatches(watches2, read6) {
|
|
|
16521
16472
|
}
|
|
16522
16473
|
|
|
16523
16474
|
// src/commands/watch.ts
|
|
16524
|
-
import { readFile as
|
|
16475
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
16525
16476
|
function cursorFor(source, watchId) {
|
|
16526
16477
|
const all = source.cursor ?? {};
|
|
16527
16478
|
return all[watchId] ?? {};
|
|
@@ -16582,7 +16533,7 @@ async function addWatch(labelOrId, id, opts) {
|
|
|
16582
16533
|
const paint = painter(color);
|
|
16583
16534
|
if (!labelOrId || !id) throw new Error('say which feed and what to call it: nanopm watch add <source> <id> --query "\u2026"');
|
|
16584
16535
|
if (opts.query && opts.queryFile) throw new Error("--query or --query-file, not both");
|
|
16585
|
-
const query = opts.queryFile ? await
|
|
16536
|
+
const query = opts.queryFile ? await readFile15(opts.queryFile, "utf8") : opts.query ?? "";
|
|
16586
16537
|
const watch = watchInput(id, query, opts);
|
|
16587
16538
|
const auth = await authenticate();
|
|
16588
16539
|
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
@@ -16769,9 +16720,9 @@ async function review(opts) {
|
|
|
16769
16720
|
}
|
|
16770
16721
|
const usable = connections.filter((c) => c.status === "ok");
|
|
16771
16722
|
if (!usable.length) {
|
|
16772
|
-
const
|
|
16723
|
+
const failed2 = connections.map((c) => c.label).join(", ");
|
|
16773
16724
|
throw new Error(
|
|
16774
|
-
`no connection is working (${
|
|
16725
|
+
`no connection is working (${failed2}), so a review would have nothing to read.
|
|
16775
16726
|
nanopm connect --list see what is wrong`
|
|
16776
16727
|
);
|
|
16777
16728
|
}
|
|
@@ -17406,12 +17357,12 @@ function renderDirection(rows2) {
|
|
|
17406
17357
|
const out = [];
|
|
17407
17358
|
for (const line of DIRECTION_ROWS) {
|
|
17408
17359
|
const row = rows2.find((r) => r.key === directionKey(line));
|
|
17409
|
-
const
|
|
17360
|
+
const next = rows2.find((r) => r.key === directionKey(line, true));
|
|
17410
17361
|
out.push(`${DIRECTION_ROW_TITLE[line]}${mark(row)}`);
|
|
17411
17362
|
out.push(` ${row?.content ?? "\u2014"}`);
|
|
17412
|
-
if (
|
|
17413
|
-
const because = reasonOf2(
|
|
17414
|
-
out.push(` proposed: ${
|
|
17363
|
+
if (next) {
|
|
17364
|
+
const because = reasonOf2(next.sources);
|
|
17365
|
+
out.push(` proposed: ${next.content}${because ? ` \xB7 ${because}` : ""}`);
|
|
17415
17366
|
}
|
|
17416
17367
|
}
|
|
17417
17368
|
out.push("Objective");
|
|
@@ -17451,13 +17402,13 @@ function render2(rows2) {
|
|
|
17451
17402
|
const out = [];
|
|
17452
17403
|
for (const line of PITCH_LINE_KEYS) {
|
|
17453
17404
|
const row = rows2.find((r) => r.key === pitchKey(line));
|
|
17454
|
-
const
|
|
17405
|
+
const next = rows2.find((r) => r.key === pitchKey(line, true));
|
|
17455
17406
|
const mark2 = row && row.provenance !== "stated" ? ` \xB7 to review${row.confidence != null ? ` (${row.confidence}% sure)` : ""}` : "";
|
|
17456
17407
|
out.push(`${PITCH_LINE_TITLE[line]}${mark2}`);
|
|
17457
17408
|
out.push(` ${row?.content ?? "\u2014"}`);
|
|
17458
|
-
if (
|
|
17459
|
-
const because = reasonOf3(
|
|
17460
|
-
out.push(` proposed: ${
|
|
17409
|
+
if (next) {
|
|
17410
|
+
const because = reasonOf3(next.sources);
|
|
17411
|
+
out.push(` proposed: ${next.content}${because ? ` \xB7 ${because}` : ""}`);
|
|
17461
17412
|
}
|
|
17462
17413
|
}
|
|
17463
17414
|
return out.join("\n");
|
|
@@ -17515,7 +17466,7 @@ async function invite(email, opts) {
|
|
|
17515
17466
|
const [, ...rest] = inviteLines({ name: project.name, code, email: email.trim().toLowerCase(), web: webUrl() });
|
|
17516
17467
|
return [`Invite to ${paint(ANSI.bold, project.name)}: ${paint(ANSI.bold, code)}`, ...rest.map((l) => l ? paint(ANSI.dim, l) : l)].join("\n");
|
|
17517
17468
|
}
|
|
17518
|
-
async function
|
|
17469
|
+
async function join17(code, opts) {
|
|
17519
17470
|
const normalized2 = code.trim().toUpperCase();
|
|
17520
17471
|
if (!INVITE_CODE.test(normalized2)) throw new Error(`that does not look like an invite code: ten letters and digits, like ${"K7PQ2XM9RT"}`);
|
|
17521
17472
|
const repo = await detectRepo(opts.cwd);
|
|
@@ -18216,7 +18167,7 @@ function buildProgram() {
|
|
|
18216
18167
|
console.log(await invite(email, { cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
|
|
18217
18168
|
});
|
|
18218
18169
|
program.command("join <code>").description("Join a project you were invited to, from inside your clone of its repository").option("--no-color", "plain output").action(async (code, opts) => {
|
|
18219
|
-
console.log(await
|
|
18170
|
+
console.log(await join17(code, { cwd: process.cwd(), color: colorOf(opts) }));
|
|
18220
18171
|
});
|
|
18221
18172
|
program.command("members").description("Who is on this project, and whether their daemon is awake").option("--project <id>", "project id (default: the project of the current repo)").option("--no-color", "plain output").action(async (opts) => {
|
|
18222
18173
|
console.log(await members({ cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
|
|
@@ -18238,16 +18189,10 @@ function buildProgram() {
|
|
|
18238
18189
|
program.command("status").description("What is happening with this product: next review, what is waiting on you, whether the daemon is awake").option("--project <id>", "project id (default: the project of the current repo)").option("--no-color", "plain output").action(async (opts) => {
|
|
18239
18190
|
console.log(await status({ cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
|
|
18240
18191
|
});
|
|
18241
|
-
program.command("
|
|
18192
|
+
program.command("build <ref>").description("Build a solution from its agent spec, in a copy of this repository, to a draft pull request").option("--project <id>", "project id (default: the project of the current repo)").option("--anyway", "build although its questions for you are not answered").option("--answers <file>", "answer its questions from a file instead of the terminal").action(async (ref, opts) => {
|
|
18242
18193
|
const cancel = new AbortController();
|
|
18243
18194
|
process.once("SIGINT", () => cancel.abort());
|
|
18244
|
-
process.exitCode = await
|
|
18245
|
-
cwd: process.cwd(),
|
|
18246
|
-
projectId: opts.project,
|
|
18247
|
-
ref: ref ?? opts.ref,
|
|
18248
|
-
answersFile: opts.answers,
|
|
18249
|
-
cancelSignal: cancel.signal
|
|
18250
|
-
});
|
|
18195
|
+
process.exitCode = await build(ref, { cwd: process.cwd(), projectId: opts.project, anyway: opts.anyway, answersFile: opts.answers, cancelSignal: cancel.signal });
|
|
18251
18196
|
});
|
|
18252
18197
|
program.command("autonomy [level]").description("How far NanoPM may go on its own: read it, or set it").option("--project <id>", "project id (default: the project of the current repo)").action(async (level, opts) => {
|
|
18253
18198
|
console.log(await autonomy(level, { cwd: process.cwd(), projectId: opts.project }));
|