@nanopm/cli 0.1.8 → 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 +1309 -1335
- package/package.json +1 -1
- package/skills/build/SKILL.md +62 -0
- package/skills/build/skill.json +15 -0
- package/skills/spec/SKILL.md +2 -2
- package/skills/spec/skill.json +1 -1
- package/skills/spec-agent/SKILL.md +16 -14
- package/skills/spec-agent/skill.json +1 -1
- package/skills/spec-review/SKILL.md +24 -16
- package/skills/spec-review/skill.json +2 -2
- package/skills/{spec-brief → spec-solution}/SKILL.md +47 -29
- package/skills/spec-solution/skill.json +9 -0
- package/skills/next/SKILL.md +0 -75
- package/skills/next/skill.json +0 -8
- package/skills/spec-brief/skill.json +0 -9
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
|
}
|
|
@@ -9234,6 +9438,9 @@ function gateOne(ctx, w, leaf) {
|
|
|
9234
9438
|
return { draft, refused };
|
|
9235
9439
|
}
|
|
9236
9440
|
function draftOf(ctx, w) {
|
|
9441
|
+
return { ...draftFields(w), sources: resolve3(ctx, w.restsOn) };
|
|
9442
|
+
}
|
|
9443
|
+
function draftFields(w) {
|
|
9237
9444
|
const headline = w.changelog.headline.trim().replace(/\s+/g, " ");
|
|
9238
9445
|
const room = CHANGELOG_MAX - headline.length - 1;
|
|
9239
9446
|
return {
|
|
@@ -9243,8 +9450,7 @@ function draftOf(ctx, w) {
|
|
|
9243
9450
|
steps: w.steps.map((s) => ({ do: s.do.trim().replace(/\s+/g, " "), who: s.who, ...s.checks ? { checks: true } : {} })),
|
|
9244
9451
|
size: w.size,
|
|
9245
9452
|
assumption: clipOrNull(w.worksIf, WORKS_IF_MAX),
|
|
9246
|
-
reasoning: wholeSentences(w.why, WHY_THIS_MAX)
|
|
9247
|
-
sources: resolve3(ctx, w.restsOn)
|
|
9453
|
+
reasoning: wholeSentences(w.why, WHY_THIS_MAX)
|
|
9248
9454
|
};
|
|
9249
9455
|
}
|
|
9250
9456
|
function resolve3(ctx, ids) {
|
|
@@ -9312,14 +9518,13 @@ async function readRoadmap(client, projectId) {
|
|
|
9312
9518
|
var SPEC_SKILL = "spec";
|
|
9313
9519
|
var SPEC_MINUTES = 5;
|
|
9314
9520
|
var SPEC_STEPS = {
|
|
9315
|
-
|
|
9316
|
-
agent: { says: "Turning the
|
|
9521
|
+
solution: { says: "Writing what your users will see, and how we\u2019ll know", label: "Writing what your users will see" },
|
|
9522
|
+
agent: { says: "Turning the solution into a spec a coding agent can build from", label: "Writing the agent spec" },
|
|
9317
9523
|
review: { says: "Reading both as you would, then as a coding agent would", label: "Checking both as a coding agent would" }
|
|
9318
9524
|
};
|
|
9319
|
-
function
|
|
9320
|
-
if (!
|
|
9321
|
-
if (!
|
|
9322
|
-
if (!brief2.measure.signal.trim()) return "it does not say how we will know";
|
|
9525
|
+
function specRefusal(spec2) {
|
|
9526
|
+
if (!spec2.experience.some((s) => s.trim())) return "it does not walk through the experience";
|
|
9527
|
+
if (!spec2.measure.signal.trim()) return "it does not say how we will know";
|
|
9323
9528
|
return null;
|
|
9324
9529
|
}
|
|
9325
9530
|
function agentSpecRefusal(markdown) {
|
|
@@ -9327,19 +9532,25 @@ function agentSpecRefusal(markdown) {
|
|
|
9327
9532
|
if (!/^##\s+Acceptance criteria\b/im.test(markdown)) return "it has no acceptance criteria";
|
|
9328
9533
|
return null;
|
|
9329
9534
|
}
|
|
9330
|
-
function specsRefusal({
|
|
9331
|
-
const b =
|
|
9332
|
-
if (b) return `the
|
|
9535
|
+
function specsRefusal({ spec: spec2, markdown }) {
|
|
9536
|
+
const b = specRefusal(spec2);
|
|
9537
|
+
if (b) return `the spec ${b}`;
|
|
9333
9538
|
const a = agentSpecRefusal(markdown);
|
|
9334
9539
|
return a ? `the agent spec ${a}` : null;
|
|
9335
9540
|
}
|
|
9541
|
+
function specRecord(mode, answer) {
|
|
9542
|
+
if (mode === "write") return { spec: answer.spec, markdown: answer.markdown, solution: null };
|
|
9543
|
+
if (!answer.solution) throw new Error("a rework writes the whole solution again, and this one came back without it");
|
|
9544
|
+
return { spec: answer.spec, markdown: answer.markdown, solution: answer.solution };
|
|
9545
|
+
}
|
|
9336
9546
|
function specSolutionOf(params) {
|
|
9337
9547
|
const id = params?.solution;
|
|
9338
9548
|
return typeof id === "string" ? id : null;
|
|
9339
9549
|
}
|
|
9340
9550
|
async function queueSpecs(client, ask7) {
|
|
9551
|
+
const mode = ask7.mode ?? "write";
|
|
9341
9552
|
const [{ data: solution }, { data: active }, { data: clone }] = await Promise.all([
|
|
9342
|
-
client.from("solutions").select("id, ref, status, opportunity_id").eq("project_id", ask7.projectId).eq("ref", ask7.ref.trim().toUpperCase()).maybeSingle(),
|
|
9553
|
+
client.from("solutions").select("id, ref, status, opportunity_id, specced_at").eq("project_id", ask7.projectId).eq("ref", ask7.ref.trim().toUpperCase()).maybeSingle(),
|
|
9343
9554
|
client.from("jobs").select("params").eq("project_id", ask7.projectId).eq("skill", SPEC_SKILL).in("status", [...ACTIVE_STATUSES]),
|
|
9344
9555
|
// The clone on the machine that will run it: the daemon reads the code only in its own (`resolveCwd`).
|
|
9345
9556
|
client.from("clones").select("repo_path").eq("project_id", ask7.projectId).eq("daemon_id", ask7.daemonId).maybeSingle()
|
|
@@ -9347,13 +9558,15 @@ async function queueSpecs(client, ask7) {
|
|
|
9347
9558
|
if (!solution || !solution.opportunity_id) throw new Error(`No solution ${ask7.ref.toUpperCase()} on the Roadmap.`);
|
|
9348
9559
|
if (solution.status === "rejected") throw new Error(`${solution.ref} was thrown out: it gets no specs.`);
|
|
9349
9560
|
if ((active ?? []).some((j) => specSolutionOf(j.params) === solution.id)) throw new Error(`Nano is already writing the specs of ${solution.ref}.`);
|
|
9561
|
+
if (mode === "write" && solution.specced_at) throw new Error(`${solution.ref} has its specs already: rework the solution to write it all again.`);
|
|
9562
|
+
if (mode === "rework" && !solution.specced_at) throw new Error(`${solution.ref} has no specs yet: write them first.`);
|
|
9350
9563
|
const { data, error } = await client.from("jobs").insert({
|
|
9351
9564
|
project_id: ask7.projectId,
|
|
9352
9565
|
skill: SPEC_SKILL,
|
|
9353
9566
|
requested_by: ask7.userId,
|
|
9354
9567
|
trigger: "user",
|
|
9355
9568
|
target_daemon_id: ask7.daemonId,
|
|
9356
|
-
params: { solution: solution.id, ...clone?.repo_path ? { repo_path: clone.repo_path } : {} }
|
|
9569
|
+
params: { solution: solution.id, ...mode === "rework" ? { rework: true } : {}, ...clone?.repo_path ? { repo_path: clone.repo_path } : {} }
|
|
9357
9570
|
}).select("id").single();
|
|
9358
9571
|
if (error?.code === "23505") throw new Error(`Nano is already writing the specs of ${solution.ref}.`);
|
|
9359
9572
|
if (error || !data) throw new Error(error?.message ?? "could not ask Nano for the specs");
|
|
@@ -9364,40 +9577,30 @@ async function queueSpecs(client, ask7) {
|
|
|
9364
9577
|
import { z as z15 } from "zod";
|
|
9365
9578
|
var SpecParams = z15.object({
|
|
9366
9579
|
solution: z15.guid(),
|
|
9580
|
+
/** *Rework the solution* (spec 74): the whole solution written again, as a new version. */
|
|
9581
|
+
rework: z15.literal(true).optional(),
|
|
9367
9582
|
repo_path: z15.string().optional(),
|
|
9368
9583
|
background: z15.boolean().optional()
|
|
9369
9584
|
}).strict();
|
|
9370
9585
|
function specAsk(params) {
|
|
9371
9586
|
const parsed = SpecParams.safeParse(params);
|
|
9372
|
-
if (parsed.success) return { ok: true, solution: parsed.data.solution };
|
|
9587
|
+
if (parsed.success) return { ok: true, solution: parsed.data.solution, mode: parsed.data.rework ? "rework" : "write" };
|
|
9373
9588
|
const issue = parsed.error.issues[0];
|
|
9374
9589
|
const unknown2 = issue.code === "unrecognized_keys" ? issue.keys?.join(", ") : null;
|
|
9375
9590
|
return { ok: false, why: unknown2 ? `this run does not take ${unknown2}` : `${issue.path.join(".") || "params"}: ${issue.message}` };
|
|
9376
9591
|
}
|
|
9377
|
-
var
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
fork: z15.object({
|
|
9381
|
-
after: z15.number().int().describe("The step the path forks after, counted from 1."),
|
|
9382
|
-
when: prose(1).describe('When it forks, a few words: "No closed search".'),
|
|
9383
|
-
then: prose(1).describe("Where it goes instead, a few words.")
|
|
9384
|
-
}).nullable().default(null).describe("One fork at most, only when the path really splits.")
|
|
9592
|
+
var Diagram = z15.object({
|
|
9593
|
+
caption: prose(1).describe('What it shows, a few words: "Starting the trial on a closed search".'),
|
|
9594
|
+
mermaid: z15.string().min(1).max(4e3).describe("The diagram in Mermaid syntax, whichever type helps most: flowchart, sequenceDiagram, stateDiagram-v2, journey, or a flowchart with two subgraphs for before and after. At most about twelve nodes, labels of a few words in quotes. No styling: no classDef, style, colours, click or links.")
|
|
9385
9595
|
});
|
|
9386
|
-
var
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
});
|
|
9391
|
-
var BriefFields = {
|
|
9392
|
-
questions: z15.array(z15.object({ ask: prose(1).describe("The question, one sentence."), blocks: prose(1).describe('What it blocks, a few words: "what the trial screen says".') })).max(5).default([]).describe("To finish this brief: up to three questions only the founder can answer. Decide everything else yourself."),
|
|
9393
|
-
in_short: prose(1).describe("In short: three sentences at most \u2014 what we build, for whom, what changes for them. The first could be read alone."),
|
|
9394
|
-
diagrams: z15.array(z15.discriminatedUnion("kind", [Journey, BeforeAfter])).max(2).default([]).describe("At most two. The journey whenever the experience has three steps or more; before and after when the solution changes something the product already does."),
|
|
9596
|
+
var SpecFields = {
|
|
9597
|
+
questions: z15.array(z15.object({ ask: prose(1).describe("The question, one sentence."), blocks: prose(1).describe('What it blocks, a few words: "what the trial screen says".') })).max(5).default([]).describe("Questions for you: up to three questions only the founder can answer, and any place the code shows the solution as settled is wrong. Decide everything else yourself."),
|
|
9598
|
+
in_short: prose(1).describe("In short: three sentences at most \u2014 what we build, for whom, what changes for them. The first could be read alone. Read first, right after the problem: not the proposed solution again, its gist."),
|
|
9599
|
+
diagrams: z15.array(Diagram).max(2).default([]).describe("Optional: only when a drawing makes it faster to understand than the words. At most two, in Mermaid, the shape that helps most."),
|
|
9395
9600
|
experience: z15.array(prose(1)).max(8).describe("The experience: three to six steps, in the present tense, from the user\u2019s side of the screen. Under a hypothesis, the founder\u2019s check comes first, in one line."),
|
|
9396
|
-
build: z15.array(prose(1)).max(8).describe("What we build: three to six points, in users\u2019 words \u2014 what is true the day it ships."),
|
|
9397
9601
|
wont_do: z15.array(z15.object({ what: prose(1), why: prose(1) })).max(6).default([]).describe("What we won\u2019t do: two to four points, each with why or when."),
|
|
9398
9602
|
measure: z15.object({
|
|
9399
9603
|
signal: prose(1).describe("The signal, one sentence: who \xB7 does what \xB7 how many \xB7 by when. Small numbers for a product with few users. Never a baseline you did not measure."),
|
|
9400
|
-
wrong_if: prose(1).describe("We were wrong if\u2026, one sentence. Under a hypothesis, it can show the problem itself wrong."),
|
|
9401
9604
|
existing: z15.array(z15.object({ name: prose(1).describe("The event as the product records it, exactly."), counts: prose(1).describe("What it counts here, and where you found it.") })).max(8).default([]).describe("Events the product already records that measure this, found in the code or in the connected source. Only what you found."),
|
|
9402
9605
|
added: z15.array(
|
|
9403
9606
|
z15.object({
|
|
@@ -9411,22 +9614,26 @@ var BriefFields = {
|
|
|
9411
9614
|
dependencies: z15.array(prose(1)).max(6).default([]).describe('What must exist first. Empty when nothing: the page says None. No analytics tool in the product: "An analytics tool" is one.')
|
|
9412
9615
|
};
|
|
9413
9616
|
var Notes2 = prose().nullable().default(null).describe("A sentence or two, for the Journal.");
|
|
9414
|
-
var
|
|
9415
|
-
|
|
9416
|
-
|
|
9617
|
+
var Code = z15.array(z15.object({ path: z15.string().max(200), holds: prose(1).describe("What it holds that matters here, one line.") })).max(16).default([]).describe("The files you read that matter for this change, each with what it holds, for the agent spec\u2019s *Start here*. Empty when the code was not there.");
|
|
9618
|
+
var Rewritten = Written.omit({ restsOn: true });
|
|
9619
|
+
var SolutionAnswer = z15.object({
|
|
9620
|
+
solution: Rewritten.nullable().default(null).describe('Only when the snapshot says mode "rework": the whole solution, written again. Null in mode "write": those fields are settled.'),
|
|
9621
|
+
...SpecFields,
|
|
9622
|
+
code: Code,
|
|
9417
9623
|
notes: Notes2
|
|
9418
9624
|
});
|
|
9625
|
+
var AgentMarkdown = z15.string().min(1).max(6e4);
|
|
9419
9626
|
var AgentAnswer = z15.object({
|
|
9420
|
-
markdown:
|
|
9627
|
+
markdown: AgentMarkdown.describe("The agent spec, whole, in markdown, with the sections named in your instructions."),
|
|
9421
9628
|
notes: Notes2
|
|
9422
9629
|
});
|
|
9423
9630
|
var ReviewAnswer = z15.object({
|
|
9424
|
-
|
|
9425
|
-
markdown:
|
|
9631
|
+
spec: z15.object(SpecFields).describe("The solution\u2019s sections, whole, corrected where they failed a reader; as they were where they did not."),
|
|
9632
|
+
markdown: AgentMarkdown.describe("The agent spec, whole, corrected; as it was where it did not fail."),
|
|
9426
9633
|
exchanges: z15.array(z15.object({ asked: prose(1).describe("What the coding agent would have had to guess, as its question, one short sentence."), did: prose(1).describe("What you did: decided it (and what), or left it to the founder.") })).max(5).default([]).describe("Two to five: the questions a coding agent asked of the spec, and what you did about each. They go in the Journal."),
|
|
9427
9634
|
changed: z15.array(z15.string().max(200)).max(10).default([]).describe("What you changed, a line each, for the Journal.")
|
|
9428
9635
|
});
|
|
9429
|
-
var SCHEMAS7 = {
|
|
9636
|
+
var SCHEMAS7 = { solution: SolutionAnswer, agent: AgentAnswer, review: ReviewAnswer };
|
|
9430
9637
|
function parseSpecAnswer(role7, json2) {
|
|
9431
9638
|
return SCHEMAS7[role7].parse(json2);
|
|
9432
9639
|
}
|
|
@@ -9435,32 +9642,32 @@ function specOutputSchema(role7) {
|
|
|
9435
9642
|
}
|
|
9436
9643
|
|
|
9437
9644
|
// ../daemon/src/git.ts
|
|
9438
|
-
import { execFile as
|
|
9439
|
-
import { promisify as
|
|
9440
|
-
var
|
|
9441
|
-
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;
|
|
9442
9649
|
var WIKI_DIR = ".nanopm/wiki/docs";
|
|
9443
|
-
async function
|
|
9444
|
-
const { stdout } = await
|
|
9650
|
+
async function git3(cwd, args) {
|
|
9651
|
+
const { stdout } = await execFileAsync5("git", ["-C", cwd, ...args], { maxBuffer: MAX_BUFFER2, timeout: 2e4 });
|
|
9445
9652
|
return stdout.trim();
|
|
9446
9653
|
}
|
|
9447
9654
|
async function listTrackedFiles(cwd) {
|
|
9448
|
-
const lines = (args) =>
|
|
9655
|
+
const lines = (args) => git3(cwd, args).then((out) => out ? out.split("\n") : [], () => []);
|
|
9449
9656
|
const [files, wiki] = await Promise.all([lines(["ls-files"]), lines(["ls-files", "--others", "--ignored", "--exclude-standard", "--", WIKI_DIR])]);
|
|
9450
9657
|
return { files, wiki };
|
|
9451
9658
|
}
|
|
9452
9659
|
async function headCommit(cwd) {
|
|
9453
|
-
return
|
|
9660
|
+
return git3(cwd, ["rev-parse", "--short", "HEAD"]).then((sha) => sha || null, () => null);
|
|
9454
9661
|
}
|
|
9455
9662
|
async function collectGitDigest(cwd, recent = 30) {
|
|
9456
9663
|
try {
|
|
9457
|
-
await
|
|
9664
|
+
await git3(cwd, ["rev-parse", "--git-dir"]);
|
|
9458
9665
|
} catch {
|
|
9459
9666
|
return null;
|
|
9460
9667
|
}
|
|
9461
9668
|
const safe = async (args) => {
|
|
9462
9669
|
try {
|
|
9463
|
-
return await
|
|
9670
|
+
return await git3(cwd, args);
|
|
9464
9671
|
} catch {
|
|
9465
9672
|
return "";
|
|
9466
9673
|
}
|
|
@@ -9557,6 +9764,7 @@ async function runSpecsJob({ client, job, adapter, spec: spec2, onEvent }) {
|
|
|
9557
9764
|
const solution = row;
|
|
9558
9765
|
if (!solution?.opportunity_id) throw new Error("That solution is not on the Roadmap.");
|
|
9559
9766
|
if (solution.status === "rejected") throw new Error(`${solution.ref} was thrown out: it gets no specs.`);
|
|
9767
|
+
const mode = asked.mode;
|
|
9560
9768
|
const clone = typeof job.params?.repo_path === "string";
|
|
9561
9769
|
const [all, card, stance, numbers, connections, commit] = await Promise.all([
|
|
9562
9770
|
readOpportunities(client, projectId),
|
|
@@ -9571,16 +9779,8 @@ async function runSpecsJob({ client, job, adapter, spec: spec2, onEvent }) {
|
|
|
9571
9779
|
const path = leaf ? pathOf(tree, leaf.id) : [];
|
|
9572
9780
|
const known = {
|
|
9573
9781
|
today: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
title: solution.title,
|
|
9577
|
-
proposed_solution: solution.description,
|
|
9578
|
-
future_changelog: parseChangelog(solution.changelog),
|
|
9579
|
-
plan: solution.steps,
|
|
9580
|
-
size: solution.size,
|
|
9581
|
-
works_if: solution.assumption,
|
|
9582
|
-
why: solution.reasoning
|
|
9583
|
-
},
|
|
9782
|
+
mode,
|
|
9783
|
+
solution: snapshotOf({ ref: solution.ref, title: solution.title, description: solution.description, changelog: solution.changelog ?? "", steps: solution.steps, size: solution.size ?? "small", assumption: solution.assumption, reasoning: solution.reasoning }),
|
|
9584
9784
|
problem: leaf ? problemOf(tree, leaf) : null,
|
|
9585
9785
|
part_of: path.slice(1).map((p) => p.problem),
|
|
9586
9786
|
objective: lensOf(card.goals),
|
|
@@ -9590,27 +9790,37 @@ async function runSpecsJob({ client, job, adapter, spec: spec2, onEvent }) {
|
|
|
9590
9790
|
numbers
|
|
9591
9791
|
};
|
|
9592
9792
|
const reads = clone ? `the code read at ${commit ?? "its current commit"}` : "no clone here, so the code is not read";
|
|
9593
|
-
|
|
9793
|
+
const doing = mode === "rework" ? "Reworking" : "Writing the specs of";
|
|
9794
|
+
await journal("spec.read", `${doing} \u201C${clip3(solution.title, 80)}\u201D (${solution.ref}): ${reads}; ${connections.length ? `${connections.length} connected source${connections.length === 1 ? "" : "s"}` : "no analytics connected"}.`);
|
|
9594
9795
|
const ctx = { adapter, spec: spec2, onEvent };
|
|
9595
9796
|
const timings = [];
|
|
9596
|
-
const time = (
|
|
9797
|
+
const time = (what2, cost) => timings.push({ what: what2, ms: cost.ms });
|
|
9597
9798
|
const repo = clone ? { repository: "the working directory", why_no_repository: null } : { repository: null, why_no_repository: "No clone of the product rode along with this run: work from what the snapshot says the product does." };
|
|
9598
9799
|
const writerGrant = [...clone ? ["read_repo"] : [], ...connections.length ? ["read_data"] : []];
|
|
9599
|
-
const
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9800
|
+
const writing = { grant: writerGrant, prompt: { ...known, ...repo, connections } };
|
|
9801
|
+
const written = await ask3(ctx, "solution", writing);
|
|
9802
|
+
time("solution", written.cost);
|
|
9803
|
+
const { solution: rewritten, code, notes, ...draft } = written.answer;
|
|
9804
|
+
const fields = mode === "rework" && rewritten ? draftFields(rewritten) : null;
|
|
9805
|
+
const wrote = fields ? `Solution written again: \u201C${clip3(fields.title, 80)}\u201D` : `Sections written: ${count(draft.experience.length, "step")} of the experience`;
|
|
9806
|
+
await step("spec.solution", `${wrote}, ${count(draft.questions.length, "question")} for you, ${count(draft.measure.added.length, "new event")}.${notes ? ` ${notes}` : ""}`, written.cost);
|
|
9807
|
+
const whole = fields ? snapshotOf({ ref: solution.ref, ...fields }) : known.solution;
|
|
9808
|
+
const first = specRecord(mode, { spec: draft, markdown: "", solution: fields });
|
|
9809
|
+
const unreadable = specRefusal(first.spec) ?? (first.solution && leaf ? solutionRefusal(first.solution, leaf) : null);
|
|
9810
|
+
if (unreadable) throw new Error(`The solution cannot be recorded: ${unreadable}.`);
|
|
9811
|
+
const target = await recordSections(client, { projectId, jobId: job.id, solution, spec: first.spec, fields: first.solution });
|
|
9812
|
+
await journal("spec.sections", first.solution ? `${target.ref} \u201C${clip3(first.solution.title, 80)}\u201D takes ${solution.ref}\u2019s place on the board; its agent spec is on its way.` : `Sections on the sheet; the agent spec is on its way.`);
|
|
9603
9813
|
const agent = await ask3(ctx, "agent", {
|
|
9604
9814
|
grant: clone ? ["read_repo"] : [],
|
|
9605
|
-
prompt: { today: known.today, ref:
|
|
9815
|
+
prompt: { today: known.today, ref: target.ref, commit: commit ?? "not read", ...repo, solution: whole, spec: draft, code, problem: known.problem, product: known.product, constraints: stance }
|
|
9606
9816
|
});
|
|
9607
9817
|
time("agent spec", agent.cost);
|
|
9608
9818
|
await step("spec.agent", `Agent spec written.${agent.answer.notes ? ` ${agent.answer.notes}` : ""}`, agent.cost);
|
|
9609
|
-
let final = {
|
|
9819
|
+
let final = { spec: first.spec, markdown: agent.answer.markdown };
|
|
9610
9820
|
try {
|
|
9611
|
-
const review2 = await ask3(ctx, "review", { prompt: {
|
|
9821
|
+
const review2 = await ask3(ctx, "review", { prompt: { mode, solution: whole, spec: draft, markdown: agent.answer.markdown, problem: known.problem } });
|
|
9612
9822
|
time("review", review2.cost);
|
|
9613
|
-
const reviewed = {
|
|
9823
|
+
const reviewed = { spec: review2.answer.spec, markdown: review2.answer.markdown };
|
|
9614
9824
|
const refused2 = specsRefusal(reviewed);
|
|
9615
9825
|
if (refused2) await journal("spec.review_kept_out", `The review\u2019s version was left out (${refused2}): the writers\u2019 drafts are recorded.`);
|
|
9616
9826
|
else final = reviewed;
|
|
@@ -9624,19 +9834,15 @@ async function runSpecsJob({ client, job, adapter, spec: spec2, onEvent }) {
|
|
|
9624
9834
|
await journal("spec.review_failed", `The review did not finish (${clip3(errorMessage(err), 200)}): the writers\u2019 drafts are recorded.`);
|
|
9625
9835
|
}
|
|
9626
9836
|
const refused = specsRefusal(final);
|
|
9627
|
-
if (refused) throw new Error(`The specs cannot be
|
|
9628
|
-
const { error: published } = await client.rpc("
|
|
9629
|
-
p_project_id: projectId,
|
|
9630
|
-
p_solution_id: solution.id,
|
|
9631
|
-
p_brief: final.brief,
|
|
9632
|
-
p_agent_spec: final.markdown
|
|
9633
|
-
});
|
|
9837
|
+
if (refused) throw new Error(`The specs cannot be recorded: ${refused}.`);
|
|
9838
|
+
const { error: published } = await client.rpc("publish_solution_spec", { p_project_id: projectId, p_solution_id: target.id, p_spec: final.spec, p_agent_spec: final.markdown });
|
|
9634
9839
|
if (published) throw new Error(`could not record the specs: ${published.message}`);
|
|
9635
|
-
const
|
|
9840
|
+
const what = first.solution ? `\u201C${clip3(solution.title, 80)}\u201D (${solution.ref}) reworked as ${target.ref} \u201C${clip3(first.solution.title, 80)}\u201D` : `Specs of \u201C${clip3(solution.title, 80)}\u201D (${solution.ref}) written`;
|
|
9841
|
+
const summary = `${what}: ${count(final.spec.questions.length, "question")} for you, and the agent spec. Took ${duration(Date.now() - started)}: ${timings.map((t) => `${t.what} ${duration(t.ms)}`).join(", ")}.`;
|
|
9636
9842
|
await journal("spec.summary", summary);
|
|
9637
9843
|
return { status: "succeeded", result: summary };
|
|
9638
9844
|
}
|
|
9639
|
-
var ROLE_NAME = {
|
|
9845
|
+
var ROLE_NAME = { solution: "spec solution writer", agent: "spec agent writer", review: "spec reviewer" };
|
|
9640
9846
|
async function ask3(ctx, role7, call2) {
|
|
9641
9847
|
const cost = await runRole({
|
|
9642
9848
|
adapter: ctx.adapter,
|
|
@@ -9653,6 +9859,33 @@ async function ask3(ctx, role7, call2) {
|
|
|
9653
9859
|
});
|
|
9654
9860
|
return { answer: stripDeep(parseSpecAnswer(role7, JSON.parse(cost.text))), cost };
|
|
9655
9861
|
}
|
|
9862
|
+
async function recordSections(client, r) {
|
|
9863
|
+
if (!r.fields) {
|
|
9864
|
+
const { error: error2 } = await client.rpc("publish_solution_spec", { p_project_id: r.projectId, p_solution_id: r.solution.id, p_spec: r.spec });
|
|
9865
|
+
if (error2) throw new Error(`could not record the sections: ${error2.message}`);
|
|
9866
|
+
return { id: r.solution.id, ref: r.solution.ref };
|
|
9867
|
+
}
|
|
9868
|
+
const f = r.fields;
|
|
9869
|
+
const { data, error } = await client.rpc("rework_spec", {
|
|
9870
|
+
p_project_id: r.projectId,
|
|
9871
|
+
p_solution_id: r.solution.id,
|
|
9872
|
+
p_job_id: r.jobId,
|
|
9873
|
+
p_title: f.title,
|
|
9874
|
+
p_description: f.description ?? "",
|
|
9875
|
+
p_changelog: f.changelog,
|
|
9876
|
+
p_steps: f.steps,
|
|
9877
|
+
p_size: f.size,
|
|
9878
|
+
p_assumption: f.assumption ?? "",
|
|
9879
|
+
p_reasoning: f.reasoning,
|
|
9880
|
+
p_spec: r.spec
|
|
9881
|
+
});
|
|
9882
|
+
const made = data?.[0];
|
|
9883
|
+
if (error || !made) throw new Error(`could not record the reworked solution: ${error?.message ?? "no new version"}`);
|
|
9884
|
+
return made;
|
|
9885
|
+
}
|
|
9886
|
+
function snapshotOf(s) {
|
|
9887
|
+
return { ref: s.ref, title: s.title, proposed_solution: s.description, future_changelog: parseChangelog(s.changelog), plan: s.steps, size: s.size, works_if: s.assumption, why: s.reasoning };
|
|
9888
|
+
}
|
|
9656
9889
|
function problemOf(tree, leaf) {
|
|
9657
9890
|
return { ref: placeOf(tree, leaf.id) ?? leaf.ref, problem: leaf.problem, description: leaf.description ?? null, status: leaf.evidence_status, confirmed_by_founder: leaf.status === "confirmed" };
|
|
9658
9891
|
}
|
|
@@ -9824,13 +10057,13 @@ var told = (r) => ({
|
|
|
9824
10057
|
});
|
|
9825
10058
|
|
|
9826
10059
|
// ../daemon/src/objective-definitions.ts
|
|
9827
|
-
import { execFile as
|
|
10060
|
+
import { execFile as execFile6 } from "child_process";
|
|
9828
10061
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
9829
10062
|
import { existsSync as existsSync3 } from "fs";
|
|
9830
|
-
import { mkdir as mkdir3, readFile as
|
|
9831
|
-
import { join as
|
|
9832
|
-
import { promisify as
|
|
9833
|
-
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);
|
|
9834
10067
|
var DEFINITIONS_TIMEOUT_MS = 9e4;
|
|
9835
10068
|
var DEFINITIONS_THINKING_TOKENS = 2e3;
|
|
9836
10069
|
var READ_TOOLS3 = /* @__PURE__ */ new Set(["Read", "Glob", "Grep", "StructuredOutput"]);
|
|
@@ -9839,7 +10072,7 @@ var COUNTING_LINES_MAX = 80;
|
|
|
9839
10072
|
var PER_FILE = 4;
|
|
9840
10073
|
async function countingLines(cwd) {
|
|
9841
10074
|
try {
|
|
9842
|
-
const { stdout } = await
|
|
10075
|
+
const { stdout } = await execFileAsync6("git", ["grep", "-n", "-I", "-i", "-E", COUNTING, "--", ".", ":!*lock*", ":!*.min.*", ":!*.map", ":!*.svg", ":!*.snap"], {
|
|
9843
10076
|
cwd,
|
|
9844
10077
|
maxBuffer: 16 * 1024 * 1024,
|
|
9845
10078
|
timeout: 5e3
|
|
@@ -9864,7 +10097,7 @@ function pickLines(raw) {
|
|
|
9864
10097
|
}
|
|
9865
10098
|
async function killRecorded(runDir) {
|
|
9866
10099
|
try {
|
|
9867
|
-
const pid = Number((await
|
|
10100
|
+
const pid = Number((await readFile6(join10(runDir, "harness.pid"), "utf8")).trim());
|
|
9868
10101
|
if (pid > 1) process.kill(pid, "SIGKILL");
|
|
9869
10102
|
} catch {
|
|
9870
10103
|
}
|
|
@@ -9884,7 +10117,7 @@ async function readDefinitions({ adapter, spec: spec2, publish: publish5, journa
|
|
|
9884
10117
|
if (!hasRepo) return give({}, "No repository to read: the card offers the common definitions.", "ok");
|
|
9885
10118
|
try {
|
|
9886
10119
|
const skill = await loadSkill("define-objective");
|
|
9887
|
-
const runDir =
|
|
10120
|
+
const runDir = join10(spec2.runDir, "definitions");
|
|
9888
10121
|
await mkdir3(runDir, { recursive: true, mode: 448 });
|
|
9889
10122
|
const schema = definitionsOutputSchema();
|
|
9890
10123
|
const [lines, listed] = await Promise.all([countingLines(spec2.cwd), listTrackedFiles(spec2.cwd)]);
|
|
@@ -9946,7 +10179,7 @@ ${renderTrackedFiles(listed.files, void 0, listed.wiki)}
|
|
|
9946
10179
|
if (!result) throw new Error("no answer");
|
|
9947
10180
|
const parsed = DefinitionsAnswer.safeParse(JSON.parse(result));
|
|
9948
10181
|
if (!parsed.success) throw new Error("an answer in the wrong shape");
|
|
9949
|
-
const checked = checkProposals(parsed.data, (file) => existsSync3(
|
|
10182
|
+
const checked = checkProposals(parsed.data, (file) => existsSync3(join10(spec2.cwd, file)));
|
|
9950
10183
|
const definitions2 = lever ? checked[lever] ? { [lever]: checked[lever] } : {} : checked;
|
|
9951
10184
|
const kept = Object.values(definitions2).reduce((n, list) => n + (list?.length ?? 0), 0);
|
|
9952
10185
|
const read6 = Object.values(definitions2).flat().filter((d) => d?.found).length;
|
|
@@ -10220,9 +10453,9 @@ function localDate(timezone, at) {
|
|
|
10220
10453
|
// ../daemon/src/run-job.ts
|
|
10221
10454
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
10222
10455
|
import { existsSync as existsSync4 } from "fs";
|
|
10223
|
-
import { mkdir as
|
|
10224
|
-
import { homedir as
|
|
10225
|
-
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";
|
|
10226
10459
|
|
|
10227
10460
|
// ../daemon/src/events.ts
|
|
10228
10461
|
var JobEventWriter = class {
|
|
@@ -10302,195 +10535,166 @@ function lighten(value) {
|
|
|
10302
10535
|
return value ?? null;
|
|
10303
10536
|
}
|
|
10304
10537
|
|
|
10305
|
-
// ../daemon/src/
|
|
10306
|
-
import {
|
|
10307
|
-
import {
|
|
10308
|
-
|
|
10309
|
-
var
|
|
10310
|
-
var
|
|
10311
|
-
|
|
10312
|
-
|
|
10313
|
-
|
|
10314
|
-
|
|
10315
|
-
|
|
10316
|
-
};
|
|
10317
|
-
function branchName(ref) {
|
|
10318
|
-
const slug = ref.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
10319
|
-
if (!slug) throw new RepoError(`"${ref}" has no letters or digits to make a branch name from`);
|
|
10320
|
-
return `nanopm/${slug}`;
|
|
10321
|
-
}
|
|
10322
|
-
async function readState(cwd) {
|
|
10323
|
-
try {
|
|
10324
|
-
await git3(cwd, ["rev-parse", "--git-dir"]);
|
|
10325
|
-
} catch {
|
|
10326
|
-
throw new RepoError(`${cwd} is not a git repository`);
|
|
10327
|
-
}
|
|
10328
|
-
const [branch, head, status2, remoteUrl] = await Promise.all([
|
|
10329
|
-
git3(cwd, ["branch", "--show-current"]).catch(() => ""),
|
|
10330
|
-
git3(cwd, ["rev-parse", "HEAD"]).catch(() => ""),
|
|
10331
|
-
git3(cwd, ["status", "--porcelain"]).catch(() => ""),
|
|
10332
|
-
git3(cwd, ["remote", "get-url", "origin"]).catch(() => "")
|
|
10333
|
-
]);
|
|
10334
|
-
const dirty = status2.split("\n").filter(Boolean).map((l) => l.slice(3));
|
|
10335
|
-
return { branch, head, clean: dirty.length === 0, dirty, hasRemote: Boolean(remoteUrl), remoteUrl: remoteUrl || void 0 };
|
|
10336
|
-
}
|
|
10337
|
-
async function ensureCleanTree(cwd) {
|
|
10338
|
-
const state = await readState(cwd);
|
|
10339
|
-
if (!state.head) {
|
|
10340
|
-
throw new RepoError("this repository has no commits yet; make the first commit, then run nanopm next");
|
|
10341
|
-
}
|
|
10342
|
-
if (!state.clean) {
|
|
10343
|
-
const shown = state.dirty.slice(0, 5).join(", ");
|
|
10344
|
-
const more = state.dirty.length > 5 ? `, and ${state.dirty.length - 5} more` : "";
|
|
10345
|
-
throw new RepoError(
|
|
10346
|
-
`the working tree has uncommitted changes (${shown}${more}). NanoPM commits only its own work, so commit or stash yours first.`
|
|
10347
|
-
);
|
|
10348
|
-
}
|
|
10349
|
-
return state;
|
|
10350
|
-
}
|
|
10351
|
-
async function createBranch(cwd, name) {
|
|
10352
|
-
const existing = await git3(cwd, ["branch", "--list", name]).catch(() => "");
|
|
10353
|
-
if (existing.trim()) {
|
|
10354
|
-
throw new RepoError(`branch ${name} already exists. Delete it or finish what is on it first.`);
|
|
10355
|
-
}
|
|
10356
|
-
await git3(cwd, ["checkout", "-b", name]);
|
|
10357
|
-
}
|
|
10358
|
-
async function commitAll(cwd, message) {
|
|
10359
|
-
await git3(cwd, ["add", "-A"]);
|
|
10360
|
-
const staged = await git3(cwd, ["diff", "--cached", "--name-only"]).catch(() => "");
|
|
10361
|
-
if (!staged) return null;
|
|
10362
|
-
await git3(cwd, ["commit", "-m", message, "-m", "Written by NanoPM. Review before merging."]);
|
|
10363
|
-
const sha = await git3(cwd, ["rev-parse", "--short", "HEAD"]);
|
|
10364
|
-
return { sha, ...await diffStat(cwd) };
|
|
10365
|
-
}
|
|
10366
|
-
async function diffStat(cwd) {
|
|
10367
|
-
const numstat = await git3(cwd, ["show", "--numstat", "--format=", "HEAD"]).catch(() => "");
|
|
10368
|
-
let files = 0;
|
|
10369
|
-
let insertions = 0;
|
|
10370
|
-
let deletions = 0;
|
|
10371
|
-
for (const line of numstat.split("\n").filter(Boolean)) {
|
|
10372
|
-
const [add2 = "", del = ""] = line.split(" ");
|
|
10373
|
-
files += 1;
|
|
10374
|
-
insertions += Number(add2) || 0;
|
|
10375
|
-
deletions += Number(del) || 0;
|
|
10376
|
-
}
|
|
10377
|
-
return { files, insertions, deletions };
|
|
10378
|
-
}
|
|
10379
|
-
async function pushBranch(cwd, name) {
|
|
10380
|
-
const state = await readState(cwd);
|
|
10381
|
-
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;
|
|
10382
10549
|
try {
|
|
10383
|
-
const
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
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)) } };
|
|
10388
10560
|
} catch (err) {
|
|
10389
|
-
|
|
10390
|
-
return {
|
|
10561
|
+
if (made) await removeWorktree(clone, dir);
|
|
10562
|
+
return { ok: false, outcome: failed(errorMessage(err)) };
|
|
10391
10563
|
}
|
|
10392
10564
|
}
|
|
10393
|
-
function
|
|
10394
|
-
const
|
|
10395
|
-
|
|
10396
|
-
}
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
|
|
10401
|
-
|
|
10402
|
-
}
|
|
10403
|
-
|
|
10404
|
-
|
|
10405
|
-
|
|
10406
|
-
}
|
|
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 ? `
|
|
10407
10607
|
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
|
|
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();
|
|
10411
10645
|
if (error) throw new Error(`could not read the solution: ${error.message}`);
|
|
10412
10646
|
if (!data) throw new Error("that solution is not on this project any more");
|
|
10413
|
-
|
|
10414
|
-
throw new Error(`${data.ref} is ${data.status}. Only a solution you accepted can be built.`);
|
|
10415
|
-
}
|
|
10416
|
-
return { ...data, sources: data.sources ?? [] };
|
|
10417
|
-
}
|
|
10418
|
-
async function startSolution(client, projectId, ref, jobId) {
|
|
10419
|
-
const { error } = await client.rpc("start_solution", { p_project_id: projectId, p_ref: ref, p_job_id: jobId });
|
|
10420
|
-
if (error && !/only an accepted solution/.test(error.message)) throw new Error(pgMessage(error));
|
|
10421
|
-
}
|
|
10422
|
-
async function unstartSolution(client, projectId, ref) {
|
|
10423
|
-
await client.rpc("unstart_solution", { p_project_id: projectId, p_ref: ref });
|
|
10647
|
+
return data;
|
|
10424
10648
|
}
|
|
10425
|
-
function
|
|
10426
|
-
const
|
|
10427
|
-
|
|
10428
|
-
if (
|
|
10429
|
-
|
|
10430
|
-
|
|
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
|
+
`);
|
|
10431
10664
|
}
|
|
10432
|
-
return lines.join("\n");
|
|
10433
10665
|
}
|
|
10434
|
-
|
|
10435
|
-
const
|
|
10436
|
-
|
|
10437
|
-
throw new RepoError("this repository has a detached HEAD; check out a branch first");
|
|
10438
|
-
}
|
|
10439
|
-
const branch = branchName(ref);
|
|
10440
|
-
await createBranch(cwd, branch);
|
|
10441
|
-
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;
|
|
10442
10669
|
}
|
|
10443
|
-
|
|
10444
|
-
|
|
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");
|
|
10445
10679
|
}
|
|
10446
|
-
async function
|
|
10447
|
-
|
|
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;
|
|
10448
10683
|
}
|
|
10449
|
-
|
|
10450
|
-
|
|
10451
|
-
|
|
10452
|
-
|
|
10453
|
-
|
|
10454
|
-
|
|
10455
|
-
|
|
10456
|
-
commit = null;
|
|
10457
|
-
}
|
|
10458
|
-
if (!commit) {
|
|
10459
|
-
await checkout(cwd, session2.baseBranch).catch(() => void 0);
|
|
10460
|
-
await deleteBranch(cwd, session2.branch);
|
|
10461
|
-
return { branch: session2.branch, pushed: false, note: "nothing was changed, so no branch was kept" };
|
|
10462
|
-
}
|
|
10463
|
-
const push = await pushBranch(cwd, session2.branch);
|
|
10464
|
-
await checkout(cwd, session2.baseBranch).catch(() => void 0);
|
|
10465
|
-
return {
|
|
10466
|
-
branch: session2.branch,
|
|
10467
|
-
commit,
|
|
10468
|
-
pushed: push.pushed,
|
|
10469
|
-
reviewUrl: push.reviewUrl,
|
|
10470
|
-
note: push.pushed ? void 0 : `not pushed: ${push.reason}`
|
|
10471
|
-
};
|
|
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");
|
|
10472
10691
|
}
|
|
10473
|
-
async function
|
|
10474
|
-
const
|
|
10475
|
-
if (
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
p_project_id: job.project_id,
|
|
10480
|
-
p_ref: solution.ref,
|
|
10481
|
-
p_job_id: job.id,
|
|
10482
|
-
p_sources: sources2
|
|
10483
|
-
});
|
|
10484
|
-
if (error) throw new Error(`the work is committed on ${result.branch}, but recording it failed: ${error.message}`);
|
|
10485
|
-
}
|
|
10486
|
-
function renderRepoResult(result) {
|
|
10487
|
-
if (!result.commit) return result.note ?? "nothing changed";
|
|
10488
|
-
const { files, insertions, deletions } = result.commit;
|
|
10489
|
-
const stat2 = `${files} file${files === 1 ? "" : "s"} changed, ${insertions} insertion${insertions === 1 ? "" : "s"}, ${deletions} deletion${deletions === 1 ? "" : "s"}`;
|
|
10490
|
-
const lines = [`${result.branch} \xB7 ${result.commit.sha} \xB7 ${stat2}`];
|
|
10491
|
-
if (result.reviewUrl) lines.push(result.reviewUrl);
|
|
10492
|
-
else if (result.note) lines.push(result.note);
|
|
10493
|
-
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" });
|
|
10494
10698
|
}
|
|
10495
10699
|
|
|
10496
10700
|
// ../daemon/src/run-job.ts
|
|
@@ -10508,107 +10712,6 @@ async function askAboutSite(opts) {
|
|
|
10508
10712
|
});
|
|
10509
10713
|
}
|
|
10510
10714
|
|
|
10511
|
-
// ../daemon/src/verify.ts
|
|
10512
|
-
import { spawn as spawn3 } from "child_process";
|
|
10513
|
-
var VERIFY_TIMEOUT_MS = 10 * 6e4;
|
|
10514
|
-
var OUTPUT_TAIL = 4e3;
|
|
10515
|
-
var DEFINES_THE_COMMAND = [
|
|
10516
|
-
/(^|\/)package\.json$/,
|
|
10517
|
-
/(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb)$/,
|
|
10518
|
-
/(^|\/)(Makefile|justfile|Justfile|Taskfile\.ya?ml)$/,
|
|
10519
|
-
/(^|\/)\.husky\//,
|
|
10520
|
-
/(^|\/)\.github\//,
|
|
10521
|
-
/(^|\/)(pyproject\.toml|uv\.lock|Cargo\.toml|Cargo\.lock|Gemfile|Gemfile\.lock)$/
|
|
10522
|
-
];
|
|
10523
|
-
function pathsNamedIn(command) {
|
|
10524
|
-
return command.split(/[\s;&|()<>]+/).filter((t) => /[/.]/.test(t) && !/^-/.test(t) && t.length > 2).map((t) => t.replace(/^\.\//, "").replace(/^["']|["']$/g, ""));
|
|
10525
|
-
}
|
|
10526
|
-
function guardVerify(changed, command) {
|
|
10527
|
-
for (const file of changed) {
|
|
10528
|
-
const pattern = DEFINES_THE_COMMAND.find((p) => p.test(file));
|
|
10529
|
-
if (pattern) {
|
|
10530
|
-
return `the change touched ${file}, which decides what the verify command does, so the command was not run`;
|
|
10531
|
-
}
|
|
10532
|
-
}
|
|
10533
|
-
const named = pathsNamedIn(command);
|
|
10534
|
-
for (const file of changed) {
|
|
10535
|
-
if (named.some((n) => file === n || file.endsWith(`/${n}`) || n.endsWith(file))) {
|
|
10536
|
-
return `the change touched ${file}, which the verify command runs, so the command was not run`;
|
|
10537
|
-
}
|
|
10538
|
-
}
|
|
10539
|
-
return null;
|
|
10540
|
-
}
|
|
10541
|
-
async function verify(opts) {
|
|
10542
|
-
const skipped = guardVerify(opts.changed, opts.command);
|
|
10543
|
-
if (skipped) return { ran: false, passed: false, skipped, output: "", durationMs: 0, timedOut: false };
|
|
10544
|
-
return runCommand(opts);
|
|
10545
|
-
}
|
|
10546
|
-
async function runCommand(opts) {
|
|
10547
|
-
const timeoutMs = opts.timeoutMs ?? VERIFY_TIMEOUT_MS;
|
|
10548
|
-
const startedAt = Date.now();
|
|
10549
|
-
const child = spawn3("sh", ["-c", opts.command], {
|
|
10550
|
-
cwd: opts.cwd,
|
|
10551
|
-
env: opts.env,
|
|
10552
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
10553
|
-
});
|
|
10554
|
-
let output = "";
|
|
10555
|
-
const collect6 = (chunk) => {
|
|
10556
|
-
output += chunk.toString();
|
|
10557
|
-
if (output.length > OUTPUT_TAIL * 4) output = output.slice(-OUTPUT_TAIL * 2);
|
|
10558
|
-
};
|
|
10559
|
-
child.stdout.on("data", collect6);
|
|
10560
|
-
child.stderr.on("data", collect6);
|
|
10561
|
-
let timedOut = false;
|
|
10562
|
-
const timer = setTimeout(() => {
|
|
10563
|
-
timedOut = true;
|
|
10564
|
-
child.kill("SIGTERM");
|
|
10565
|
-
setTimeout(() => child.kill("SIGKILL"), 5e3).unref();
|
|
10566
|
-
}, timeoutMs);
|
|
10567
|
-
const onAbort = () => child.kill("SIGTERM");
|
|
10568
|
-
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
10569
|
-
const exitCode = await new Promise((resolve6) => {
|
|
10570
|
-
child.on("close", (code) => resolve6(code));
|
|
10571
|
-
child.on("error", () => resolve6(127));
|
|
10572
|
-
});
|
|
10573
|
-
clearTimeout(timer);
|
|
10574
|
-
opts.signal?.removeEventListener("abort", onAbort);
|
|
10575
|
-
return {
|
|
10576
|
-
ran: true,
|
|
10577
|
-
passed: exitCode === 0 && !timedOut,
|
|
10578
|
-
output: tail(output, OUTPUT_TAIL),
|
|
10579
|
-
exitCode: exitCode ?? void 0,
|
|
10580
|
-
durationMs: Date.now() - startedAt,
|
|
10581
|
-
timedOut
|
|
10582
|
-
};
|
|
10583
|
-
}
|
|
10584
|
-
function tail(text2, max) {
|
|
10585
|
-
const trimmed = text2.trimEnd();
|
|
10586
|
-
if (trimmed.length <= max) return trimmed;
|
|
10587
|
-
return `\u2026 ${trimmed.slice(-max)}`;
|
|
10588
|
-
}
|
|
10589
|
-
function renderVerifyResult(result, command) {
|
|
10590
|
-
if (!result.ran) return result.skipped ?? "no verify command is set for this project";
|
|
10591
|
-
const secs = (result.durationMs / 1e3).toFixed(0);
|
|
10592
|
-
if (result.timedOut) return `the check NanoPM could not run itself, \`${command}\`, did not finish within ${secs}s and was stopped`;
|
|
10593
|
-
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`;
|
|
10594
|
-
}
|
|
10595
|
-
function retryPrompt(result, command) {
|
|
10596
|
-
return [
|
|
10597
|
-
`Your change does not pass this project's own check.`,
|
|
10598
|
-
``,
|
|
10599
|
-
`The daemon ran \`${command}\` in the repository root and it failed with exit ${result.exitCode}.`,
|
|
10600
|
-
`You did not run this and cannot run it; this output is the only evidence you get.`,
|
|
10601
|
-
``,
|
|
10602
|
-
"```",
|
|
10603
|
-
result.output || "(the command produced no output)",
|
|
10604
|
-
"```",
|
|
10605
|
-
``,
|
|
10606
|
-
`Fix what this reports, in the files you already changed where possible. Do not disable`,
|
|
10607
|
-
`the check, do not edit what the command runs, and do not change unrelated code to make`,
|
|
10608
|
-
`it pass. If the failure is not caused by your change, say so plainly and stop.`
|
|
10609
|
-
].join("\n");
|
|
10610
|
-
}
|
|
10611
|
-
|
|
10612
10715
|
// ../daemon/src/run-job.ts
|
|
10613
10716
|
var QUICK_RUNS = { reword: runRewordJob, talk: runTalkJob };
|
|
10614
10717
|
async function runJob(job, ctx) {
|
|
@@ -10660,8 +10763,8 @@ async function runJob(job, ctx) {
|
|
|
10660
10763
|
return outcome;
|
|
10661
10764
|
}
|
|
10662
10765
|
async function execute(job, skill, ctx, events) {
|
|
10663
|
-
const runDir =
|
|
10664
|
-
await
|
|
10766
|
+
const runDir = join12(nanopmHome(), "run", job.id);
|
|
10767
|
+
await mkdir5(runDir, { recursive: true, mode: 448 });
|
|
10665
10768
|
const resumeSessionId = job.resume_of ? await sessionOf(ctx.client, job.resume_of) : void 0;
|
|
10666
10769
|
const sessionId = resumeSessionId ?? randomUUID12();
|
|
10667
10770
|
await ctx.client.from("jobs").update({ harness_session_id: sessionId }).eq("id", job.id);
|
|
@@ -10705,14 +10808,14 @@ async function execute(job, skill, ctx, events) {
|
|
|
10705
10808
|
clearTimeout(timer);
|
|
10706
10809
|
clearInterval(heartbeat2);
|
|
10707
10810
|
clearInterval(cancelPoll);
|
|
10708
|
-
await
|
|
10811
|
+
await rm4(runDir, { recursive: true, force: true });
|
|
10709
10812
|
}
|
|
10710
10813
|
}
|
|
10711
10814
|
const quickRun2 = QUICK_RUNS[job.skill];
|
|
10712
10815
|
if (quickRun2) {
|
|
10713
10816
|
const cwd2 = scratchDir(job.id);
|
|
10714
10817
|
try {
|
|
10715
|
-
await
|
|
10818
|
+
await mkdir5(cwd2, { recursive: true, mode: 448 });
|
|
10716
10819
|
const caps = skill.manifest.capabilities;
|
|
10717
10820
|
return await quickRun2({
|
|
10718
10821
|
client: ctx.client,
|
|
@@ -10732,13 +10835,22 @@ async function execute(job, skill, ctx, events) {
|
|
|
10732
10835
|
clearTimeout(timer);
|
|
10733
10836
|
clearInterval(heartbeat2);
|
|
10734
10837
|
clearInterval(cancelPoll);
|
|
10735
|
-
await
|
|
10736
|
-
await
|
|
10838
|
+
await rm4(runDir, { recursive: true, force: true });
|
|
10839
|
+
await rm4(cwd2, { recursive: true, force: true });
|
|
10737
10840
|
}
|
|
10738
10841
|
}
|
|
10739
|
-
const capabilities = capCapabilities(skill.manifest.capabilities, autonomy2);
|
|
10842
|
+
const capabilities = capCapabilities(skill.manifest.capabilities, autonomy2, { asked: skill.manifest.asked && job.requested_by === ctx.userId });
|
|
10740
10843
|
const harnessWeb = await claudeSearches(searchProvider);
|
|
10741
|
-
|
|
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;
|
|
10742
10854
|
const definitionsRead = job.skill === "onboard" && !job.resume_of ? readDefinitions({
|
|
10743
10855
|
adapter,
|
|
10744
10856
|
spec: { cwd, runDir, systemPrompt: "", prompt: "", capabilities: [], mcp: [], maxTurns: 1, sessionId, env: harnessEnv(timeoutMs), signal: abort.signal },
|
|
@@ -10762,29 +10874,6 @@ async function execute(job, skill, ctx, events) {
|
|
|
10762
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}`;
|
|
10763
10875
|
await logActivity(ctx, job, "scan.site", site.missed.length ? `${read6} (${site.missed.length} not read)` : read6);
|
|
10764
10876
|
}
|
|
10765
|
-
let item;
|
|
10766
|
-
let session2;
|
|
10767
|
-
if (capabilities.includes("write_repo")) {
|
|
10768
|
-
const solutionId = params.solution_id;
|
|
10769
|
-
if (typeof solutionId !== "string" || !solutionId) {
|
|
10770
|
-
clearTimeout(timer);
|
|
10771
|
-
clearInterval(heartbeat2);
|
|
10772
|
-
clearInterval(cancelPoll);
|
|
10773
|
-
return { status: "failed", error: { code: "harness_error", message: "this job names no solution to build" } };
|
|
10774
|
-
}
|
|
10775
|
-
try {
|
|
10776
|
-
item = await loadAcceptedSolution(ctx.client, job.project_id, solutionId);
|
|
10777
|
-
session2 = await openRepoSession(cwd, item.ref);
|
|
10778
|
-
await startSolution(ctx.client, job.project_id, item.ref, job.id);
|
|
10779
|
-
params.item = renderSolution(item);
|
|
10780
|
-
await logActivity(ctx, job, "repo.branch", `working on ${session2.branch}`);
|
|
10781
|
-
} catch (err) {
|
|
10782
|
-
clearTimeout(timer);
|
|
10783
|
-
clearInterval(heartbeat2);
|
|
10784
|
-
clearInterval(cancelPoll);
|
|
10785
|
-
return { status: "failed", error: { code: "harness_error", message: errorMessage(err) } };
|
|
10786
|
-
}
|
|
10787
|
-
}
|
|
10788
10877
|
const spec2 = {
|
|
10789
10878
|
cwd,
|
|
10790
10879
|
runDir,
|
|
@@ -10804,20 +10893,21 @@ async function execute(job, skill, ctx, events) {
|
|
|
10804
10893
|
].filter(Boolean).join("\n\n"),
|
|
10805
10894
|
capabilities,
|
|
10806
10895
|
harnessWeb,
|
|
10896
|
+
network: build2?.network,
|
|
10807
10897
|
withoutTools: skill.manifest.withoutTools,
|
|
10808
10898
|
// The workspace reaches the memory server only for a job that changes files; that is
|
|
10809
|
-
// what lets
|
|
10899
|
+
// what lets the build's tools refuse everywhere else.
|
|
10810
10900
|
mcp: ctx.mcpServers?.(job, timeoutMs, capabilities.includes("write_repo") ? cwd : void 0) ?? [],
|
|
10811
10901
|
model: skill.manifest.model,
|
|
10812
10902
|
maxTurns: skill.manifest.maxTurns,
|
|
10813
10903
|
sessionId,
|
|
10814
10904
|
resumeSessionId,
|
|
10815
|
-
|
|
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),
|
|
10816
10907
|
signal: abort.signal
|
|
10817
10908
|
};
|
|
10818
10909
|
const verifyCommand2 = project?.preferences?.verify_command?.trim() || null;
|
|
10819
10910
|
let outcome;
|
|
10820
|
-
let verified;
|
|
10821
10911
|
let webCalls = 0;
|
|
10822
10912
|
const onHarnessEvent = async (event) => {
|
|
10823
10913
|
await events.harness(event);
|
|
@@ -10835,9 +10925,27 @@ async function execute(job, skill, ctx, events) {
|
|
|
10835
10925
|
return result;
|
|
10836
10926
|
};
|
|
10837
10927
|
try {
|
|
10838
|
-
if (
|
|
10928
|
+
if (build2) {
|
|
10839
10929
|
try {
|
|
10840
|
-
return await
|
|
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
|
+
}
|
|
10946
|
+
if (job.skill === "personas") {
|
|
10947
|
+
try {
|
|
10948
|
+
return await runPersonaJob({ client: ctx.client, job, adapter, spec: spec2, onEvent: onHarnessEvent });
|
|
10841
10949
|
} catch (error) {
|
|
10842
10950
|
if (cancelled) return { status: "cancelled" };
|
|
10843
10951
|
return { status: "failed", error: { code: timedOut ? "timeout" : "harness_error", message: errorMessage(error) } };
|
|
@@ -10915,83 +11023,23 @@ async function execute(job, skill, ctx, events) {
|
|
|
10915
11023
|
return { status: "failed", error: { code: timedOut ? "timeout" : "harness_error", message: errorMessage(error) } };
|
|
10916
11024
|
}
|
|
10917
11025
|
}
|
|
10918
|
-
for (
|
|
10919
|
-
|
|
10920
|
-
|
|
10921
|
-
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
if (event.type === "finished") {
|
|
10925
|
-
attemptOutcome = { status: "succeeded", result: event.result, usage: event.usage };
|
|
10926
|
-
} else if (event.type === "failed") {
|
|
10927
|
-
attemptOutcome = { status: "failed", error: { code: event.code, message: event.message } };
|
|
10928
|
-
}
|
|
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 } };
|
|
10929
11032
|
}
|
|
10930
|
-
if (attemptOutcome) await noteIfItNeverSearched(attemptOutcome, capabilities.includes("read_web"));
|
|
10931
|
-
outcome = mergeAttempts(outcome, attemptOutcome);
|
|
10932
|
-
if (attemptOutcome?.status !== "succeeded" || !session2 || !verifyCommand2) break;
|
|
10933
|
-
verified = await verify({
|
|
10934
|
-
cwd,
|
|
10935
|
-
command: verifyCommand2,
|
|
10936
|
-
env: harnessEnv(timeoutMs),
|
|
10937
|
-
changed: await changedFiles(cwd),
|
|
10938
|
-
signal: abort.signal
|
|
10939
|
-
});
|
|
10940
|
-
await logActivity(ctx, job, "repo.verify", renderVerifyResult(verified, verifyCommand2), {
|
|
10941
|
-
outcome: verified.passed ? "ok" : "error",
|
|
10942
|
-
usage: { duration_ms: verified.durationMs }
|
|
10943
|
-
});
|
|
10944
|
-
await events.write("text", { text: renderVerifyResult(verified, verifyCommand2) });
|
|
10945
|
-
if (verified.passed || !verified.ran || attempt7 === 2) break;
|
|
10946
11033
|
}
|
|
11034
|
+
if (outcome) await noteIfItNeverSearched(outcome, capabilities.includes("read_web"));
|
|
10947
11035
|
} finally {
|
|
10948
11036
|
clearTimeout(timer);
|
|
10949
11037
|
clearInterval(heartbeat2);
|
|
10950
11038
|
clearInterval(cancelPoll);
|
|
10951
11039
|
await definitionsRead;
|
|
10952
|
-
await
|
|
10953
|
-
}
|
|
10954
|
-
if (outcome?.status === "succeeded" && verified && verified.ran && !verified.passed) {
|
|
10955
|
-
outcome = {
|
|
10956
|
-
...outcome,
|
|
10957
|
-
status: "failed",
|
|
10958
|
-
error: {
|
|
10959
|
-
code: "harness_error",
|
|
10960
|
-
message: `${renderVerifyResult(verified, verifyCommand2)}. The work is on the branch; the solution stays accepted.`
|
|
10961
|
-
}
|
|
10962
|
-
};
|
|
10963
|
-
}
|
|
10964
|
-
if (outcome?.status === "succeeded") {
|
|
10965
|
-
if (!session2 || !item) return outcome;
|
|
10966
|
-
try {
|
|
10967
|
-
const repo = await closeRepoSession(cwd, session2, item);
|
|
10968
|
-
await logActivity(ctx, job, "repo.commit", renderRepoResult(repo).split("\n")[0]);
|
|
10969
|
-
if (!repo.commit) {
|
|
10970
|
-
await unstartSolution(ctx.client, job.project_id, item.ref).catch(() => void 0);
|
|
10971
|
-
return {
|
|
10972
|
-
...outcome,
|
|
10973
|
-
status: "failed",
|
|
10974
|
-
error: { code: "harness_error", message: "the skill finished without changing anything" }
|
|
10975
|
-
};
|
|
10976
|
-
}
|
|
10977
|
-
await finishSolution(ctx.client, job, item, repo);
|
|
10978
|
-
return { ...outcome, result: [outcome.result, "", renderRepoResult(repo)].filter((l) => l !== void 0).join("\n") };
|
|
10979
|
-
} catch (err) {
|
|
10980
|
-
return {
|
|
10981
|
-
...outcome,
|
|
10982
|
-
status: "failed",
|
|
10983
|
-
error: { code: "harness_error", message: errorMessage(err) }
|
|
10984
|
-
};
|
|
10985
|
-
}
|
|
10986
|
-
}
|
|
10987
|
-
if (session2) {
|
|
10988
|
-
const kept = await abandonRepoSession(cwd, session2, verified?.ran && !verified.passed ? item : void 0).catch(() => void 0);
|
|
10989
|
-
if (item) await unstartSolution(ctx.client, job.project_id, item.ref).catch(() => void 0);
|
|
10990
|
-
if (kept?.commit) {
|
|
10991
|
-
await logActivity(ctx, job, "repo.commit", renderRepoResult(kept).split("\n")[0], { outcome: "error" });
|
|
10992
|
-
if (outcome) outcome = { ...outcome, result: [outcome.result, "", renderRepoResult(kept)].filter(Boolean).join("\n") };
|
|
10993
|
-
}
|
|
11040
|
+
await rm4(runDir, { recursive: true, force: true });
|
|
10994
11041
|
}
|
|
11042
|
+
if (outcome?.status === "succeeded") return outcome;
|
|
10995
11043
|
if (cancelled) return { status: "cancelled" };
|
|
10996
11044
|
if (timedOut) {
|
|
10997
11045
|
return { status: "failed", error: { code: "timeout", message: `exceeded the ${skill.manifest.timeout} cap` } };
|
|
@@ -11002,10 +11050,6 @@ async function sessionOf(client, jobId) {
|
|
|
11002
11050
|
const { data } = await client.from("jobs").select("harness_session_id").eq("id", jobId).maybeSingle();
|
|
11003
11051
|
return data?.harness_session_id ?? void 0;
|
|
11004
11052
|
}
|
|
11005
|
-
function mergeAttempts(first, second) {
|
|
11006
|
-
if (!first?.usage || !second?.usage) return second ?? first;
|
|
11007
|
-
return { ...second, usage: sumUsage(first.usage, second.usage) };
|
|
11008
|
-
}
|
|
11009
11053
|
async function askForTheSite(ctx, job, project, events) {
|
|
11010
11054
|
const started = Date.now();
|
|
11011
11055
|
const search = await searchForSite(project.name);
|
|
@@ -11031,7 +11075,7 @@ function harnessWebFor(searchProvider, machineCanSearch) {
|
|
|
11031
11075
|
}
|
|
11032
11076
|
async function machineWebSearch() {
|
|
11033
11077
|
try {
|
|
11034
|
-
const parsed = JSON.parse(await
|
|
11078
|
+
const parsed = JSON.parse(await readFile8(join12(nanopmHome(), "daemon.json"), "utf8"));
|
|
11035
11079
|
return parsed.webSearch === true;
|
|
11036
11080
|
} catch {
|
|
11037
11081
|
return false;
|
|
@@ -11073,7 +11117,7 @@ function buildHeader(job, project, cwd, harnessWeb = false, memory = false) {
|
|
|
11073
11117
|
return lines.join("\n");
|
|
11074
11118
|
}
|
|
11075
11119
|
function scratchDir(jobId) {
|
|
11076
|
-
return
|
|
11120
|
+
return join12(tmpdir(), `nanopm-run-${jobId}`);
|
|
11077
11121
|
}
|
|
11078
11122
|
async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
|
|
11079
11123
|
const refuse2 = (why) => {
|
|
@@ -11082,12 +11126,12 @@ async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
|
|
|
11082
11126
|
const requested = params.repo_path;
|
|
11083
11127
|
if (typeof requested !== "string" || !requested) {
|
|
11084
11128
|
if (mustBeTheClone) refuse2("the job named no repository");
|
|
11085
|
-
await
|
|
11129
|
+
await mkdir5(fallback, { recursive: true, mode: 448 });
|
|
11086
11130
|
return fallback;
|
|
11087
11131
|
}
|
|
11088
11132
|
if (!job.project_id) {
|
|
11089
11133
|
if (mustBeTheClone) refuse2("the job belongs to no project");
|
|
11090
|
-
await
|
|
11134
|
+
await mkdir5(fallback, { recursive: true, mode: 448 });
|
|
11091
11135
|
return fallback;
|
|
11092
11136
|
}
|
|
11093
11137
|
const { data: clone } = await ctx.client.from("clones").select("repo_path").eq("project_id", job.project_id).eq("daemon_id", ctx.daemonId).maybeSingle();
|
|
@@ -11095,12 +11139,12 @@ async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
|
|
|
11095
11139
|
if (!allowed || resolve4(allowed) !== resolve4(requested)) {
|
|
11096
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`);
|
|
11097
11141
|
console.error(`job ${job.id}: repo_path ${requested} is not this machine's clone; running in a scratch directory instead`);
|
|
11098
|
-
await
|
|
11142
|
+
await mkdir5(fallback, { recursive: true, mode: 448 });
|
|
11099
11143
|
return fallback;
|
|
11100
11144
|
}
|
|
11101
11145
|
if (existsSync4(allowed)) return resolve4(allowed);
|
|
11102
11146
|
if (mustBeTheClone) refuse2(`${allowed} is not on this machine any more`);
|
|
11103
|
-
await
|
|
11147
|
+
await mkdir5(fallback, { recursive: true, mode: 448 });
|
|
11104
11148
|
return fallback;
|
|
11105
11149
|
}
|
|
11106
11150
|
function asData8(label, body) {
|
|
@@ -11109,10 +11153,10 @@ ${body}
|
|
|
11109
11153
|
</${label}>`;
|
|
11110
11154
|
}
|
|
11111
11155
|
function harnessEnv(timeoutMs, source = process.env) {
|
|
11112
|
-
const home = source.HOME ??
|
|
11156
|
+
const home = source.HOME ?? homedir4();
|
|
11113
11157
|
const env = {
|
|
11114
11158
|
HOME: home,
|
|
11115
|
-
PATH: [source.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
11159
|
+
PATH: [source.PATH ?? "/usr/local/bin:/usr/bin:/bin", join12(home, ".local", "bin")].join(":"),
|
|
11116
11160
|
MCP_TOOL_TIMEOUT: String(timeoutMs)
|
|
11117
11161
|
};
|
|
11118
11162
|
for (const key of ["USER", "LANG", "TMPDIR", "SHELL", "ANTHROPIC_API_KEY", "CLAUDE_CONFIG_DIR"]) {
|
|
@@ -11289,12 +11333,12 @@ async function chainAfter(auth, job, outcome, log) {
|
|
|
11289
11333
|
const parent = job.parent_job_id ? await parentOf(auth, job.parent_job_id) : null;
|
|
11290
11334
|
const own = job.params ?? {};
|
|
11291
11335
|
const deep = own.depth === "deep" && job.trigger === "hook";
|
|
11292
|
-
const
|
|
11293
|
-
if (!
|
|
11336
|
+
const next = daemonQueues(job.skill, 0, parent?.skill ?? null, deep);
|
|
11337
|
+
if (!next.length) return;
|
|
11294
11338
|
if (await chainDepth(auth, job) >= CHAIN_DEPTH_MAX) return;
|
|
11295
11339
|
const from = deep ? own.repo_path : parent?.params?.repo_path;
|
|
11296
11340
|
const repo = typeof from === "string" ? { repo_path: from } : null;
|
|
11297
|
-
for (const skill of
|
|
11341
|
+
for (const skill of next) {
|
|
11298
11342
|
const params = { ...repo ?? {}, ...deep ? { depth: "deep", background: true } : {} };
|
|
11299
11343
|
const { data, error } = await auth.client.from("jobs").insert({
|
|
11300
11344
|
project_id: job.project_id,
|
|
@@ -11377,7 +11421,7 @@ var heldInThisProcess = false;
|
|
|
11377
11421
|
function acquireLock() {
|
|
11378
11422
|
if (heldInThisProcess) throw new Error("a nanopm daemon is already running in this process");
|
|
11379
11423
|
mkdirSync(nanopmHome(), { recursive: true, mode: 448 });
|
|
11380
|
-
const path =
|
|
11424
|
+
const path = join13(nanopmHome(), "daemon.pid");
|
|
11381
11425
|
for (; ; ) {
|
|
11382
11426
|
try {
|
|
11383
11427
|
const fd = openSync(path, "wx");
|
|
@@ -11464,13 +11508,13 @@ async function reclaimOwnJobs(client, daemonId) {
|
|
|
11464
11508
|
async function harnessPids(runDir) {
|
|
11465
11509
|
const dirs = [runDir];
|
|
11466
11510
|
try {
|
|
11467
|
-
for (const d of await
|
|
11511
|
+
for (const d of await readdir3(runDir, { withFileTypes: true })) if (d.isDirectory()) dirs.push(join13(runDir, d.name));
|
|
11468
11512
|
} catch {
|
|
11469
11513
|
}
|
|
11470
11514
|
const pids = [];
|
|
11471
11515
|
for (const dir of dirs) {
|
|
11472
11516
|
try {
|
|
11473
|
-
const pid = Number((await
|
|
11517
|
+
const pid = Number((await readFile9(join13(dir, "harness.pid"), "utf8")).trim());
|
|
11474
11518
|
if (pid > 1) pids.push(pid);
|
|
11475
11519
|
} catch {
|
|
11476
11520
|
}
|
|
@@ -11478,20 +11522,20 @@ async function harnessPids(runDir) {
|
|
|
11478
11522
|
return pids;
|
|
11479
11523
|
}
|
|
11480
11524
|
async function killOrphanedHarness(jobId) {
|
|
11481
|
-
const runDir =
|
|
11525
|
+
const runDir = join13(nanopmHome(), "run", jobId);
|
|
11482
11526
|
for (const pid of await harnessPids(runDir)) {
|
|
11483
11527
|
try {
|
|
11484
11528
|
process.kill(pid, "SIGTERM");
|
|
11485
11529
|
} catch {
|
|
11486
11530
|
}
|
|
11487
11531
|
}
|
|
11488
|
-
await
|
|
11532
|
+
await rm5(runDir, { recursive: true, force: true });
|
|
11489
11533
|
}
|
|
11490
11534
|
async function registerDaemon(auth, version, opts = {}) {
|
|
11491
|
-
const path =
|
|
11535
|
+
const path = join13(nanopmHome(), "daemon.json");
|
|
11492
11536
|
let identity = null;
|
|
11493
11537
|
try {
|
|
11494
|
-
identity = JSON.parse(await
|
|
11538
|
+
identity = JSON.parse(await readFile9(path, "utf8"));
|
|
11495
11539
|
} catch {
|
|
11496
11540
|
identity = null;
|
|
11497
11541
|
}
|
|
@@ -11529,8 +11573,8 @@ async function registerDaemon(auth, version, opts = {}) {
|
|
|
11529
11573
|
return data.id;
|
|
11530
11574
|
}
|
|
11531
11575
|
async function rememberDaemon(id, shared = false) {
|
|
11532
|
-
await
|
|
11533
|
-
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 });
|
|
11534
11578
|
}
|
|
11535
11579
|
async function heartbeat(client, daemonId) {
|
|
11536
11580
|
await client.from("daemons").update({ status: "online", last_seen_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", daemonId);
|
|
@@ -11881,48 +11925,6 @@ function refuseFetch(url, allowlist, open = false) {
|
|
|
11881
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.`;
|
|
11882
11926
|
}
|
|
11883
11927
|
|
|
11884
|
-
// ../memory-mcp/src/repo-rules.ts
|
|
11885
|
-
import { lstat, unlink } from "fs/promises";
|
|
11886
|
-
import { relative as relative3, resolve as resolve5, sep as sep2 } from "path";
|
|
11887
|
-
var REMOVE_MAX = 50;
|
|
11888
|
-
function refuseRemove(workspace, requested) {
|
|
11889
|
-
const root2 = resolve5(workspace);
|
|
11890
|
-
const target = resolve5(root2, requested);
|
|
11891
|
-
const rel = relative3(root2, target);
|
|
11892
|
-
if (!rel || rel.startsWith("..") || rel.startsWith(`..${sep2}`)) return "outside the workspace";
|
|
11893
|
-
if (rel === ".git" || rel.startsWith(`.git${sep2}`)) return "inside .git";
|
|
11894
|
-
if (looksLikeASecret(rel)) return "looks like a secret, which the agent may neither read nor remove";
|
|
11895
|
-
return null;
|
|
11896
|
-
}
|
|
11897
|
-
async function removeFiles(workspace, paths) {
|
|
11898
|
-
const out = [];
|
|
11899
|
-
for (const requested of paths.slice(0, REMOVE_MAX)) {
|
|
11900
|
-
const refusal = refuseRemove(workspace, requested);
|
|
11901
|
-
if (refusal) {
|
|
11902
|
-
out.push({ path: requested, removed: false, reason: refusal });
|
|
11903
|
-
continue;
|
|
11904
|
-
}
|
|
11905
|
-
const target = resolve5(workspace, requested);
|
|
11906
|
-
try {
|
|
11907
|
-
const stat2 = await lstat(target);
|
|
11908
|
-
if (stat2.isSymbolicLink()) {
|
|
11909
|
-
out.push({ path: requested, removed: false, reason: "a symlink; what it points at is not touched" });
|
|
11910
|
-
continue;
|
|
11911
|
-
}
|
|
11912
|
-
if (stat2.isDirectory()) {
|
|
11913
|
-
out.push({ path: requested, removed: false, reason: "a directory; name the files in it" });
|
|
11914
|
-
continue;
|
|
11915
|
-
}
|
|
11916
|
-
await unlink(target);
|
|
11917
|
-
out.push({ path: requested, removed: true });
|
|
11918
|
-
} catch (err) {
|
|
11919
|
-
const code = err.code;
|
|
11920
|
-
out.push({ path: requested, removed: false, reason: code === "ENOENT" ? "not there" : err instanceof Error ? err.message : String(err) });
|
|
11921
|
-
}
|
|
11922
|
-
}
|
|
11923
|
-
return out;
|
|
11924
|
-
}
|
|
11925
|
-
|
|
11926
11928
|
// ../memory-mcp/src/problem-rules.ts
|
|
11927
11929
|
import { z as z20 } from "zod";
|
|
11928
11930
|
var STATEMENT_MAX = 300;
|
|
@@ -12297,7 +12299,7 @@ ${renderMap()}`,
|
|
|
12297
12299
|
}
|
|
12298
12300
|
},
|
|
12299
12301
|
async ({ ref, outcome, note }) => {
|
|
12300
|
-
return json(await
|
|
12302
|
+
return json(await finishSolution(ctx, ref, outcome, note));
|
|
12301
12303
|
}
|
|
12302
12304
|
);
|
|
12303
12305
|
server.registerTool(
|
|
@@ -12397,20 +12399,20 @@ ${renderMap()}`,
|
|
|
12397
12399
|
}
|
|
12398
12400
|
);
|
|
12399
12401
|
server.registerTool(
|
|
12400
|
-
"
|
|
12402
|
+
"build_plan",
|
|
12401
12403
|
{
|
|
12402
|
-
description:
|
|
12403
|
-
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) }
|
|
12404
12406
|
},
|
|
12405
|
-
async ({
|
|
12406
|
-
|
|
12407
|
-
|
|
12408
|
-
|
|
12409
|
-
|
|
12410
|
-
|
|
12411
|
-
}
|
|
12412
|
-
|
|
12413
|
-
}
|
|
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")
|
|
12414
12416
|
);
|
|
12415
12417
|
server.registerTool(
|
|
12416
12418
|
"web_search",
|
|
@@ -13197,7 +13199,7 @@ async function markFast(ctx, problemRef) {
|
|
|
13197
13199
|
await logActivity2(ctx, "problems.fast", `${problem.ref} is one small change`);
|
|
13198
13200
|
return { problem: problem.ref, fast: true };
|
|
13199
13201
|
}
|
|
13200
|
-
async function
|
|
13202
|
+
async function finishSolution(ctx, ref, outcome, note) {
|
|
13201
13203
|
if (ctx.actor === "mapping-review") throw new Error("A targeted collection cannot decide solution outcomes.");
|
|
13202
13204
|
const upper = ref.toUpperCase();
|
|
13203
13205
|
const { data: current } = await ctx.client.from("solutions").select("status").eq("project_id", ctx.projectId).eq("ref", upper).is("superseded_by", null).maybeSingle();
|
|
@@ -13495,7 +13497,8 @@ var MEMORY_TOOL_NAMES = [
|
|
|
13495
13497
|
"play_store_search",
|
|
13496
13498
|
"list_market",
|
|
13497
13499
|
"write_market",
|
|
13498
|
-
"
|
|
13500
|
+
"build_plan",
|
|
13501
|
+
"build_step",
|
|
13499
13502
|
// Talk to Nano (docs/✅nanopm-68-talk-to-nano.md): the map read, and changed as the founder asked.
|
|
13500
13503
|
...TALK_TOOLS
|
|
13501
13504
|
];
|
|
@@ -13506,7 +13509,7 @@ async function startMemoryServer(env = process.env) {
|
|
|
13506
13509
|
}
|
|
13507
13510
|
|
|
13508
13511
|
// src/commands/add.ts
|
|
13509
|
-
import { readFile as
|
|
13512
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
13510
13513
|
|
|
13511
13514
|
// src/project.ts
|
|
13512
13515
|
import { execFile as execFile7 } from "child_process";
|
|
@@ -13514,11 +13517,11 @@ import { basename } from "path";
|
|
|
13514
13517
|
import { promisify as promisify7 } from "util";
|
|
13515
13518
|
|
|
13516
13519
|
// src/local.ts
|
|
13517
|
-
import { readFile as
|
|
13518
|
-
import { join as
|
|
13520
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
13521
|
+
import { join as join14 } from "path";
|
|
13519
13522
|
async function localDaemonId() {
|
|
13520
13523
|
try {
|
|
13521
|
-
const parsed = JSON.parse(await
|
|
13524
|
+
const parsed = JSON.parse(await readFile10(join14(nanopmHome(), "daemon.json"), "utf8"));
|
|
13522
13525
|
return parsed.id ?? null;
|
|
13523
13526
|
} catch {
|
|
13524
13527
|
return null;
|
|
@@ -13653,7 +13656,7 @@ async function add(file, opts) {
|
|
|
13653
13656
|
text2 = await readStdin();
|
|
13654
13657
|
} else {
|
|
13655
13658
|
try {
|
|
13656
|
-
text2 = await
|
|
13659
|
+
text2 = await readFile11(file, "utf8");
|
|
13657
13660
|
} catch {
|
|
13658
13661
|
throw new Error(`could not read ${file}`);
|
|
13659
13662
|
}
|
|
@@ -13668,7 +13671,7 @@ async function add(file, opts) {
|
|
|
13668
13671
|
}
|
|
13669
13672
|
|
|
13670
13673
|
// src/commands/run.ts
|
|
13671
|
-
import { readFile as
|
|
13674
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
13672
13675
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
13673
13676
|
import { setTimeout as sleep3 } from "timers/promises";
|
|
13674
13677
|
|
|
@@ -13835,7 +13838,7 @@ function phaseOf(tool) {
|
|
|
13835
13838
|
if (name === "query") return "data";
|
|
13836
13839
|
if (name === "web_search" || name === "web_fetch") return "web";
|
|
13837
13840
|
if (REVIEW_WRITES.has(name)) return "review";
|
|
13838
|
-
if (name === "
|
|
13841
|
+
if (name === "build_plan" || name === "build_step") return "build";
|
|
13839
13842
|
return null;
|
|
13840
13843
|
}
|
|
13841
13844
|
function touched(tool, input) {
|
|
@@ -14000,7 +14003,7 @@ function questionBlock(p) {
|
|
|
14000
14003
|
|
|
14001
14004
|
// src/commands/run.ts
|
|
14002
14005
|
async function answersFromFile(path) {
|
|
14003
|
-
const raw = JSON.parse(await
|
|
14006
|
+
const raw = JSON.parse(await readFile12(path, "utf8"));
|
|
14004
14007
|
const entries = raw.map((a) => typeof a === "string" ? { answer: a } : a);
|
|
14005
14008
|
return scriptedAnswerer(entries);
|
|
14006
14009
|
}
|
|
@@ -14223,15 +14226,15 @@ async function askOne(auth, projectId, question, daemonId, opts, out) {
|
|
|
14223
14226
|
// src/commands/daemon-service.ts
|
|
14224
14227
|
import { execFile as execFile8 } from "child_process";
|
|
14225
14228
|
import { existsSync as existsSync5 } from "fs";
|
|
14226
|
-
import { mkdir as
|
|
14227
|
-
import { homedir as
|
|
14228
|
-
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";
|
|
14229
14232
|
import { promisify as promisify8 } from "util";
|
|
14230
14233
|
|
|
14231
14234
|
// package.json
|
|
14232
14235
|
var package_default = {
|
|
14233
14236
|
name: "@nanopm/cli",
|
|
14234
|
-
version: "0.1.
|
|
14237
|
+
version: "0.1.11",
|
|
14235
14238
|
description: "An autonomous product manager for builders who run several small products. Runs on your machine, drives your own coding harness.",
|
|
14236
14239
|
type: "module",
|
|
14237
14240
|
license: "MIT",
|
|
@@ -14283,10 +14286,10 @@ var VERSION = package_default.version;
|
|
|
14283
14286
|
var execFileAsync8 = promisify8(execFile8);
|
|
14284
14287
|
var LAUNCHD_LABEL = "pm.nano.daemon";
|
|
14285
14288
|
function plistPath() {
|
|
14286
|
-
return
|
|
14289
|
+
return join15(homedir5(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
14287
14290
|
}
|
|
14288
14291
|
function daemonLogPath() {
|
|
14289
|
-
return
|
|
14292
|
+
return join15(nanopmHome(), "daemon.log");
|
|
14290
14293
|
}
|
|
14291
14294
|
function renderPlist(opts) {
|
|
14292
14295
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -14345,13 +14348,13 @@ async function installDaemon(out = (l) => console.log(l), opts = {}) {
|
|
|
14345
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.`);
|
|
14346
14349
|
const bin = process.argv[1];
|
|
14347
14350
|
if (!bin) throw new Error("cannot locate the nanopm binary");
|
|
14348
|
-
const home =
|
|
14349
|
-
const path = [process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
14350
|
-
await
|
|
14351
|
-
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 });
|
|
14352
14355
|
if (opts.shared !== void 0) await rememberShared(opts.shared);
|
|
14353
14356
|
await found({ ...detection, authenticated: true });
|
|
14354
|
-
await
|
|
14357
|
+
await writeFile7(plistPath(), renderPlist({ node: process.execPath, bin, home, path, logPath: daemonLogPath(), nanopmHome: nanopmHome() }), { mode: 420 });
|
|
14355
14358
|
const target = guiTarget();
|
|
14356
14359
|
await execFileAsync8("launchctl", ["bootout", target, plistPath()]).catch(() => void 0);
|
|
14357
14360
|
await execFileAsync8("launchctl", ["bootstrap", target, plistPath()]);
|
|
@@ -14372,7 +14375,7 @@ async function uninstallDaemon(out = (l) => console.log(l)) {
|
|
|
14372
14375
|
requireMac();
|
|
14373
14376
|
const target = guiTarget();
|
|
14374
14377
|
await execFileAsync8("launchctl", ["bootout", target, plistPath()]).catch(() => void 0);
|
|
14375
|
-
await
|
|
14378
|
+
await rm6(plistPath(), { force: true });
|
|
14376
14379
|
out(`Removed ${LAUNCHD_LABEL}.`);
|
|
14377
14380
|
}
|
|
14378
14381
|
async function daemonStatus(out = (l) => console.log(l)) {
|
|
@@ -14399,7 +14402,7 @@ async function daemonStatus(out = (l) => console.log(l)) {
|
|
|
14399
14402
|
}
|
|
14400
14403
|
async function daemonLogs(lines = 50) {
|
|
14401
14404
|
try {
|
|
14402
|
-
const text2 = await
|
|
14405
|
+
const text2 = await readFile13(daemonLogPath(), "utf8");
|
|
14403
14406
|
return text2.trimEnd().split("\n").slice(-lines).join("\n");
|
|
14404
14407
|
} catch {
|
|
14405
14408
|
return `no log at ${daemonLogPath()} yet`;
|
|
@@ -14407,41 +14410,41 @@ async function daemonLogs(lines = 50) {
|
|
|
14407
14410
|
}
|
|
14408
14411
|
async function localWebSearch() {
|
|
14409
14412
|
try {
|
|
14410
|
-
const parsed = JSON.parse(await
|
|
14413
|
+
const parsed = JSON.parse(await readFile13(join15(nanopmHome(), "daemon.json"), "utf8"));
|
|
14411
14414
|
return parsed.webSearch === true;
|
|
14412
14415
|
} catch {
|
|
14413
14416
|
return false;
|
|
14414
14417
|
}
|
|
14415
14418
|
}
|
|
14416
14419
|
async function rememberWebSearch(webSearch2) {
|
|
14417
|
-
const path =
|
|
14420
|
+
const path = join15(nanopmHome(), "daemon.json");
|
|
14418
14421
|
let parsed = {};
|
|
14419
14422
|
try {
|
|
14420
|
-
parsed = JSON.parse(await
|
|
14423
|
+
parsed = JSON.parse(await readFile13(path, "utf8"));
|
|
14421
14424
|
} catch {
|
|
14422
14425
|
parsed = {};
|
|
14423
14426
|
}
|
|
14424
|
-
await
|
|
14425
|
-
await
|
|
14427
|
+
await mkdir7(nanopmHome(), { recursive: true, mode: 448 });
|
|
14428
|
+
await writeFile7(path, JSON.stringify({ ...parsed, webSearch: webSearch2 }, null, 2) + "\n", { mode: 384 });
|
|
14426
14429
|
}
|
|
14427
14430
|
async function localShared() {
|
|
14428
14431
|
try {
|
|
14429
|
-
const parsed = JSON.parse(await
|
|
14432
|
+
const parsed = JSON.parse(await readFile13(join15(nanopmHome(), "daemon.json"), "utf8"));
|
|
14430
14433
|
return Boolean(parsed.shared);
|
|
14431
14434
|
} catch {
|
|
14432
14435
|
return false;
|
|
14433
14436
|
}
|
|
14434
14437
|
}
|
|
14435
14438
|
async function rememberShared(shared) {
|
|
14436
|
-
const path =
|
|
14439
|
+
const path = join15(nanopmHome(), "daemon.json");
|
|
14437
14440
|
let parsed = {};
|
|
14438
14441
|
try {
|
|
14439
|
-
parsed = JSON.parse(await
|
|
14442
|
+
parsed = JSON.parse(await readFile13(path, "utf8"));
|
|
14440
14443
|
} catch {
|
|
14441
14444
|
parsed = {};
|
|
14442
14445
|
}
|
|
14443
|
-
await
|
|
14444
|
-
await
|
|
14446
|
+
await mkdir7(nanopmHome(), { recursive: true, mode: 448 });
|
|
14447
|
+
await writeFile7(path, JSON.stringify({ ...parsed, shared }, null, 2) + "\n", { mode: 384 });
|
|
14445
14448
|
}
|
|
14446
14449
|
|
|
14447
14450
|
// src/format/log.ts
|
|
@@ -14684,497 +14687,141 @@ The install line does the same: curl -fsSL https://nanopm.app/install | sh`);
|
|
|
14684
14687
|
out(await restartDaemon() ? `Updated ${VERSION} \u2192 ${latest}; the daemon restarted on it.` : `Updated ${VERSION} \u2192 ${latest}.`);
|
|
14685
14688
|
}
|
|
14686
14689
|
|
|
14687
|
-
// src/
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14691
|
-
building: { mark: "\u25D4", colour: ANSI.blue, label: "building" },
|
|
14692
|
-
rejected: { mark: "\u2298", colour: ANSI.red, label: "rejected" },
|
|
14693
|
-
done: { mark: "\u25CF", colour: ANSI.green, label: "done" },
|
|
14694
|
-
dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
|
|
14695
|
-
};
|
|
14696
|
-
var WORK_KINDS = /* @__PURE__ */ new Set(["branch", "git", "url", "job"]);
|
|
14697
|
-
function renderRoadmapItem(item, opts, problem, builtBy) {
|
|
14698
|
-
const paint = painter(opts.color);
|
|
14699
|
-
const shape2 = SHAPE[item.status] ?? { mark: "\xB7", colour: "", label: item.status };
|
|
14700
|
-
const out = [`${paint(shape2.colour, shape2.mark)} ${paint(ANSI.bold, item.ref)} ${item.title}`];
|
|
14701
|
-
if (problem) out.push(paint(ANSI.dim, ` answers ${problem.ref} ${clip(problem.statement, Math.max(30, columns() - 20))}`));
|
|
14702
|
-
out.push("");
|
|
14703
|
-
for (const line of wrap(item.reasoning, Math.max(40, columns() - 4))) out.push(` ${line}`);
|
|
14704
|
-
out.push("");
|
|
14705
|
-
const facts = [
|
|
14706
|
-
["status", shape2.label + (item.outcome ? ` \xB7 ${item.outcome}${item.outcome_note ? `: ${item.outcome_note}` : ""}` : "")],
|
|
14707
|
-
["rank", String(item.rank)],
|
|
14708
|
-
["who", item.actor === "founder" ? "you" : "NanoPM"],
|
|
14709
|
-
["bets on", item.assumption ?? "\u2014"],
|
|
14710
|
-
["we'll know", item.eval ?? "\u2014"]
|
|
14711
|
-
];
|
|
14712
|
-
for (const [k, v] of facts) out.push(paint(ANSI.dim, ` ${k.padEnd(9)} ${v}`));
|
|
14713
|
-
const sources2 = item.sources ?? [];
|
|
14714
|
-
const restsOn = sources2.filter((s) => !WORK_KINDS.has(s.kind));
|
|
14715
|
-
if (restsOn.length) {
|
|
14716
|
-
out.push(paint(ANSI.dim, ` ${"rests on".padEnd(9)} ${restsOn.map((s) => `${s.kind}:${s.ref}`).join(", ")}`));
|
|
14717
|
-
}
|
|
14718
|
-
const where = renderWhere(sources2);
|
|
14719
|
-
if (where) out.push(paint(ANSI.dim, ` ${"where".padEnd(9)} ${where}${builtBy ? ` \xB7 built by ${builtBy}` : ""}`));
|
|
14720
|
-
return out.join("\n");
|
|
14721
|
-
}
|
|
14722
|
-
function renderWhere(sources2) {
|
|
14723
|
-
const branch = sources2.find((s) => s.kind === "branch")?.ref;
|
|
14724
|
-
const commit = sources2.find((s) => s.kind === "git")?.ref;
|
|
14725
|
-
const url = sources2.find((s) => s.kind === "url")?.ref;
|
|
14726
|
-
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";
|
|
14727
14694
|
}
|
|
14728
|
-
var
|
|
14729
|
-
|
|
14730
|
-
|
|
14731
|
-
|
|
14732
|
-
merge: { text: "merged into", colour: ANSI.blue },
|
|
14733
|
-
separate: { text: "kept apart", colour: ANSI.blue },
|
|
14734
|
-
weight: { text: "weighed", colour: ANSI.blue },
|
|
14735
|
-
area: { text: "filed", colour: ANSI.blue },
|
|
14736
|
-
accept: { text: "accepted", colour: ANSI.green },
|
|
14737
|
-
reject: { text: "rejected", colour: ANSI.red },
|
|
14738
|
-
first: { text: "put first", colour: ANSI.blue },
|
|
14739
|
-
done: { text: "did", colour: ANSI.green },
|
|
14740
|
-
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."
|
|
14741
14699
|
};
|
|
14742
|
-
function
|
|
14743
|
-
const
|
|
14744
|
-
|
|
14745
|
-
const
|
|
14746
|
-
|
|
14747
|
-
const
|
|
14748
|
-
const
|
|
14749
|
-
|
|
14750
|
-
const by = row.by ? paint(ANSI.dim, ` \xB7 by ${row.by}`) : "";
|
|
14751
|
-
out.push(`${paint(ANSI.dim, when)} ${paint(verb.colour, verb.text)} ${about}${by}`.trimEnd());
|
|
14752
|
-
if (row.body) {
|
|
14753
|
-
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]}`);
|
|
14754
14708
|
}
|
|
14709
|
+
lines.push("", " nanopm autonomy <level>");
|
|
14710
|
+
return lines.join("\n");
|
|
14755
14711
|
}
|
|
14756
|
-
|
|
14757
|
-
|
|
14758
|
-
|
|
14759
|
-
|
|
14760
|
-
|
|
14761
|
-
|
|
14762
|
-
|
|
14763
|
-
|
|
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");
|
|
14764
14727
|
}
|
|
14765
|
-
function
|
|
14766
|
-
const
|
|
14767
|
-
const
|
|
14768
|
-
const
|
|
14769
|
-
const
|
|
14770
|
-
|
|
14771
|
-
|
|
14772
|
-
|
|
14773
|
-
|
|
14774
|
-
|
|
14775
|
-
|
|
14776
|
-
|
|
14777
|
-
|
|
14778
|
-
|
|
14779
|
-
|
|
14780
|
-
}, []);
|
|
14781
|
-
out.push("", ...legend);
|
|
14782
|
-
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."));
|
|
14783
|
-
const refWidth = Math.max(5, ...board.cards.map((c) => c.pick.ref.length));
|
|
14784
|
-
const gutter = " ".repeat(refWidth + 4);
|
|
14785
|
-
const titleWidth = Math.min(Math.max(...board.cards.map((c) => c.pick.title.length)), width - gutter.length - 8);
|
|
14786
|
-
for (const h of HORIZONS) {
|
|
14787
|
-
const column = board.cards.filter((c) => c.horizon === h);
|
|
14788
|
-
out.push("", `${paint(ANSI.bold, HORIZON_WORD[h])} ${paint(ANSI.dim, String(column.length))}`);
|
|
14789
|
-
if (!column.length) out.push(paint(ANSI.dim, ` Move a card here: nanopm roadmap move <ref> ${h}`));
|
|
14790
|
-
for (const c of column) {
|
|
14791
|
-
const t = themes.get(c.branch.id);
|
|
14792
|
-
out.push(` ${paint(ANSI.dim, c.pick.ref.padEnd(refWidth))} ${paint(t.colour, "\u25CF")} ${paint(ANSI.dim, t.name)}`);
|
|
14793
|
-
out.push(`${gutter}${clip(c.pick.title, titleWidth).padEnd(titleWidth)} ${sizeWord(c.pick.size, paint)}`.trimEnd());
|
|
14794
|
-
const depth = board.working.get(c.leaf.id);
|
|
14795
|
-
if (depth) out.push(`${gutter}${paint(ANSI.blue, "\u25CF")} ${paint(ANSI.dim, WORK_WORD[depth])}`);
|
|
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");
|
|
14796
14743
|
}
|
|
14744
|
+
return [`${project.name} verifies with:`, "", ` ${current}`, "", ' nanopm verify "<command>" \xB7 nanopm verify --clear'].join("\n");
|
|
14797
14745
|
}
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14803
|
-
);
|
|
14804
|
-
return out.join("\n");
|
|
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");
|
|
14805
14752
|
}
|
|
14806
|
-
|
|
14753
|
+
|
|
14754
|
+
// src/format/opportunities.ts
|
|
14755
|
+
function renderOpportunities(rows2, lens, opts) {
|
|
14807
14756
|
const paint = painter(opts.color);
|
|
14808
14757
|
const width = Math.max(40, columns());
|
|
14809
|
-
const theme = at.branch ? themesOf(at.tree).get(at.branch.id) : void 0;
|
|
14810
14758
|
const out = [];
|
|
14811
|
-
if (
|
|
14812
|
-
|
|
14813
|
-
out.push(
|
|
14814
|
-
|
|
14815
|
-
|
|
14816
|
-
|
|
14817
|
-
|
|
14818
|
-
const solves = wrap(at.leaf.problem, width - 6 - at.leaf.ref.length - 2);
|
|
14819
|
-
solves[solves.length - 1] += ` ${paint(ANSI.dim, at.leaf.ref)}`;
|
|
14820
|
-
for (const line of solves) out.push(` ${line}`);
|
|
14821
|
-
if (at.branch && at.branch.id !== at.leaf.id) {
|
|
14822
|
-
for (const line of wrap(`Part of: ${at.branch.problem} ${at.branch.ref}`, width - 6)) out.push(` ${paint(ANSI.dim, line)}`);
|
|
14823
|
-
}
|
|
14824
|
-
} else {
|
|
14825
|
-
out.push(paint(ANSI.dim, " The problem it answered is no longer on the map."));
|
|
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
|
-
const
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
|
|
14833
|
-
|
|
14834
|
-
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
|
|
14838
|
-
|
|
14839
|
-
|
|
14840
|
-
|
|
14841
|
-
|
|
14842
|
-
|
|
14843
|
-
|
|
14844
|
-
|
|
14845
|
-
|
|
14846
|
-
|
|
14847
|
-
|
|
14848
|
-
|
|
14849
|
-
|
|
14850
|
-
|
|
14851
|
-
out.push("");
|
|
14852
|
-
if (s.status === "rejected") out.push(paint(ANSI.dim, " Thrown out."));
|
|
14853
|
-
else if (card?.pick.id === s.id) {
|
|
14854
|
-
const column = at.cards.filter((c) => c.horizon === card.horizon);
|
|
14855
|
-
const yours = s.placed_by === "founder" ? ", where you put it" : "";
|
|
14856
|
-
out.push(paint(ANSI.dim, ` On the board: ${HORIZON_WORD[card.horizon]}, ${column.indexOf(card) + 1} of ${column.length}${yours}.`));
|
|
14857
|
-
} else out.push(paint(ANSI.dim, card ? ` Not on the board: ${card.pick.ref} is this problem's card.` : " Not on the board."));
|
|
14858
|
-
if (others.length) out.push(paint(ANSI.dim, ` Other solutions to this problem: ${others.map((o) => o.ref).join(", ")}.`));
|
|
14859
|
-
if (s.status === "proposed") {
|
|
14860
|
-
const move = card?.pick.id === s.id ? `nanopm roadmap move ${s.ref} now|next|later [position] \xB7 ` : "";
|
|
14861
|
-
out.push(paint(ANSI.dim, ` ${move}nanopm reject ${s.ref} ["why"]`));
|
|
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}`) : ""}`);
|
|
14862
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]`.'));
|
|
14863
14793
|
return out.join("\n");
|
|
14864
14794
|
}
|
|
14865
14795
|
|
|
14866
|
-
// src/format/
|
|
14867
|
-
var
|
|
14868
|
-
|
|
14869
|
-
confirmed: { mark: "\u25C9", colour: ANSI.amber, label: "confirmed" },
|
|
14870
|
-
addressed: { mark: "\u25CF", colour: ANSI.green, label: "addressed" },
|
|
14871
|
-
dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
|
|
14872
|
-
};
|
|
14873
|
-
function renderImportance(problem, opts) {
|
|
14874
|
-
const paint = painter(opts.color);
|
|
14875
|
-
if (problem.importance_basis === "evidence") {
|
|
14876
|
-
return paint(ANSI.bold, String(Math.round(problem.importance)).padStart(3));
|
|
14877
|
-
}
|
|
14878
|
-
const value = problem.importance;
|
|
14879
|
-
const word = value >= 67 ? "high" : value >= 34 ? "med" : value > 0 ? "low" : "\u2014";
|
|
14880
|
-
return paint(ANSI.dim, `~${word}`.padStart(5));
|
|
14881
|
-
}
|
|
14882
|
-
function renderProblems(problems2, duplicates, opts) {
|
|
14883
|
-
const paint = painter(opts.color);
|
|
14884
|
-
if (!problems2.length) {
|
|
14885
|
-
return [
|
|
14886
|
-
"No problems on the list yet.",
|
|
14887
|
-
"",
|
|
14888
|
-
paint(ANSI.dim, " nanopm review reads your data, then works out what they say about your users"),
|
|
14889
|
-
paint(ANSI.dim, " nanopm opportunities what to work on, for your objective")
|
|
14890
|
-
].join("\n");
|
|
14891
|
-
}
|
|
14892
|
-
const refWidth = Math.max(...problems2.map((p) => p.ref.length));
|
|
14893
|
-
const out = [];
|
|
14894
|
-
const map = groupProblemMap(problems2.map((p) => ({ ...p, parentRef: p.parent_ref })));
|
|
14895
|
-
const ordered = [...map.branches.flatMap((b) => [b.parent, ...b.children]), ...map.unplaced];
|
|
14896
|
-
for (const problem of ordered) {
|
|
14897
|
-
const shape2 = SHAPE2[problem.status] ?? { mark: "\xB7", colour: ANSI.dim, label: problem.status };
|
|
14898
|
-
const score = renderImportance(problem, opts);
|
|
14899
|
-
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))}`;
|
|
14900
|
-
out.push(head);
|
|
14901
|
-
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"}`));
|
|
14902
|
-
const indent = ` ${" ".repeat(refWidth)} `;
|
|
14903
|
-
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";
|
|
14904
|
-
out.push(paint(ANSI.dim, indent + clip(tail2, Math.max(40, columns() - indent.length))));
|
|
14905
|
-
}
|
|
14906
|
-
const unproven = problems2.filter((p) => p.proofs === 0).length;
|
|
14907
|
-
if (unproven) {
|
|
14908
|
-
out.push("");
|
|
14909
|
-
out.push(
|
|
14910
|
-
paint(
|
|
14911
|
-
ANSI.dim,
|
|
14912
|
-
`${unproven} of ${problems2.length} rest on judgement, not proof. A tilde marks those.`
|
|
14913
|
-
)
|
|
14914
|
-
);
|
|
14915
|
-
}
|
|
14916
|
-
if (duplicates.length) {
|
|
14917
|
-
out.push("");
|
|
14918
|
-
for (const pair of duplicates.slice(0, 3)) {
|
|
14919
|
-
const why = pair.flagged ? `NanoPM is not sure ${pair.a} and ${pair.b} are different problems` : `${pair.a} and ${pair.b} read alike`;
|
|
14920
|
-
out.push(paint(ANSI.amber, `? ${why}`));
|
|
14921
|
-
}
|
|
14922
|
-
out.push(paint(ANSI.dim, ` nanopm decisions why \xB7 nanopm problems merge ${duplicates[0].b} ${duplicates[0].a}`));
|
|
14923
|
-
}
|
|
14924
|
-
if (opts.manualMerges !== void 0) {
|
|
14925
|
-
out.push("");
|
|
14926
|
-
out.push(
|
|
14927
|
-
paint(
|
|
14928
|
-
ANSI.dim,
|
|
14929
|
-
`${opts.manualMerges} merge${opts.manualMerges === 1 ? "" : "s"} you have had to make by hand.`
|
|
14930
|
-
)
|
|
14931
|
-
);
|
|
14932
|
-
}
|
|
14933
|
-
out.push("");
|
|
14934
|
-
out.push(paint(ANSI.dim, " nanopm problems <ref> \xB7 nanopm problems merge <from> <into> \xB7 nanopm problems weight <ref> <0-100>"));
|
|
14935
|
-
return out.join("\n");
|
|
14936
|
-
}
|
|
14937
|
-
function renderProblem(problem, evidence2, addressedBy, opts) {
|
|
14796
|
+
// src/format/needs.ts
|
|
14797
|
+
var MARK = { observed: "\u25CF", stated: "\u2013", inferred: "\u25CB" };
|
|
14798
|
+
function renderNeeds(persona, tree, opts) {
|
|
14938
14799
|
const paint = painter(opts.color);
|
|
14939
|
-
const
|
|
14940
|
-
const width = columns();
|
|
14800
|
+
const width = Math.max(40, columns());
|
|
14941
14801
|
const out = [];
|
|
14942
|
-
|
|
14943
|
-
out.push(
|
|
14944
|
-
|
|
14945
|
-
|
|
14946
|
-
|
|
14947
|
-
const m = problem.mapping ?? null;
|
|
14948
|
-
if (m) {
|
|
14949
|
-
const story = problemStory(m, { proofs: problem.proofs, kinds: problem.proof_kinds ?? [], provenance: problem.provenance ?? "inferred", authorKind: problem.author_kind ?? "nanopm", createdAt: problem.created_at ?? "" });
|
|
14950
|
-
out.push(...wrap(story.story, width));
|
|
14951
|
-
out.push("");
|
|
14952
|
-
out.push(...wrap(story.doubt, width).map((l) => paint(ANSI.dim, l)));
|
|
14953
|
-
out.push(...wrap(story.assumes ? `${story.test} ${story.assumes}` : story.test, width).map((l) => paint(ANSI.dim, l)));
|
|
14954
|
-
out.push("");
|
|
14955
|
-
out.push(paint(ANSI.dim, "how I got here"));
|
|
14956
|
-
facts.push(["kind", `${m.level === 1 ? "a need" : "a difficulty"} \xB7 ${m.knowledge} \xB7 ${m.origin === "expert_inference" ? "expert inference" : "source analysis"}`]);
|
|
14957
|
-
if (m.parent_reason) facts.push(["grouping", m.parent_reason]);
|
|
14958
|
-
}
|
|
14959
|
-
if (problem.segment) facts.push(["who", problem.segment]);
|
|
14960
|
-
facts.push([
|
|
14961
|
-
"weight",
|
|
14962
|
-
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}` : ""}`
|
|
14963
|
-
]);
|
|
14964
|
-
if (problem.parent_ref) facts.push(["part of", problem.parent_ref]);
|
|
14965
|
-
for (const [label, value] of facts) {
|
|
14966
|
-
out.push(`${paint(ANSI.dim, label.padEnd(9))}${clip(value, width - 9)}`);
|
|
14967
|
-
}
|
|
14968
|
-
if (evidence2.length) {
|
|
14969
|
-
out.push("");
|
|
14970
|
-
out.push(paint(ANSI.dim, "sources"));
|
|
14971
|
-
for (const item of evidence2) {
|
|
14972
|
-
const when = (item.occurred_at ?? item.created_at).slice(0, 10);
|
|
14973
|
-
const where = item.metric_key ?? item.source_ref ?? item.kind;
|
|
14974
|
-
for (const line of wrap(item.body, width - 2)) out.push(` ${line}`);
|
|
14975
|
-
out.push(paint(ANSI.dim, ` \u2014 ${item.relation ?? "supports"} \xB7 ${item.kind} \xB7 ${where} \xB7 ${when}`));
|
|
14976
|
-
}
|
|
14977
|
-
} else {
|
|
14978
|
-
out.push("");
|
|
14979
|
-
out.push(paint(ANSI.dim, "No proof yet. This ranking is a judgement, and the next review may settle it."));
|
|
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");
|
|
14980
14807
|
}
|
|
14981
|
-
|
|
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) {
|
|
14982
14820
|
out.push("");
|
|
14983
|
-
|
|
14984
|
-
for (const item of addressedBy) {
|
|
14985
|
-
out.push(` ${paint(ANSI.dim, item.ref)} ${clip(item.title, width - 12)} ${paint(ANSI.dim, `(${item.status})`)}`);
|
|
14986
|
-
}
|
|
14821
|
+
line(need, 0);
|
|
14987
14822
|
}
|
|
14988
|
-
out.push("");
|
|
14989
|
-
out.push(paint(ANSI.dim,
|
|
14990
|
-
return out.join("\n");
|
|
14991
|
-
}
|
|
14992
|
-
|
|
14993
|
-
// src/format/tree.ts
|
|
14994
|
-
var SOLUTION_SHAPE = {
|
|
14995
|
-
proposed: { mark: "\u25CB", colour: ANSI.amber, label: "proposed" },
|
|
14996
|
-
accepted: { mark: "\u25D0", colour: ANSI.green, label: "accepted" },
|
|
14997
|
-
building: { mark: "\u25D4", colour: ANSI.blue, label: "building" },
|
|
14998
|
-
done: { mark: "\u25CF", colour: ANSI.green, label: "done" },
|
|
14999
|
-
rejected: { mark: "\u2298", colour: ANSI.red, label: "rejected" },
|
|
15000
|
-
dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
|
|
15001
|
-
};
|
|
15002
|
-
var PROBLEM_SHAPE = {
|
|
15003
|
-
candidate: { mark: "\u25CB", colour: ANSI.dim },
|
|
15004
|
-
confirmed: { mark: "\u25C9", colour: ANSI.amber },
|
|
15005
|
-
addressed: { mark: "\u25CF", colour: ANSI.green },
|
|
15006
|
-
dropped: { mark: "\u2298", colour: ANSI.dim }
|
|
15007
|
-
};
|
|
15008
|
-
var NO_AREA = "no area yet";
|
|
15009
|
-
var SHOWN = /* @__PURE__ */ new Set(["proposed", "accepted", "building", "done"]);
|
|
15010
|
-
function renderTree(problems2, solutions, strategy, opts) {
|
|
15011
|
-
const paint = painter(opts.color);
|
|
15012
|
-
const out = [];
|
|
15013
|
-
const width = Math.max(40, columns());
|
|
15014
|
-
if (strategy) {
|
|
15015
|
-
out.push(paint(ANSI.bold, "Strategy"));
|
|
15016
|
-
for (const line of wrap(strategy, width - 4)) out.push(` ${line}`);
|
|
15017
|
-
out.push("");
|
|
15018
|
-
}
|
|
15019
|
-
const shown = solutions.filter((s) => SHOWN.has(s.status));
|
|
15020
|
-
const open = problems2.filter((p) => p.status !== "dropped" && (!opts.alongside || shown.some((s) => s.problem_id === p.id)));
|
|
15021
|
-
if (opts.alongside && !open.length) return "";
|
|
15022
|
-
if (!open.length && !shown.length) {
|
|
15023
|
-
out.push("No problems yet. `nanopm opportunities` shows what to work on.");
|
|
15024
|
-
return out.join("\n");
|
|
15025
|
-
}
|
|
15026
|
-
const byProblem = /* @__PURE__ */ new Map();
|
|
15027
|
-
for (const s of shown) byProblem.set(s.problem_id, [...byProblem.get(s.problem_id) ?? [], s]);
|
|
15028
|
-
const refWidth = Math.max(...[...open.map((p) => p.ref), ...shown.map((s) => s.ref)].map((r) => r.length), 5);
|
|
15029
|
-
for (const [area, rows2] of groupByArea(open)) {
|
|
15030
|
-
out.push(paint(ANSI.bold, area));
|
|
15031
|
-
for (const p of rows2) {
|
|
15032
|
-
const shape2 = PROBLEM_SHAPE[p.status] ?? { mark: "\xB7", colour: "" };
|
|
15033
|
-
const kind = p.kind === "founder" ? paint(ANSI.dim, " (founder)") : "";
|
|
15034
|
-
const proof = p.proofs ? `${p.proofs} proof${p.proofs === 1 ? "" : "s"}` : "no proof";
|
|
15035
|
-
const tail2 = `${renderImportance(p, opts)} ${paint(ANSI.dim, proof)}`;
|
|
15036
|
-
const head = ` ${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, p.ref.padEnd(refWidth))} `;
|
|
15037
|
-
out.push(head + clip(p.statement, Math.max(30, width - visibleLength(head) - visibleLength(tail2) - 2)) + kind + " " + tail2);
|
|
15038
|
-
const under = byProblem.get(p.id) ?? [];
|
|
15039
|
-
if (!under.length) {
|
|
15040
|
-
out.push(paint(ANSI.dim, ` ${" ".repeat(refWidth + 2)} no solution yet`));
|
|
15041
|
-
continue;
|
|
15042
|
-
}
|
|
15043
|
-
for (const s of under) out.push(...solutionLines(s, refWidth, width, paint));
|
|
15044
|
-
}
|
|
15045
|
-
out.push("");
|
|
15046
|
-
}
|
|
15047
|
-
const waiting = shown.filter((s) => s.status === "proposed").length;
|
|
15048
|
-
if (waiting) {
|
|
15049
|
-
out.push(`${waiting} waiting on you.`);
|
|
15050
|
-
out.push(paint(ANSI.dim, ' nanopm accept <ref> \xB7 nanopm reject <ref> "why" \xB7 nanopm roadmap <ref> \xB7 nanopm roadmap --board'));
|
|
15051
|
-
} else if (!opts.alongside) {
|
|
15052
|
-
out.push(paint(ANSI.dim, "Nothing waiting on you."));
|
|
15053
|
-
}
|
|
15054
|
-
return out.join("\n").trimEnd();
|
|
15055
|
-
}
|
|
15056
|
-
function solutionLines(s, refWidth, width, paint, indent = 6) {
|
|
15057
|
-
const shape2 = SOLUTION_SHAPE[s.status] ?? { mark: "\xB7", colour: "", label: s.status };
|
|
15058
|
-
const pad = " ".repeat(indent);
|
|
15059
|
-
const STATE = { accepted: "chosen", building: "building", done: "done" };
|
|
15060
|
-
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;
|
|
15061
|
-
const badge = tail2 ? paint(ANSI.dim, ` ${tail2}`) : "";
|
|
15062
|
-
const head = `${pad}${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, s.ref.padEnd(refWidth))} `;
|
|
15063
|
-
const lines = [head + clip(s.title, Math.max(30, width - visibleLength(head) - visibleLength(badge))) + badge];
|
|
15064
|
-
const reason2 = `${pad} ${" ".repeat(refWidth)} `;
|
|
15065
|
-
lines.push(paint(ANSI.dim, reason2 + clip(firstSentence(s.reasoning), Math.max(30, width - reason2.length))));
|
|
15066
|
-
return lines;
|
|
15067
|
-
}
|
|
15068
|
-
function groupByArea(problems2) {
|
|
15069
|
-
const groups = /* @__PURE__ */ new Map();
|
|
15070
|
-
for (const p of problems2) {
|
|
15071
|
-
const area = p.area ?? NO_AREA;
|
|
15072
|
-
groups.set(area, [...groups.get(area) ?? [], p]);
|
|
15073
|
-
}
|
|
15074
|
-
const named = [...groups.entries()].filter(([a]) => a !== NO_AREA);
|
|
15075
|
-
const rest = groups.get(NO_AREA);
|
|
15076
|
-
return rest ? [...named, [NO_AREA, rest]] : named;
|
|
15077
|
-
}
|
|
15078
|
-
var BOARD_COLUMNS = [
|
|
15079
|
-
{ status: "proposed", label: "Proposed" },
|
|
15080
|
-
{ status: "accepted", label: "Chosen" },
|
|
15081
|
-
{ status: "building", label: "Building" },
|
|
15082
|
-
{ status: "done", label: "Done" }
|
|
15083
|
-
];
|
|
15084
|
-
function renderBoard(problems2, solutions, opts) {
|
|
15085
|
-
const paint = painter(opts.color);
|
|
15086
|
-
const out = [];
|
|
15087
|
-
const byId = new Map(problems2.map((p) => [p.id, p]));
|
|
15088
|
-
const width = Math.max(40, columns());
|
|
15089
|
-
const refWidth = Math.max(...solutions.map((s) => s.ref.length), 5);
|
|
15090
|
-
for (const col of BOARD_COLUMNS) {
|
|
15091
|
-
const rows2 = solutions.filter((s) => s.status === col.status);
|
|
15092
|
-
out.push(`${paint(ANSI.bold, col.label)} ${paint(ANSI.dim, String(rows2.length))}`);
|
|
15093
|
-
if (!rows2.length) out.push(paint(ANSI.dim, " \u2014"));
|
|
15094
|
-
for (const s of rows2) {
|
|
15095
|
-
const shape2 = SOLUTION_SHAPE[s.status];
|
|
15096
|
-
const problem = byId.get(s.problem_id);
|
|
15097
|
-
const head = ` ${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, s.ref.padEnd(refWidth))} `;
|
|
15098
|
-
out.push(head + clip(s.title, Math.max(30, width - visibleLength(head))));
|
|
15099
|
-
const under = [problem ? `${problem.ref} ${clip(problem.statement, 50)}` : "its problem was dropped", s.assumption ? clip(s.assumption, 40) : null].filter(Boolean).join(" \xB7 ");
|
|
15100
|
-
out.push(paint(ANSI.dim, ` ${" ".repeat(refWidth + 2)} ${under}`));
|
|
15101
|
-
}
|
|
15102
|
-
out.push("");
|
|
15103
|
-
}
|
|
15104
|
-
return out.join("\n").trimEnd();
|
|
15105
|
-
}
|
|
15106
|
-
|
|
15107
|
-
// src/format/opportunities.ts
|
|
15108
|
-
function renderOpportunities(rows2, lens, opts) {
|
|
15109
|
-
const paint = painter(opts.color);
|
|
15110
|
-
const width = Math.max(40, columns());
|
|
15111
|
-
const out = [];
|
|
15112
|
-
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`.";
|
|
15113
|
-
out.push(`${paint(ANSI.bold, "Opportunities")} ${paint(ANSI.dim, `${lens.lever} \u2014 ${lens.sentence}`)}`);
|
|
15114
|
-
out.push(paint(ANSI.dim, lens.definition ? `Counts: ${lens.definition}` : "What it counts is not defined yet: drawn against the lever and your sentence."));
|
|
15115
|
-
if (opts.previous) out.push(paint(ANSI.amber, "Drawn for your previous objective. `nanopm opportunities run` draws it for this one."));
|
|
15116
|
-
if (!rows2.length) {
|
|
15117
|
-
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.");
|
|
15118
|
-
return out.join("\n");
|
|
15119
|
-
}
|
|
15120
|
-
const tree = liveTree(rows2);
|
|
15121
|
-
const live = rows2.filter(isLive);
|
|
15122
|
-
const evidenced = live.filter((r) => r.evidence_status === "evidence_backed").length;
|
|
15123
|
-
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.`));
|
|
15124
|
-
const who = (r) => r.personas.map((p) => opts.names?.get(p) ?? p).join(", ");
|
|
15125
|
-
const one = (r, place) => {
|
|
15126
|
-
const indent = " ".repeat(r.depth - 1);
|
|
15127
|
-
const yours = r.status === "confirmed" ? paint(ANSI.green, " yours") : "";
|
|
15128
|
-
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)}`);
|
|
15129
|
-
const pad = `${indent} `;
|
|
15130
|
-
const facts = [PRIORITY_WORD[r.priority], EVIDENCE_STATUS_WORD[r.evidence_status], NEXT_ACTION_WORD[r.next_action], who(r)].filter(Boolean).join(" \xB7 ");
|
|
15131
|
-
out.push(`${pad}${paint(ANSI.dim, facts)}`);
|
|
15132
|
-
const gist = r.description ? paragraphs(r.description)[0] : null;
|
|
15133
|
-
if (gist) for (const l of wrap(gist, width - pad.length)) out.push(`${pad}${l}`);
|
|
15134
|
-
if (r.learn) for (const l of wrap(`To learn more: ${r.learn}`, width - pad.length)) out.push(`${pad}${paint(ANSI.dim, l)}`);
|
|
15135
|
-
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}.`)}`);
|
|
15136
|
-
if (r.reconsider) for (const l of wrap(`Worth reconsidering: ${r.reconsider}`, width - pad.length)) out.push(`${pad}${paint(ANSI.amber, l)}`);
|
|
15137
|
-
for (const c of r.children) one(c, `${place}.${c.rank}`);
|
|
15138
|
-
};
|
|
15139
|
-
for (const r of tree) one(r, String(r.rank));
|
|
15140
|
-
const aside = rows2.filter((r) => r.status === "set_aside");
|
|
15141
|
-
if (aside.length) {
|
|
15142
|
-
out.push("", paint(ANSI.bold, `Set aside (${aside.length})`));
|
|
15143
|
-
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}`) : ""}`);
|
|
15144
|
-
}
|
|
15145
|
-
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]`.'));
|
|
15146
|
-
return out.join("\n");
|
|
15147
|
-
}
|
|
15148
|
-
|
|
15149
|
-
// src/format/needs.ts
|
|
15150
|
-
var MARK = { observed: "\u25CF", stated: "\u2013", inferred: "\u25CB" };
|
|
15151
|
-
function renderNeeds(persona, tree, opts) {
|
|
15152
|
-
const paint = painter(opts.color);
|
|
15153
|
-
const width = Math.max(40, columns());
|
|
15154
|
-
const out = [];
|
|
15155
|
-
const counts = needCounts(tree);
|
|
15156
|
-
out.push(`${paint(ANSI.bold, `${persona.name}\u2019s needs`)} ${paint(ANSI.dim, `${counts.needs} needs \xB7 ${counts.difficulties + counts.precise} difficulties`)}`);
|
|
15157
|
-
if (!tree.length) {
|
|
15158
|
-
out.push("", `Nothing mapped yet. \`nanopm needs --refresh ${persona.slug}\` maps it in depth; add --short for a quick pass.`);
|
|
15159
|
-
return out.join("\n");
|
|
15160
|
-
}
|
|
15161
|
-
const line = (n, depth) => {
|
|
15162
|
-
const indent = " ".repeat(depth);
|
|
15163
|
-
const lead = n.level === 1 ? `${n.emoji ?? " "} ` : `${MARK[n.provenance]} `;
|
|
15164
|
-
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"}`) : "";
|
|
15165
|
-
const state = n.status === "dropped" ? paint(ANSI.dim, " dropped") : n.status === "confirmed" || n.author_kind === "user" ? "" : paint(ANSI.amber, " to review");
|
|
15166
|
-
const label = n.status === "dropped" ? paint(ANSI.dim, n.label) : n.level === 1 ? paint(ANSI.bold, n.label) : n.label;
|
|
15167
|
-
const body = wrap(`${label}${where}${state}${n.proposal ? paint(ANSI.amber, " \xB7 NanoPM suggests a new wording") : ""}`, width - indent.length - 12);
|
|
15168
|
-
out.push(`${indent}${lead}${paint(ANSI.dim, n.ref.padEnd(8))} ${body[0] ?? ""}`);
|
|
15169
|
-
for (const more of body.slice(1)) out.push(`${indent} ${more}`);
|
|
15170
|
-
for (const c of n.children) line(c, depth + 1);
|
|
15171
|
-
};
|
|
15172
|
-
for (const need of tree) {
|
|
15173
|
-
out.push("");
|
|
15174
|
-
line(need, 0);
|
|
15175
|
-
}
|
|
15176
|
-
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>\`.`));
|
|
15177
|
-
if (opts.others?.length) out.push(paint(ANSI.dim, `Other personas: ${opts.others.join(", ")}.`));
|
|
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(", ")}.`));
|
|
15178
14825
|
return out.join("\n");
|
|
15179
14826
|
}
|
|
15180
14827
|
|
|
@@ -15335,51 +14982,471 @@ async function setAsideOpportunity(ref, why, opts) {
|
|
|
15335
14982
|
await rpc2("set_aside_opportunity", { p_ref: ref, p_reason: why ?? "not now" }, opts);
|
|
15336
14983
|
return `${ref.toUpperCase()} is set aside for this objective, with the narrower problems under it. It comes back if the objective changes.`;
|
|
15337
14984
|
}
|
|
15338
|
-
async function moveOpportunity(ref, place, opts) {
|
|
15339
|
-
const rank2 = Number(place);
|
|
15340
|
-
if (!Number.isInteger(rank2) || rank2 < 1) throw new Error("say where: nanopm opportunities move <ref> <place>, a place from 1");
|
|
15341
|
-
await rpc2("move_opportunity", { p_ref: ref, p_rank: rank2 }, opts);
|
|
15342
|
-
return `${ref.toUpperCase()} is at #${rank2} among its siblings, and the place is yours. Moved above a higher priority, it takes that priority.`;
|
|
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}`);
|
|
15020
|
+
}
|
|
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`.");
|
|
15029
|
+
return out.join("\n");
|
|
15030
|
+
}
|
|
15031
|
+
|
|
15032
|
+
// src/format/roadmap.ts
|
|
15033
|
+
var SHAPE = {
|
|
15034
|
+
proposed: { mark: "\u25CB", colour: ANSI.amber, label: "proposed" },
|
|
15035
|
+
accepted: { mark: "\u25D0", colour: ANSI.green, label: "accepted" },
|
|
15036
|
+
building: { mark: "\u25D4", colour: ANSI.blue, label: "building" },
|
|
15037
|
+
rejected: { mark: "\u2298", colour: ANSI.red, label: "rejected" },
|
|
15038
|
+
done: { mark: "\u25CF", colour: ANSI.green, label: "done" },
|
|
15039
|
+
dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
|
|
15040
|
+
};
|
|
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 }
|
|
15086
|
+
};
|
|
15087
|
+
function renderDecisions(rows2, opts) {
|
|
15088
|
+
const paint = painter(opts.color);
|
|
15089
|
+
if (!rows2.length) return "No decisions yet.";
|
|
15090
|
+
const out = [];
|
|
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}`));
|
|
15099
|
+
}
|
|
15100
|
+
}
|
|
15101
|
+
return out.join("\n");
|
|
15102
|
+
}
|
|
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] }]));
|
|
15106
|
+
}
|
|
15107
|
+
function sizeWord(size, paint) {
|
|
15108
|
+
return size === "big" ? `${paint(ANSI.bold, "\u25AA")} ${SIZE_WORD.big}` : size === "small" ? paint(ANSI.dim, SIZE_WORD.small) : "";
|
|
15109
|
+
}
|
|
15110
|
+
function renderSolutionsBoard(board, opts) {
|
|
15111
|
+
const paint = painter(opts.color);
|
|
15112
|
+
const width = Math.max(40, columns());
|
|
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])}`);
|
|
15141
|
+
}
|
|
15142
|
+
}
|
|
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");
|
|
15150
|
+
}
|
|
15151
|
+
function renderSolutionSheet(s, at, opts) {
|
|
15152
|
+
const paint = painter(opts.color);
|
|
15153
|
+
const width = Math.max(40, columns());
|
|
15154
|
+
const theme = at.branch ? themesOf(at.tree).get(at.branch.id) : void 0;
|
|
15155
|
+
const out = [];
|
|
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."));
|
|
15171
|
+
}
|
|
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));
|
|
15222
|
+
}
|
|
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));
|
|
15226
|
+
}
|
|
15227
|
+
function renderProblems(problems2, duplicates, opts) {
|
|
15228
|
+
const paint = painter(opts.color);
|
|
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));
|
|
15238
|
+
const out = [];
|
|
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))));
|
|
15250
|
+
}
|
|
15251
|
+
const unproven = problems2.filter((p) => p.proofs === 0).length;
|
|
15252
|
+
if (unproven) {
|
|
15253
|
+
out.push("");
|
|
15254
|
+
out.push(
|
|
15255
|
+
paint(
|
|
15256
|
+
ANSI.dim,
|
|
15257
|
+
`${unproven} of ${problems2.length} rest on judgement, not proof. A tilde marks those.`
|
|
15258
|
+
)
|
|
15259
|
+
);
|
|
15260
|
+
}
|
|
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>"));
|
|
15280
|
+
return out.join("\n");
|
|
15281
|
+
}
|
|
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");
|
|
15336
|
+
}
|
|
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();
|
|
15343
15400
|
}
|
|
15344
|
-
|
|
15345
|
-
const
|
|
15346
|
-
|
|
15347
|
-
|
|
15348
|
-
|
|
15349
|
-
|
|
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;
|
|
15350
15412
|
}
|
|
15351
|
-
|
|
15352
|
-
|
|
15353
|
-
const
|
|
15354
|
-
|
|
15355
|
-
|
|
15356
|
-
|
|
15357
|
-
]);
|
|
15358
|
-
|
|
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;
|
|
15359
15422
|
}
|
|
15360
|
-
|
|
15361
|
-
|
|
15362
|
-
|
|
15363
|
-
|
|
15364
|
-
|
|
15365
|
-
|
|
15366
|
-
|
|
15367
|
-
|
|
15368
|
-
const
|
|
15369
|
-
|
|
15370
|
-
|
|
15371
|
-
|
|
15372
|
-
|
|
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("");
|
|
15373
15448
|
}
|
|
15374
|
-
|
|
15375
|
-
const minutes = deep ? DEEP_MINUTES * n : FLASH_MINUTES;
|
|
15376
|
-
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`;
|
|
15377
|
-
const how = deep ? `${DEPTH_WORD.deep}${n > 1 ? ", one after the other" : ""}` : `${DEPTH_WORD.quick}, a first pass`;
|
|
15378
|
-
const out = [`${deep ? "Going deeper on" : "Brainstorming"} ${what}: ${how}, about ${minutes} minutes on ${daemon.hostname}.`];
|
|
15379
|
-
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))}`);
|
|
15380
|
-
if (plan.busy) out.push(`${plan.busy} left out: Nano is already finding solutions to ${plan.busy === 1 ? "it" : "them"}.`);
|
|
15381
|
-
out.push("The solutions land on `nanopm roadmap`.");
|
|
15382
|
-
return out.join("\n");
|
|
15449
|
+
return out.join("\n").trimEnd();
|
|
15383
15450
|
}
|
|
15384
15451
|
|
|
15385
15452
|
// src/commands/roadmap.ts
|
|
@@ -15539,8 +15606,8 @@ async function decide(kind, ref, body, opts) {
|
|
|
15539
15606
|
const row = Array.isArray(data) ? data[0] : void 0;
|
|
15540
15607
|
if (!row) throw new Error(`nothing changed for ${ref}`);
|
|
15541
15608
|
if (onMap) {
|
|
15542
|
-
const
|
|
15543
|
-
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.`;
|
|
15544
15611
|
return `Thrown out: ${row.ref}${body?.trim() ? ` \u2014 ${body.trim()}` : ""}.${after}`;
|
|
15545
15612
|
}
|
|
15546
15613
|
const verb = kind === "accept" ? "Accepted" : "Rejected";
|
|
@@ -15557,123 +15624,36 @@ function normalizeRef(input, items) {
|
|
|
15557
15624
|
return given;
|
|
15558
15625
|
}
|
|
15559
15626
|
|
|
15560
|
-
// src/commands/
|
|
15561
|
-
async function
|
|
15627
|
+
// src/commands/build.ts
|
|
15628
|
+
async function build(ref, opts) {
|
|
15562
15629
|
const { renderer, out } = rendererFor(opts.out);
|
|
15563
15630
|
const auth = await authenticate();
|
|
15564
15631
|
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15565
|
-
const autonomy2 = await readAutonomy(auth.client, project.id);
|
|
15566
|
-
if (autonomy2 === "propose") {
|
|
15567
|
-
throw new Error(
|
|
15568
|
-
`${project.name} is on "propose", so NanoPM may look but not touch.
|
|
15569
|
-
nanopm autonomy execute_after_validation to let it change files, asking you first`
|
|
15570
|
-
);
|
|
15571
|
-
}
|
|
15572
|
-
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");
|
|
15573
|
-
if (error) throw new Error(`could not read the solutions: ${error.message}`);
|
|
15574
|
-
const accepted = items ?? [];
|
|
15575
|
-
if (!accepted.length) {
|
|
15576
|
-
throw new Error("nothing is accepted yet. Run `nanopm roadmap`, then `nanopm accept <ref>`.");
|
|
15577
|
-
}
|
|
15578
|
-
let item = accepted[0];
|
|
15579
|
-
if (opts.ref) {
|
|
15580
|
-
const wanted = normalizeRef(opts.ref, accepted);
|
|
15581
|
-
const found = accepted.find((i) => i.ref === wanted);
|
|
15582
|
-
if (!found) {
|
|
15583
|
-
throw new Error(`${opts.ref} is not an accepted solution NanoPM can build. Accepted: ${accepted.map((i) => i.ref).join(", ")}.`);
|
|
15584
|
-
}
|
|
15585
|
-
item = found;
|
|
15586
|
-
}
|
|
15587
15632
|
const repo = await detectRepo(opts.cwd);
|
|
15588
|
-
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.");
|
|
15589
15640
|
await rememberClone(auth.client, project.id, repo.root);
|
|
15590
|
-
const
|
|
15591
|
-
|
|
15592
|
-
|
|
15593
|
-
|
|
15594
|
-
|
|
15595
|
-
|
|
15596
|
-
|
|
15597
|
-
})
|
|
15598
|
-
|
|
15599
|
-
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}`);
|
|
15600
15650
|
const answerer = opts.answersFile ? await answersFromFile(opts.answersFile) : answersFromTerminal(out);
|
|
15601
|
-
return streamJob(auth.client,
|
|
15602
|
-
}
|
|
15603
|
-
async function readAutonomy(client, projectId) {
|
|
15604
|
-
const { data } = await client.from("project_preferences").select("autonomy").eq("project_id", projectId).maybeSingle();
|
|
15605
|
-
return data?.autonomy ?? "propose";
|
|
15606
|
-
}
|
|
15607
|
-
var EXPLAINED = {
|
|
15608
|
-
propose: "NanoPM reads and proposes. It never changes a file.",
|
|
15609
|
-
execute_after_validation: "NanoPM changes files, on its own branch, after showing you what it will do and waiting for a yes.",
|
|
15610
|
-
execute_then_report: "NanoPM acts first and tells you after. Not available yet."
|
|
15611
|
-
};
|
|
15612
|
-
async function autonomy(level, opts) {
|
|
15613
|
-
const auth = await authenticate();
|
|
15614
|
-
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15615
|
-
const current = await readAutonomy(auth.client, project.id);
|
|
15616
|
-
if (!level) {
|
|
15617
|
-
const lines = [`${project.name} is on "${current}".`, ""];
|
|
15618
|
-
for (const l of AUTONOMY_LEVELS) {
|
|
15619
|
-
lines.push(` ${l === current ? "\u25CF" : "\u25CB"} ${l.padEnd(26)} ${EXPLAINED[l]}`);
|
|
15620
|
-
}
|
|
15621
|
-
lines.push("", " nanopm autonomy <level>");
|
|
15622
|
-
return lines.join("\n");
|
|
15623
|
-
}
|
|
15624
|
-
const wanted = level.trim();
|
|
15625
|
-
if (!AUTONOMY_LEVELS.includes(wanted)) {
|
|
15626
|
-
throw new Error(`"${level}" is not a level. Pick one of: ${AUTONOMY_LEVELS.join(", ")}.`);
|
|
15627
|
-
}
|
|
15628
|
-
if (wanted === "execute_then_report") {
|
|
15629
|
-
throw new Error(
|
|
15630
|
-
"execute_then_report is not available yet. NanoPM asks before it changes anything until the review loop has been used in anger."
|
|
15631
|
-
);
|
|
15632
|
-
}
|
|
15633
|
-
if (wanted === current) return `${project.name} is already on "${current}".`;
|
|
15634
|
-
const { error } = await auth.client.from("project_preferences").update({ autonomy: wanted }).eq("project_id", project.id);
|
|
15635
|
-
if (error) {
|
|
15636
|
-
throw new Error(`could not change the dial: ${error.message}`);
|
|
15637
|
-
}
|
|
15638
|
-
return [`${project.name} is now on "${wanted}".`, EXPLAINED[wanted]].join("\n");
|
|
15639
|
-
}
|
|
15640
|
-
async function verifyCommand(command, opts) {
|
|
15641
|
-
const auth = await authenticate();
|
|
15642
|
-
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15643
|
-
const { data } = await auth.client.from("project_preferences").select("verify_command").eq("project_id", project.id).maybeSingle();
|
|
15644
|
-
const current = data?.verify_command ?? null;
|
|
15645
|
-
if (!command && !opts.clear) {
|
|
15646
|
-
if (!current) {
|
|
15647
|
-
return [
|
|
15648
|
-
`${project.name} has no check, so nothing verifies what NanoPM writes.`,
|
|
15649
|
-
"",
|
|
15650
|
-
' nanopm verify "npm run lint && npm test"',
|
|
15651
|
-
"",
|
|
15652
|
-
"The daemon runs it in the repository root after a change and before the solution is",
|
|
15653
|
-
"marked done. A failure goes back to NanoPM once, to fix."
|
|
15654
|
-
].join("\n");
|
|
15655
|
-
}
|
|
15656
|
-
return [`${project.name} verifies with:`, "", ` ${current}`, "", ' nanopm verify "<command>" \xB7 nanopm verify --clear'].join("\n");
|
|
15657
|
-
}
|
|
15658
|
-
const next2 = opts.clear ? null : command.trim();
|
|
15659
|
-
if (next2 !== null && !next2) throw new Error('a check needs a command: nanopm verify "npm test"');
|
|
15660
|
-
const { error } = await auth.client.from("project_preferences").update({ verify_command: next2 }).eq("project_id", project.id);
|
|
15661
|
-
if (error) throw new Error(`could not save the check: ${error.message}`);
|
|
15662
|
-
if (next2 === null) return `${project.name} no longer runs a check. Nothing will verify what NanoPM writes.`;
|
|
15663
|
-
return [
|
|
15664
|
-
`${project.name} will verify with:`,
|
|
15665
|
-
"",
|
|
15666
|
-
` ${next2}`,
|
|
15667
|
-
"",
|
|
15668
|
-
"It runs in the repository root. If your change touches what the command itself runs \u2014",
|
|
15669
|
-
"package.json, a lockfile, the script it calls \u2014 the check is skipped and you are told,",
|
|
15670
|
-
"because an agent that can rewrite its own examiner is not being examined."
|
|
15671
|
-
].join("\n");
|
|
15651
|
+
return streamJob(auth.client, jobId, { pollMs: 700, out, renderer, cancelSignal: opts.cancelSignal, answerer, verbose: verboseWanted() });
|
|
15672
15652
|
}
|
|
15673
15653
|
|
|
15674
15654
|
// src/detect-sources.ts
|
|
15675
|
-
import { readFile as
|
|
15676
|
-
import { join as
|
|
15655
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
15656
|
+
import { join as join16 } from "path";
|
|
15677
15657
|
var ENV_FILES = [".env", ".env.local", ".env.development.local", ".env.development", ".env.example"];
|
|
15678
15658
|
var POSTGRES_KEYS = [
|
|
15679
15659
|
"DATABASE_URL",
|
|
@@ -15723,7 +15703,7 @@ async function envFiles(repoRoot) {
|
|
|
15723
15703
|
const out = [];
|
|
15724
15704
|
for (const file of ENV_FILES) {
|
|
15725
15705
|
try {
|
|
15726
|
-
out.push({ file, env: parseEnv(await
|
|
15706
|
+
out.push({ file, env: parseEnv(await readFile14(join16(repoRoot, file), "utf8")) });
|
|
15727
15707
|
} catch {
|
|
15728
15708
|
continue;
|
|
15729
15709
|
}
|
|
@@ -15783,7 +15763,7 @@ function describeUrl(url) {
|
|
|
15783
15763
|
// src/format/closing.ts
|
|
15784
15764
|
function closingLines(project, exit, env = process.env, mode = "onboard") {
|
|
15785
15765
|
if (exit !== 0) return [];
|
|
15786
|
-
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";
|
|
15787
15767
|
const chain = chainedAfter(mode).map((skill) => skillSentence(skill));
|
|
15788
15768
|
return [
|
|
15789
15769
|
"",
|
|
@@ -15791,7 +15771,7 @@ function closingLines(project, exit, env = process.env, mode = "onboard") {
|
|
|
15791
15771
|
...chain.length ? [`NanoPM is now ${joinAnd(chain)}; the Roadmap fills in as it goes. Watch it at ${webUrl(env)}/p/${project.slug}/journal`] : [],
|
|
15792
15772
|
// The map is no longer chained after onboarding, so the first three clauses are what
|
|
15793
15773
|
// builds it: what users said, where they leave traces, then the map that reads both.
|
|
15794
|
-
`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`,
|
|
15795
15775
|
"With a partner: nanopm invite <email> prints a link; they open it, or in their clone: nanopm login, then nanopm join <code>"
|
|
15796
15776
|
];
|
|
15797
15777
|
}
|
|
@@ -15937,7 +15917,9 @@ async function spec(ref, opts) {
|
|
|
15937
15917
|
const wanted = ref.trim().toUpperCase();
|
|
15938
15918
|
const { data: solution } = await auth.client.from("solutions").select("id, ref, title, agent_spec").eq("project_id", project.id).eq("ref", wanted).maybeSingle();
|
|
15939
15919
|
if (!solution) throw new Error(`no solution ${wanted}. Run \`nanopm roadmap\` to see them.`);
|
|
15940
|
-
if (
|
|
15920
|
+
if (opts.write && opts.rework) throw new Error("--write or --rework, not both.");
|
|
15921
|
+
const mode = opts.rework ? "rework" : opts.write ? "write" : null;
|
|
15922
|
+
if (!mode) {
|
|
15941
15923
|
if (!solution.agent_spec) return `${solution.ref} has no specs yet: \`nanopm spec ${solution.ref} --write\`, or *Write the specs* on its sheet.`;
|
|
15942
15924
|
return solution.agent_spec.trimEnd();
|
|
15943
15925
|
}
|
|
@@ -15945,11 +15927,9 @@ async function spec(ref, opts) {
|
|
|
15945
15927
|
if (!daemon) throw new Error("Your machine is asleep: none of your daemons has been seen in the last two minutes, so nothing was queued. `nanopm daemon install` starts it; then ask again.");
|
|
15946
15928
|
const repo = await detectRepo(opts.cwd);
|
|
15947
15929
|
if (repo) await rememberClone(auth.client, project.id, repo.root);
|
|
15948
|
-
await queueSpecs(auth.client, { projectId: project.id, userId: auth.userId, daemonId: daemon.id, ref: solution.ref });
|
|
15949
|
-
|
|
15950
|
-
|
|
15951
|
-
`They land on its sheet; \`nanopm spec ${solution.ref}\` prints the agent spec.`
|
|
15952
|
-
].join("\n");
|
|
15930
|
+
await queueSpecs(auth.client, { projectId: project.id, userId: auth.userId, daemonId: daemon.id, ref: solution.ref, mode });
|
|
15931
|
+
const where = `about ${SPEC_MINUTES} minutes on ${daemon.hostname}${repo ? ", reading this code" : ""}`;
|
|
15932
|
+
return mode === "rework" ? [`Reworking ${solution.ref} \u201C${clip(solution.title, 60)}\u201D: the whole solution and its agent spec, ${where}.`, "The new version takes its card on the Roadmap; `nanopm roadmap` names it."].join("\n") : [`Writing the specs of ${solution.ref} \u201C${clip(solution.title, 60)}\u201D: what it lacks and the agent spec, ${where}.`, `They land on its sheet; \`nanopm spec ${solution.ref}\` prints the agent spec.`].join("\n");
|
|
15953
15933
|
}
|
|
15954
15934
|
|
|
15955
15935
|
// src/commands/want.ts
|
|
@@ -16492,7 +16472,7 @@ async function seedWatches(watches2, read6) {
|
|
|
16492
16472
|
}
|
|
16493
16473
|
|
|
16494
16474
|
// src/commands/watch.ts
|
|
16495
|
-
import { readFile as
|
|
16475
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
16496
16476
|
function cursorFor(source, watchId) {
|
|
16497
16477
|
const all = source.cursor ?? {};
|
|
16498
16478
|
return all[watchId] ?? {};
|
|
@@ -16553,7 +16533,7 @@ async function addWatch(labelOrId, id, opts) {
|
|
|
16553
16533
|
const paint = painter(color);
|
|
16554
16534
|
if (!labelOrId || !id) throw new Error('say which feed and what to call it: nanopm watch add <source> <id> --query "\u2026"');
|
|
16555
16535
|
if (opts.query && opts.queryFile) throw new Error("--query or --query-file, not both");
|
|
16556
|
-
const query = opts.queryFile ? await
|
|
16536
|
+
const query = opts.queryFile ? await readFile15(opts.queryFile, "utf8") : opts.query ?? "";
|
|
16557
16537
|
const watch = watchInput(id, query, opts);
|
|
16558
16538
|
const auth = await authenticate();
|
|
16559
16539
|
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
@@ -16740,9 +16720,9 @@ async function review(opts) {
|
|
|
16740
16720
|
}
|
|
16741
16721
|
const usable = connections.filter((c) => c.status === "ok");
|
|
16742
16722
|
if (!usable.length) {
|
|
16743
|
-
const
|
|
16723
|
+
const failed2 = connections.map((c) => c.label).join(", ");
|
|
16744
16724
|
throw new Error(
|
|
16745
|
-
`no connection is working (${
|
|
16725
|
+
`no connection is working (${failed2}), so a review would have nothing to read.
|
|
16746
16726
|
nanopm connect --list see what is wrong`
|
|
16747
16727
|
);
|
|
16748
16728
|
}
|
|
@@ -17377,12 +17357,12 @@ function renderDirection(rows2) {
|
|
|
17377
17357
|
const out = [];
|
|
17378
17358
|
for (const line of DIRECTION_ROWS) {
|
|
17379
17359
|
const row = rows2.find((r) => r.key === directionKey(line));
|
|
17380
|
-
const
|
|
17360
|
+
const next = rows2.find((r) => r.key === directionKey(line, true));
|
|
17381
17361
|
out.push(`${DIRECTION_ROW_TITLE[line]}${mark(row)}`);
|
|
17382
17362
|
out.push(` ${row?.content ?? "\u2014"}`);
|
|
17383
|
-
if (
|
|
17384
|
-
const because = reasonOf2(
|
|
17385
|
-
out.push(` proposed: ${
|
|
17363
|
+
if (next) {
|
|
17364
|
+
const because = reasonOf2(next.sources);
|
|
17365
|
+
out.push(` proposed: ${next.content}${because ? ` \xB7 ${because}` : ""}`);
|
|
17386
17366
|
}
|
|
17387
17367
|
}
|
|
17388
17368
|
out.push("Objective");
|
|
@@ -17422,13 +17402,13 @@ function render2(rows2) {
|
|
|
17422
17402
|
const out = [];
|
|
17423
17403
|
for (const line of PITCH_LINE_KEYS) {
|
|
17424
17404
|
const row = rows2.find((r) => r.key === pitchKey(line));
|
|
17425
|
-
const
|
|
17405
|
+
const next = rows2.find((r) => r.key === pitchKey(line, true));
|
|
17426
17406
|
const mark2 = row && row.provenance !== "stated" ? ` \xB7 to review${row.confidence != null ? ` (${row.confidence}% sure)` : ""}` : "";
|
|
17427
17407
|
out.push(`${PITCH_LINE_TITLE[line]}${mark2}`);
|
|
17428
17408
|
out.push(` ${row?.content ?? "\u2014"}`);
|
|
17429
|
-
if (
|
|
17430
|
-
const because = reasonOf3(
|
|
17431
|
-
out.push(` proposed: ${
|
|
17409
|
+
if (next) {
|
|
17410
|
+
const because = reasonOf3(next.sources);
|
|
17411
|
+
out.push(` proposed: ${next.content}${because ? ` \xB7 ${because}` : ""}`);
|
|
17432
17412
|
}
|
|
17433
17413
|
}
|
|
17434
17414
|
return out.join("\n");
|
|
@@ -17486,7 +17466,7 @@ async function invite(email, opts) {
|
|
|
17486
17466
|
const [, ...rest] = inviteLines({ name: project.name, code, email: email.trim().toLowerCase(), web: webUrl() });
|
|
17487
17467
|
return [`Invite to ${paint(ANSI.bold, project.name)}: ${paint(ANSI.bold, code)}`, ...rest.map((l) => l ? paint(ANSI.dim, l) : l)].join("\n");
|
|
17488
17468
|
}
|
|
17489
|
-
async function
|
|
17469
|
+
async function join17(code, opts) {
|
|
17490
17470
|
const normalized2 = code.trim().toUpperCase();
|
|
17491
17471
|
if (!INVITE_CODE.test(normalized2)) throw new Error(`that does not look like an invite code: ten letters and digits, like ${"K7PQ2XM9RT"}`);
|
|
17492
17472
|
const repo = await detectRepo(opts.cwd);
|
|
@@ -17941,8 +17921,8 @@ function buildProgram() {
|
|
|
17941
17921
|
}
|
|
17942
17922
|
console.log(await roadmap(ref, { cwd: process.cwd(), projectId: opts.project, board: opts.board, color: colorOf(opts) }));
|
|
17943
17923
|
});
|
|
17944
|
-
program.command("spec <ref>").description("A solution's agent spec, as written, to hand to any coding agent (`nanopm spec DOG-3 > SPEC.md`); `--write` asks Nano for
|
|
17945
|
-
console.log(await spec(ref, { cwd: process.cwd(), projectId: opts.project, write: opts.write }));
|
|
17924
|
+
program.command("spec <ref>").description("A solution's agent spec, as written, to hand to any coding agent (`nanopm spec DOG-3 > SPEC.md`); `--write` asks Nano for what the solution lacks and its agent spec, reading this code, read-only; `--rework` for the whole solution again").option("--project <id>", "project id (default: the project of the current repo)").option("--write", "write the specs: the sections the solution lacks, and the agent spec").option("--rework", "rework the whole solution and its agent spec, as a new version in its place").action(async (ref, opts) => {
|
|
17925
|
+
console.log(await spec(ref, { cwd: process.cwd(), projectId: opts.project, write: opts.write, rework: opts.rework }));
|
|
17946
17926
|
});
|
|
17947
17927
|
for (const [kind, help] of [
|
|
17948
17928
|
["accept", "Accept a solution; NanoPM builds it when you ask, or you do it and say so with 'nanopm done'"],
|
|
@@ -18187,7 +18167,7 @@ function buildProgram() {
|
|
|
18187
18167
|
console.log(await invite(email, { cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
|
|
18188
18168
|
});
|
|
18189
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) => {
|
|
18190
|
-
console.log(await
|
|
18170
|
+
console.log(await join17(code, { cwd: process.cwd(), color: colorOf(opts) }));
|
|
18191
18171
|
});
|
|
18192
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) => {
|
|
18193
18173
|
console.log(await members({ cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
|
|
@@ -18209,16 +18189,10 @@ function buildProgram() {
|
|
|
18209
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) => {
|
|
18210
18190
|
console.log(await status({ cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
|
|
18211
18191
|
});
|
|
18212
|
-
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) => {
|
|
18213
18193
|
const cancel = new AbortController();
|
|
18214
18194
|
process.once("SIGINT", () => cancel.abort());
|
|
18215
|
-
process.exitCode = await
|
|
18216
|
-
cwd: process.cwd(),
|
|
18217
|
-
projectId: opts.project,
|
|
18218
|
-
ref: ref ?? opts.ref,
|
|
18219
|
-
answersFile: opts.answers,
|
|
18220
|
-
cancelSignal: cancel.signal
|
|
18221
|
-
});
|
|
18195
|
+
process.exitCode = await build(ref, { cwd: process.cwd(), projectId: opts.project, anyway: opts.anyway, answersFile: opts.answers, cancelSignal: cancel.signal });
|
|
18222
18196
|
});
|
|
18223
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) => {
|
|
18224
18198
|
console.log(await autonomy(level, { cwd: process.cwd(), projectId: opts.project }));
|