@nanopm/cli 0.1.10 → 0.1.12
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 +1627 -1464
- package/package.json +1 -1
- package/skills/build/SKILL.md +62 -0
- package/skills/build/skill.json +15 -0
- package/skills/next/SKILL.md +0 -75
- package/skills/next/skill.json +0 -8
package/dist/index.js
CHANGED
|
@@ -156,7 +156,8 @@ var NEEDS_GO_AHEAD = /* @__PURE__ */ new Set(["execute_after_validation"]);
|
|
|
156
156
|
function requiresGoAhead(autonomy2) {
|
|
157
157
|
return NEEDS_GO_AHEAD.has(autonomy2);
|
|
158
158
|
}
|
|
159
|
-
function capCapabilities(requested, autonomy2) {
|
|
159
|
+
function capCapabilities(requested, autonomy2, opts = {}) {
|
|
160
|
+
if (opts.asked) return [...requested];
|
|
160
161
|
const denied = CAPPED_BELOW[autonomy2];
|
|
161
162
|
const granted = requested.filter((c) => !denied.has(c));
|
|
162
163
|
if (granted.includes("write_repo") && requiresGoAhead(autonomy2) && !granted.includes("ask_user")) {
|
|
@@ -387,6 +388,12 @@ var SkillManifestSchema = z2.object({
|
|
|
387
388
|
},
|
|
388
389
|
{ message: "timeout_deep must look like 3h, 5m, 90s" }
|
|
389
390
|
).optional(),
|
|
391
|
+
/**
|
|
392
|
+
* Run only when the founder asks for it on one item, by a click or a command, never by Nano on
|
|
393
|
+
* its own: the autonomy dial, which governs what Nano does unasked, does not cap it
|
|
394
|
+
* (`capCapabilities`). A build is the one today (docs/⏳nanopm-76-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) {
|
|
@@ -674,6 +673,18 @@ function formatStreamEvent(event) {
|
|
|
674
673
|
return "\u2298 cancel requested";
|
|
675
674
|
case "error":
|
|
676
675
|
return `\u2717 ${String(p.code ?? "error")}: ${String(p.message ?? "")}`;
|
|
676
|
+
// A build (docs/⏳nanopm-76-build-it.md): its plan, each step with its commit, a pause, the pull request.
|
|
677
|
+
case "plan":
|
|
678
|
+
return ["The plan:", ...(Array.isArray(p.steps) ? p.steps : []).map((s, i) => ` ${i + 1}. ${String(s)}`)].join("\n");
|
|
679
|
+
case "step":
|
|
680
|
+
return `\u2713 ${String(p.step ?? "")}. ${String(p.says ?? "")}${p.commit ? ` \xB7 ${String(p.commit)}` : ""}`;
|
|
681
|
+
case "paused": {
|
|
682
|
+
const at = new Date(String(p.resets_at ?? ""));
|
|
683
|
+
const when = Number.isNaN(at.getTime()) ? "when it resets" : `at ${at.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`;
|
|
684
|
+
return `\u25CC paused: your Claude limit is reached. Nano carries on by itself ${when}.`;
|
|
685
|
+
}
|
|
686
|
+
case "pr":
|
|
687
|
+
return `\u2387 draft pull request #${String(p.number ?? "")} \xB7 ${String(p.url ?? "")}`;
|
|
677
688
|
default:
|
|
678
689
|
return null;
|
|
679
690
|
}
|
|
@@ -944,7 +955,7 @@ var AFTER_DEEP = {
|
|
|
944
955
|
function daemonQueues(skill, depth = 0, parent = null, deep = false) {
|
|
945
956
|
if (deep) return depth >= CHAIN_DEPTH_MAX ? [] : AFTER_DEEP[skill] ?? [];
|
|
946
957
|
const theirs = new Set(SQL_OWNED[skill] ?? []);
|
|
947
|
-
return chainedAfter(skill, depth, parent).filter((
|
|
958
|
+
return chainedAfter(skill, depth, parent).filter((next) => !theirs.has(next));
|
|
948
959
|
}
|
|
949
960
|
var NOT_CHAINED_BY_DAEMON = ["ingest"];
|
|
950
961
|
var CHAIN_DEPTH_MAX = 3;
|
|
@@ -954,7 +965,7 @@ function chainedAfter(skill, depth = 0, parent = null) {
|
|
|
954
965
|
const gates = ONLY_AFTER[skill] ?? {};
|
|
955
966
|
const bars2 = NEVER_AFTER[skill] ?? {};
|
|
956
967
|
return (CHAINED[skill] ?? []).filter(
|
|
957
|
-
(
|
|
968
|
+
(next) => (!gates[next] || parent !== null && gates[next].includes(parent)) && !(parent !== null && bars2[next]?.includes(parent))
|
|
958
969
|
);
|
|
959
970
|
}
|
|
960
971
|
var CHAINED_SKILLS = [...new Set(Object.values(CHAINED).flatMap((s) => s ?? []))];
|
|
@@ -974,7 +985,7 @@ var SKILL_SENTENCE = {
|
|
|
974
985
|
direction: "working out where this is going",
|
|
975
986
|
solutions: "proposing options",
|
|
976
987
|
review: "reviewing",
|
|
977
|
-
|
|
988
|
+
build: "building",
|
|
978
989
|
ask: "answering a question",
|
|
979
990
|
want: "shaping what you want",
|
|
980
991
|
ingest: "watching the numbers",
|
|
@@ -982,22 +993,22 @@ var SKILL_SENTENCE = {
|
|
|
982
993
|
spec: "writing specs"
|
|
983
994
|
};
|
|
984
995
|
function skillSentence(skill, opts = {}) {
|
|
985
|
-
if (skill === "
|
|
996
|
+
if (skill === "build") return opts.ref ? `building ${opts.ref}` : "building an option";
|
|
986
997
|
return SKILL_SENTENCE[skill] ?? `running ${skill}`;
|
|
987
998
|
}
|
|
988
999
|
|
|
989
1000
|
// ../shared/src/time.ts
|
|
990
1001
|
var DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
991
1002
|
function describeNextRun(schedule, now = /* @__PURE__ */ new Date(), opts = {}) {
|
|
992
|
-
const
|
|
1003
|
+
const next = new Date(schedule.next_due_at);
|
|
993
1004
|
const local = new Intl.DateTimeFormat("en-GB", {
|
|
994
1005
|
timeZone: schedule.timezone,
|
|
995
1006
|
weekday: "long",
|
|
996
1007
|
hour: "2-digit",
|
|
997
1008
|
minute: "2-digit",
|
|
998
1009
|
hour12: false
|
|
999
|
-
}).format(
|
|
1000
|
-
const hours = Math.round((
|
|
1010
|
+
}).format(next);
|
|
1011
|
+
const hours = Math.round((next.getTime() - now.getTime()) / 36e5);
|
|
1001
1012
|
const when = hours < 1 ? "any minute" : hours < 36 ? `in ${hours}h` : `in ${Math.round(hours / 24)} days`;
|
|
1002
1013
|
return opts.timezone === false ? `${local}, ${when}` : `${local} ${schedule.timezone}, ${when}`;
|
|
1003
1014
|
}
|
|
@@ -2018,10 +2029,10 @@ function placeRanks(crew, pinned) {
|
|
|
2018
2029
|
while (slots[at] !== null && at > 0) at -= 1;
|
|
2019
2030
|
slots[at] = id;
|
|
2020
2031
|
}
|
|
2021
|
-
let
|
|
2032
|
+
let next = 0;
|
|
2022
2033
|
for (let i = 0; i < total; i += 1) {
|
|
2023
2034
|
if (slots[i] !== null) continue;
|
|
2024
|
-
slots[i] = loose[
|
|
2035
|
+
slots[i] = loose[next++] ?? null;
|
|
2025
2036
|
}
|
|
2026
2037
|
const out = /* @__PURE__ */ new Map();
|
|
2027
2038
|
slots.forEach((id, i) => id && out.set(id, i + 1));
|
|
@@ -2672,7 +2683,7 @@ import { z as z9 } from "zod";
|
|
|
2672
2683
|
var PITCH_LINE_KEYS = ["what", "who", "need"];
|
|
2673
2684
|
var PITCH_TOPIC = "identity";
|
|
2674
2685
|
var PITCH_KEY = /^pitch-/;
|
|
2675
|
-
var pitchKey = (line,
|
|
2686
|
+
var pitchKey = (line, next = false) => `pitch-${line}${next ? "-next" : ""}`;
|
|
2676
2687
|
var PITCH_LINE_TITLE = { what: "What it is", who: "Who it is for", need: "What they come for" };
|
|
2677
2688
|
|
|
2678
2689
|
// ../shared/src/pitch-crew.ts
|
|
@@ -2724,7 +2735,7 @@ var DIRECTION_ROWS = ["mission", "vision"];
|
|
|
2724
2735
|
var MISSION_TOPIC = "mission";
|
|
2725
2736
|
var VISION_TOPIC = "vision";
|
|
2726
2737
|
var DIRECTION_KEY = /^(?:mission|vision)(?:-next)?$/;
|
|
2727
|
-
var directionKey = (row,
|
|
2738
|
+
var directionKey = (row, next = false) => `${row}${next ? "-next" : ""}`;
|
|
2728
2739
|
var DIRECTION_ROW_TITLE = { mission: "Mission", vision: "Vision" };
|
|
2729
2740
|
|
|
2730
2741
|
// ../shared/src/direction-crew.ts
|
|
@@ -3483,7 +3494,7 @@ function foldRound(r) {
|
|
|
3483
3494
|
}
|
|
3484
3495
|
}
|
|
3485
3496
|
const answer = TalkAnswer.safeParse(r.result);
|
|
3486
|
-
const
|
|
3497
|
+
const failed2 = r.status === "failed" || r.status === "cancelled";
|
|
3487
3498
|
const error = r.error ?? {};
|
|
3488
3499
|
return {
|
|
3489
3500
|
id: r.id,
|
|
@@ -3494,8 +3505,8 @@ function foldRound(r) {
|
|
|
3494
3505
|
depth: params.data.depth,
|
|
3495
3506
|
actions: [...actions.values()],
|
|
3496
3507
|
answer: answer.success ? answer.data : null,
|
|
3497
|
-
state: r.status === "succeeded" ? "done" :
|
|
3498
|
-
...
|
|
3508
|
+
state: r.status === "succeeded" ? "done" : failed2 ? "failed" : "working",
|
|
3509
|
+
...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
3510
|
...doing ? { doing } : {}
|
|
3500
3511
|
};
|
|
3501
3512
|
}
|
|
@@ -3522,7 +3533,7 @@ var PROBLEM_TOOLS = ["propose_problems", "add_evidence", "flag_duplicate", "file
|
|
|
3522
3533
|
var READ_WEB_TOOLS = ["web_search", "web_fetch"];
|
|
3523
3534
|
var STORE_TOOLS = ["app_store_search", "play_store_search"];
|
|
3524
3535
|
var MARKET_TOOLS = ["write_market"];
|
|
3525
|
-
var
|
|
3536
|
+
var BUILD_TOOLS = ["build_plan", "build_step"];
|
|
3526
3537
|
var ALL_MEMORY_TOOLS = [
|
|
3527
3538
|
...MEMORY_READ_TOOLS,
|
|
3528
3539
|
...MEMORY_WRITE_TOOLS,
|
|
@@ -3532,7 +3543,7 @@ var ALL_MEMORY_TOOLS = [
|
|
|
3532
3543
|
...PROBLEM_TOOLS,
|
|
3533
3544
|
...READ_WEB_TOOLS,
|
|
3534
3545
|
...MARKET_TOOLS,
|
|
3535
|
-
...
|
|
3546
|
+
...BUILD_TOOLS,
|
|
3536
3547
|
...TALK_TOOLS,
|
|
3537
3548
|
"last_review",
|
|
3538
3549
|
"list_problems",
|
|
@@ -3574,7 +3585,7 @@ function claudeCodeToolFlags(capabilities, opts = {}) {
|
|
|
3574
3585
|
tools.add(t);
|
|
3575
3586
|
allowed.add(`${t}(${pathRule(workspace)})`);
|
|
3576
3587
|
}
|
|
3577
|
-
for (const t of
|
|
3588
|
+
for (const t of BUILD_TOOLS) allowed.add(mcpTool(t));
|
|
3578
3589
|
} else {
|
|
3579
3590
|
for (const t of WRITE_TOOLS) disallowed.add(t);
|
|
3580
3591
|
}
|
|
@@ -3632,7 +3643,7 @@ function claudeCodeToolFlags(capabilities, opts = {}) {
|
|
|
3632
3643
|
allowed.delete(t);
|
|
3633
3644
|
disallowed.add(t);
|
|
3634
3645
|
}
|
|
3635
|
-
return { tools: [...tools], allowedTools: [...allowed], disallowedTools: [...disallowed] };
|
|
3646
|
+
return { tools: [...tools], allowedTools: [...allowed], disallowedTools: [...disallowed], permissionMode: tools.has("Bash") ? "auto" : "default" };
|
|
3636
3647
|
}
|
|
3637
3648
|
|
|
3638
3649
|
// ../daemon/src/adapters/claude-code/sandbox.ts
|
|
@@ -3674,12 +3685,26 @@ function buildSandboxSettings(home) {
|
|
|
3674
3685
|
}
|
|
3675
3686
|
return { permissions: { deny, defaultMode: "default" } };
|
|
3676
3687
|
}
|
|
3677
|
-
function sandboxSettingsFor(home, grantedTools) {
|
|
3688
|
+
function sandboxSettingsFor(home, grantedTools, network = [], answerFile) {
|
|
3678
3689
|
const settings = buildSandboxSettings(home);
|
|
3679
3690
|
const granted = new Set(grantedTools);
|
|
3680
3691
|
settings.permissions.deny = settings.permissions.deny.filter((rule) => !granted.has(rule));
|
|
3692
|
+
if (granted.has("Bash")) {
|
|
3693
|
+
settings.sandbox = {
|
|
3694
|
+
enabled: true,
|
|
3695
|
+
allowUnsandboxedCommands: false,
|
|
3696
|
+
failIfUnavailable: true,
|
|
3697
|
+
network: { allowedDomains: network.filter((h) => !/(^|\.)github(usercontent)?\.com$/.test(h)) }
|
|
3698
|
+
};
|
|
3699
|
+
}
|
|
3700
|
+
if (answerFile) settings.hooks = { PreToolUse: [{ matcher: "mcp__nanopm__ask_user", hooks: [{ type: "command", command: askHook(answerFile) }] }] };
|
|
3681
3701
|
return settings;
|
|
3682
3702
|
}
|
|
3703
|
+
function askHook(answerFile) {
|
|
3704
|
+
const quote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
3705
|
+
const decision = (d) => quote(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: d } }));
|
|
3706
|
+
return `if [ -f ${quote(answerFile)} ]; then mv ${quote(answerFile)} ${quote(`${answerFile}.taken`)}; printf '%s' ${decision("allow")}; else printf '%s' ${decision("defer")}; fi`;
|
|
3707
|
+
}
|
|
3683
3708
|
|
|
3684
3709
|
// ../daemon/src/adapters/claude-code/stream.ts
|
|
3685
3710
|
var RESULT_FAILURES = {
|
|
@@ -3697,6 +3722,8 @@ var StreamParser = class {
|
|
|
3697
3722
|
model = "unknown";
|
|
3698
3723
|
toolCalls = /* @__PURE__ */ new Map();
|
|
3699
3724
|
sawResult = false;
|
|
3725
|
+
/** When the Claude window opens again, from the last rejected `rate_limit_event`. */
|
|
3726
|
+
resetsAt;
|
|
3700
3727
|
// The read size, counted here because the stream is the one place every call passes.
|
|
3701
3728
|
contextPeak = 0;
|
|
3702
3729
|
readCalls = {};
|
|
@@ -3707,6 +3734,9 @@ var StreamParser = class {
|
|
|
3707
3734
|
get currentSessionId() {
|
|
3708
3735
|
return this.sessionId;
|
|
3709
3736
|
}
|
|
3737
|
+
get limitResetsAt() {
|
|
3738
|
+
return this.resetsAt;
|
|
3739
|
+
}
|
|
3710
3740
|
/** Feed one NDJSON line; get zero or more events. Non-JSON lines are ignored. */
|
|
3711
3741
|
parseLine(line) {
|
|
3712
3742
|
const trimmed = line.trim();
|
|
@@ -3758,10 +3788,18 @@ var StreamParser = class {
|
|
|
3758
3788
|
}
|
|
3759
3789
|
return events;
|
|
3760
3790
|
}
|
|
3791
|
+
case "rate_limit_event": {
|
|
3792
|
+
const info = obj.rate_limit_info;
|
|
3793
|
+
if (info?.status === "rejected" && typeof info.resetsAt === "number") this.resetsAt = info.resetsAt;
|
|
3794
|
+
return [];
|
|
3795
|
+
}
|
|
3761
3796
|
case "result": {
|
|
3762
3797
|
this.sawResult = true;
|
|
3763
3798
|
this.sessionId = obj.session_id ?? this.sessionId;
|
|
3764
3799
|
const usage = this.usageFrom(obj);
|
|
3800
|
+
if (obj.stop_reason === "tool_deferred" && obj.deferred_tool_use?.name) {
|
|
3801
|
+
return [{ type: "deferred", sessionId: this.sessionId ?? "", tool: obj.deferred_tool_use.name, input: obj.deferred_tool_use.input, usage }];
|
|
3802
|
+
}
|
|
3765
3803
|
if (obj.subtype === "success" && !obj.is_error) {
|
|
3766
3804
|
if (this.structuredOutput && obj.structured_output === void 0) {
|
|
3767
3805
|
return [{ type: "failed", sessionId: this.sessionId, code: "harness_error", message: "Claude omitted the required structured output." }];
|
|
@@ -3770,7 +3808,7 @@ var StreamParser = class {
|
|
|
3770
3808
|
}
|
|
3771
3809
|
const message = obj.result || `claude ended with ${obj.subtype ?? "an unknown result"}`;
|
|
3772
3810
|
const code = isRateLimit(message) ? "rate_limit" : RESULT_FAILURES[obj.subtype ?? ""] ?? "harness_error";
|
|
3773
|
-
return [{ type: "failed", sessionId: this.sessionId, code, message }];
|
|
3811
|
+
return [{ type: "failed", sessionId: this.sessionId, code, message, ...code === "rate_limit" && this.resetsAt ? { resetsAt: this.resetsAt } : {} }];
|
|
3774
3812
|
}
|
|
3775
3813
|
default:
|
|
3776
3814
|
return [];
|
|
@@ -3818,8 +3856,8 @@ function classifyExit(stderr, aborted) {
|
|
|
3818
3856
|
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
3857
|
return { code: "auth", message: "Claude Code is not logged in on this machine. Run `claude login` or set ANTHROPIC_API_KEY." };
|
|
3820
3858
|
}
|
|
3821
|
-
const
|
|
3822
|
-
return { code: "harness_error", message:
|
|
3859
|
+
const tail = stderr.trim().split("\n").slice(-3).join(" ").trim();
|
|
3860
|
+
return { code: "harness_error", message: tail || "claude exited without a result" };
|
|
3823
3861
|
}
|
|
3824
3862
|
|
|
3825
3863
|
// ../daemon/src/adapters/claude-code/index.ts
|
|
@@ -3856,10 +3894,10 @@ var ClaudeCodeAdapter = class {
|
|
|
3856
3894
|
const result = parsePrintResult(stdout);
|
|
3857
3895
|
return result?.is_error ? { ok: false, message: result.result ?? "claude reported an error" } : { ok: true, message: `claude answered in ${result?.subtype ?? "success"}` };
|
|
3858
3896
|
} catch (err) {
|
|
3859
|
-
const
|
|
3860
|
-
const reported = parsePrintResult(
|
|
3897
|
+
const failed2 = err;
|
|
3898
|
+
const reported = parsePrintResult(failed2.stdout)?.result;
|
|
3861
3899
|
if (reported) return { ok: false, message: reported };
|
|
3862
|
-
const text2 =
|
|
3900
|
+
const text2 = failed2.stderr?.trim() || failed2.message || String(err);
|
|
3863
3901
|
return { ok: false, message: classifyExit(text2, false).message };
|
|
3864
3902
|
}
|
|
3865
3903
|
}
|
|
@@ -3895,9 +3933,9 @@ var ClaudeCodeAdapter = class {
|
|
|
3895
3933
|
child.stderr.on("data", (chunk) => {
|
|
3896
3934
|
stderr += chunk.toString();
|
|
3897
3935
|
});
|
|
3898
|
-
const exit = new Promise((
|
|
3899
|
-
child.on("close", (code) =>
|
|
3900
|
-
child.on("error", () =>
|
|
3936
|
+
const exit = new Promise((resolve5) => {
|
|
3937
|
+
child.on("close", (code) => resolve5(code));
|
|
3938
|
+
child.on("error", () => resolve5(127));
|
|
3901
3939
|
});
|
|
3902
3940
|
const parser = new StreamParser();
|
|
3903
3941
|
let searched = false;
|
|
@@ -3937,7 +3975,7 @@ var ClaudeCodeAdapter = class {
|
|
|
3937
3975
|
"--append-system-prompt",
|
|
3938
3976
|
spec2.systemPrompt,
|
|
3939
3977
|
"--permission-mode",
|
|
3940
|
-
|
|
3978
|
+
flags.permissionMode,
|
|
3941
3979
|
"--max-turns",
|
|
3942
3980
|
String(spec2.maxTurns),
|
|
3943
3981
|
"--setting-sources",
|
|
@@ -3986,7 +4024,7 @@ var ClaudeCodeAdapter = class {
|
|
|
3986
4024
|
await writeFile2(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 384 });
|
|
3987
4025
|
const settingsPath = join3(spec2.runDir, "settings.json");
|
|
3988
4026
|
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 });
|
|
4027
|
+
await writeFile2(settingsPath, JSON.stringify(sandboxSettingsFor(spec2.env.HOME ?? "", flags.tools, spec2.network, spec2.answerFile)), { mode: 384 });
|
|
3990
4028
|
const child = spawn(this.claudePath, this.buildArgs(spec2, mcpConfigPath, settingsPath), {
|
|
3991
4029
|
cwd: spec2.cwd,
|
|
3992
4030
|
env: spec2.env,
|
|
@@ -4005,9 +4043,9 @@ var ClaudeCodeAdapter = class {
|
|
|
4005
4043
|
stderr += chunk.toString();
|
|
4006
4044
|
if (stderr.length > 64e3) stderr = stderr.slice(-32e3);
|
|
4007
4045
|
});
|
|
4008
|
-
const exit = new Promise((
|
|
4009
|
-
child.on("close", (code, signal) =>
|
|
4010
|
-
child.on("error", () =>
|
|
4046
|
+
const exit = new Promise((resolve5) => {
|
|
4047
|
+
child.on("close", (code, signal) => resolve5({ code, signal }));
|
|
4048
|
+
child.on("error", () => resolve5({ code: 127, signal: null }));
|
|
4011
4049
|
});
|
|
4012
4050
|
const parser = new StreamParser(spec2.outputSchema !== void 0);
|
|
4013
4051
|
try {
|
|
@@ -4017,7 +4055,8 @@ var ClaudeCodeAdapter = class {
|
|
|
4017
4055
|
const { code } = await exit;
|
|
4018
4056
|
if (!parser.finished) {
|
|
4019
4057
|
const failure2 = classifyExit(stderr, aborted);
|
|
4020
|
-
|
|
4058
|
+
const resetsAt = failure2.code === "rate_limit" ? parser.limitResetsAt : void 0;
|
|
4059
|
+
yield { type: "failed", sessionId: parser.currentSessionId, ...failure2, message: appendExit(failure2.message, code), ...resetsAt ? { resetsAt } : {} };
|
|
4021
4060
|
}
|
|
4022
4061
|
} finally {
|
|
4023
4062
|
spec2.signal.removeEventListener("abort", onAbort);
|
|
@@ -4042,10 +4081,10 @@ function parsePrintResult(stdout) {
|
|
|
4042
4081
|
}
|
|
4043
4082
|
|
|
4044
4083
|
// ../daemon/src/daemon.ts
|
|
4045
|
-
import { mkdir as
|
|
4084
|
+
import { mkdir as mkdir6, readdir as readdir3, readFile as readFile9, rm as rm5, writeFile as writeFile6 } from "fs/promises";
|
|
4046
4085
|
import { openSync, closeSync, unlinkSync, writeFileSync, readFileSync as readFileSyncNode, mkdirSync } from "fs";
|
|
4047
4086
|
import { hostname as hostname2 } from "os";
|
|
4048
|
-
import { join as
|
|
4087
|
+
import { join as join14 } from "path";
|
|
4049
4088
|
import { setTimeout as sleep } from "timers/promises";
|
|
4050
4089
|
|
|
4051
4090
|
// ../daemon/src/awake.ts
|
|
@@ -4114,8 +4153,8 @@ async function installVersion(version, home = homedir2()) {
|
|
|
4114
4153
|
});
|
|
4115
4154
|
return { ok: true };
|
|
4116
4155
|
} catch (err) {
|
|
4117
|
-
const
|
|
4118
|
-
return { ok: false, why: (
|
|
4156
|
+
const failed2 = err;
|
|
4157
|
+
return { ok: false, why: (failed2.stderr?.trim() || failed2.message || String(err)).split("\n").slice(-3).join(" ") };
|
|
4119
4158
|
}
|
|
4120
4159
|
}
|
|
4121
4160
|
function autoUpdateOff(home = homedir2()) {
|
|
@@ -4130,6 +4169,7 @@ function shouldUpdate(s) {
|
|
|
4130
4169
|
}
|
|
4131
4170
|
|
|
4132
4171
|
// ../daemon/src/mcp.ts
|
|
4172
|
+
import { join as join5 } from "path";
|
|
4133
4173
|
function memoryMcpCommand(env = process.env) {
|
|
4134
4174
|
const override = env.NANOPM_MCP_COMMAND;
|
|
4135
4175
|
if (override) {
|
|
@@ -4156,13 +4196,17 @@ function memoryMcpServer(job, auth, askTimeoutMs, workspace) {
|
|
|
4156
4196
|
NANOPM_PROJECT_ID: job.project_id,
|
|
4157
4197
|
NANOPM_USER_ID: auth.userId,
|
|
4158
4198
|
NANOPM_ACTOR: job.skill,
|
|
4159
|
-
|
|
4199
|
+
// A build: its questions come back through the answer file the hook hands over (buildAnswerFile).
|
|
4200
|
+
...workspace ? { NANOPM_WORKSPACE: workspace, NANOPM_ANSWER_FILE: buildAnswerFile(job.id) } : {},
|
|
4160
4201
|
NANOPM_ASK_TIMEOUT_MS: String(askTimeoutMs),
|
|
4161
4202
|
PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
4162
4203
|
HOME: process.env.HOME ?? ""
|
|
4163
4204
|
}
|
|
4164
4205
|
};
|
|
4165
4206
|
}
|
|
4207
|
+
function buildAnswerFile(jobId) {
|
|
4208
|
+
return join5(nanopmHome(), "run", jobId, "answer");
|
|
4209
|
+
}
|
|
4166
4210
|
|
|
4167
4211
|
// ../connectors/src/postgres.ts
|
|
4168
4212
|
import postgres from "postgres";
|
|
@@ -4177,15 +4221,15 @@ function stripLiterals(sql) {
|
|
|
4177
4221
|
};
|
|
4178
4222
|
while (i < n) {
|
|
4179
4223
|
const c = sql[i];
|
|
4180
|
-
const
|
|
4181
|
-
if (c === "-" &&
|
|
4224
|
+
const next = sql[i + 1];
|
|
4225
|
+
if (c === "-" && next === "-") {
|
|
4182
4226
|
const end = sql.indexOf("\n", i);
|
|
4183
4227
|
const stop2 = end === -1 ? n : end;
|
|
4184
4228
|
blank(i, stop2);
|
|
4185
4229
|
i = stop2;
|
|
4186
4230
|
continue;
|
|
4187
4231
|
}
|
|
4188
|
-
if (c === "/" &&
|
|
4232
|
+
if (c === "/" && next === "*") {
|
|
4189
4233
|
let depth = 1;
|
|
4190
4234
|
let k = i + 2;
|
|
4191
4235
|
while (k < n && depth > 0) {
|
|
@@ -5273,14 +5317,183 @@ async function sendDigestFor(client, accessToken, jobId, fetchImpl = fetch) {
|
|
|
5273
5317
|
}
|
|
5274
5318
|
|
|
5275
5319
|
// ../daemon/src/deploys.ts
|
|
5320
|
+
import { execFile as execFile4 } from "child_process";
|
|
5321
|
+
import { promisify as promisify4 } from "util";
|
|
5322
|
+
|
|
5323
|
+
// ../daemon/src/repo.ts
|
|
5276
5324
|
import { execFile as execFile3 } from "child_process";
|
|
5325
|
+
import { readFile as readFile2, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
5326
|
+
import { join as join6 } from "path";
|
|
5327
|
+
|
|
5328
|
+
// ../shared/src/build.ts
|
|
5329
|
+
var BUILD_SKILL = "build";
|
|
5330
|
+
var BUILD_STEPS_MIN = 1;
|
|
5331
|
+
var BUILD_STEPS_MAX = 8;
|
|
5332
|
+
var BUILD_STEP_MAX = 120;
|
|
5333
|
+
var BUILD_RESUMES_MAX = 3;
|
|
5334
|
+
var BUILD_TRAILER = "Co-Authored-By: NanoPM <noreply@nanopm.app>";
|
|
5335
|
+
var BUILD_COLUMNS = "id, ref, title, status, actor, agent_spec, spec";
|
|
5336
|
+
function buildRefusal(solution, opts = {}) {
|
|
5337
|
+
if (solution.actor === "founder") return `${solution.ref} is yours to do, not Nano's to build.`;
|
|
5338
|
+
if (!["proposed", "accepted", "building"].includes(solution.status)) return `${solution.ref} is ${solution.status}: only a solution on the roadmap can be built.`;
|
|
5339
|
+
if (!solution.agent_spec) return `${solution.ref} has no agent spec yet. Write its specs first: nanopm spec ${solution.ref} --write`;
|
|
5340
|
+
const open = solution.spec?.questions?.length ?? 0;
|
|
5341
|
+
if (open && !opts.anyway) {
|
|
5342
|
+
return `${solution.ref} still has ${open} question${open === 1 ? "" : "s"} for you. Answer ${open === 1 ? "it" : "them"} in its spec, or build anyway: nanopm build ${solution.ref} --anyway`;
|
|
5343
|
+
}
|
|
5344
|
+
return null;
|
|
5345
|
+
}
|
|
5346
|
+
function stepsLeft(plan, done) {
|
|
5347
|
+
return (plan ?? []).map((_, i) => i).filter((i) => !done.has(i));
|
|
5348
|
+
}
|
|
5349
|
+
function buildDir(home, jobId) {
|
|
5350
|
+
return `${home.replace(/\/+$/, "")}/.cache/nanopm/builds/${jobId}`;
|
|
5351
|
+
}
|
|
5352
|
+
function buildBranch(ref, title) {
|
|
5353
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
|
|
5354
|
+
return `nano/${ref.toLowerCase()}${slug ? `-${slug}` : ""}`;
|
|
5355
|
+
}
|
|
5356
|
+
var REGISTRIES = {
|
|
5357
|
+
"package-lock.json": ["registry.npmjs.org"],
|
|
5358
|
+
"npm-shrinkwrap.json": ["registry.npmjs.org"],
|
|
5359
|
+
"pnpm-lock.yaml": ["registry.npmjs.org"],
|
|
5360
|
+
"yarn.lock": ["registry.yarnpkg.com", "registry.npmjs.org"],
|
|
5361
|
+
"bun.lockb": ["registry.npmjs.org"],
|
|
5362
|
+
"bun.lock": ["registry.npmjs.org"],
|
|
5363
|
+
"poetry.lock": ["pypi.org", "files.pythonhosted.org"],
|
|
5364
|
+
"uv.lock": ["pypi.org", "files.pythonhosted.org"],
|
|
5365
|
+
"Pipfile.lock": ["pypi.org", "files.pythonhosted.org"],
|
|
5366
|
+
"requirements.txt": ["pypi.org", "files.pythonhosted.org"],
|
|
5367
|
+
"Cargo.lock": ["crates.io", "index.crates.io", "static.crates.io"],
|
|
5368
|
+
"Gemfile.lock": ["rubygems.org", "index.rubygems.org"],
|
|
5369
|
+
"go.sum": ["proxy.golang.org", "sum.golang.org"],
|
|
5370
|
+
"composer.lock": ["repo.packagist.org"]
|
|
5371
|
+
};
|
|
5372
|
+
function buildNetwork(rootFiles) {
|
|
5373
|
+
return [...new Set(rootFiles.flatMap((f) => REGISTRIES[f] ?? []))].sort();
|
|
5374
|
+
}
|
|
5375
|
+
async function queueBuild(client, ask7) {
|
|
5376
|
+
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();
|
|
5377
|
+
if (readError) throw new Error(`could not read the solution: ${pgMessage(readError)}`);
|
|
5378
|
+
if (!data) throw new Error(`No solution ${ask7.ref} on the Roadmap.`);
|
|
5379
|
+
const solution = data;
|
|
5380
|
+
const refused = buildRefusal(solution, { anyway: ask7.anyway || Boolean(ask7.resumeOf) });
|
|
5381
|
+
if (refused) throw new Error(refused);
|
|
5382
|
+
const { data: job, error } = await client.from("jobs").insert({
|
|
5383
|
+
project_id: ask7.projectId,
|
|
5384
|
+
skill: BUILD_SKILL,
|
|
5385
|
+
requested_by: ask7.userId,
|
|
5386
|
+
trigger: "user",
|
|
5387
|
+
target_daemon_id: ask7.daemonId,
|
|
5388
|
+
// `was`: where the solution goes back on the board if the build stops (`unstart_solution`).
|
|
5389
|
+
// `origin`: the run that made the copy of the code, which every continuation works in.
|
|
5390
|
+
params: { solution_id: solution.id, repo_path: ask7.repoPath, was: ask7.resumeOf?.was ?? solution.status, ...ask7.resumeOf ? { origin: ask7.resumeOf.origin } : {}, ...ask7.anyway ? { anyway: true } : {} },
|
|
5391
|
+
// Continuing a stopped build takes up its worktree, its history and its session.
|
|
5392
|
+
...ask7.resumeOf ? { resume_of: ask7.resumeOf.id } : {}
|
|
5393
|
+
}).select("id").single();
|
|
5394
|
+
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.");
|
|
5395
|
+
if (error || !job) throw new Error(`could not queue the build: ${error ? pgMessage(error) : "no row"}`);
|
|
5396
|
+
return { jobId: job.id, solution };
|
|
5397
|
+
}
|
|
5398
|
+
var DECIDE_FOR_ME = "Decide yourself: pick what you judge best for the users, then say what you decided and why under *Decisions I made*.";
|
|
5399
|
+
function answerForAgent(answer) {
|
|
5400
|
+
return /^\s*decide(\s+for\s+me)?\s*\.?\s*$/i.test(answer) ? DECIDE_FOR_ME : answer;
|
|
5401
|
+
}
|
|
5402
|
+
function trustedPlace(p, at) {
|
|
5403
|
+
const [branch, base, fork] = [p.branch, p.base, p.fork].map((v) => typeof v === "string" ? v : "");
|
|
5404
|
+
if (!/^nano\/[a-z0-9][a-z0-9-]*$/.test(branch) || !/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(base) || !/^[0-9a-f]{7,40}$/.test(fork)) return null;
|
|
5405
|
+
return { dir: buildDir(at.home, at.jobId), branch, base, fork };
|
|
5406
|
+
}
|
|
5407
|
+
function buildHistory(events, at) {
|
|
5408
|
+
let place = null;
|
|
5409
|
+
let plan = null;
|
|
5410
|
+
const done = /* @__PURE__ */ new Set();
|
|
5411
|
+
let last = null;
|
|
5412
|
+
const answers = /* @__PURE__ */ new Map();
|
|
5413
|
+
for (const e of events) {
|
|
5414
|
+
if (e.kind === "worktree") place = trustedPlace(e.payload, at);
|
|
5415
|
+
else if (e.kind === "plan" && Array.isArray(e.payload.steps)) plan = e.payload.steps;
|
|
5416
|
+
else if (e.kind === "step" && typeof e.payload.step === "number") done.add(e.payload.step - 1);
|
|
5417
|
+
else if (e.kind === "answer") answers.set(e.seq, String(e.payload.answer ?? ""));
|
|
5418
|
+
if (e.kind === "question" || e.kind === "paused") last = e;
|
|
5419
|
+
else if (e.kind === "plan" || e.kind === "step") last = null;
|
|
5420
|
+
}
|
|
5421
|
+
if (!place) return { place, plan, done, resume: null };
|
|
5422
|
+
if (last?.kind === "question" && answers.has(last.seq) && last.payload.about === "usage") return { place, plan, done, resume: { why: "limit" } };
|
|
5423
|
+
if (last?.kind === "question" && answers.has(last.seq)) return { place, plan, done, resume: { why: "answer", answer: answers.get(last.seq) } };
|
|
5424
|
+
if (last?.kind === "paused") return { place, plan, done, resume: { why: "limit" } };
|
|
5425
|
+
return { place, plan, done, resume: { why: "lost" } };
|
|
5426
|
+
}
|
|
5427
|
+
function wasOf(params) {
|
|
5428
|
+
return params?.was === "proposed" ? "proposed" : "accepted";
|
|
5429
|
+
}
|
|
5430
|
+
async function loadBuildHistory(client, jobIds, at) {
|
|
5431
|
+
const { data, error } = await client.from("job_events").select("kind, seq, payload").in("job_id", [...jobIds]).in("kind", ["worktree", "plan", "step", "question", "answer", "paused"]).order("id");
|
|
5432
|
+
if (error) throw new Error(`could not read the build's history: ${error.message}`);
|
|
5433
|
+
return buildHistory((data ?? []).map((e) => ({ kind: e.kind, seq: e.seq, payload: e.payload ?? {} })), at);
|
|
5434
|
+
}
|
|
5435
|
+
|
|
5436
|
+
// ../daemon/src/repo.ts
|
|
5277
5437
|
import { promisify as promisify3 } from "util";
|
|
5278
5438
|
var execFileAsync3 = promisify3(execFile3);
|
|
5279
|
-
var
|
|
5280
|
-
var
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5439
|
+
var MAX_BUFFER = 4 * 1024 * 1024;
|
|
5440
|
+
var TIMEOUT_MS = 6e4;
|
|
5441
|
+
var LOCKED = {
|
|
5442
|
+
GIT_CONFIG_COUNT: "2",
|
|
5443
|
+
GIT_CONFIG_KEY_0: "core.hooksPath",
|
|
5444
|
+
GIT_CONFIG_VALUE_0: "/dev/null",
|
|
5445
|
+
GIT_CONFIG_KEY_1: "core.fsmonitor",
|
|
5446
|
+
GIT_CONFIG_VALUE_1: "false"
|
|
5447
|
+
};
|
|
5448
|
+
async function exec(cmd, repo, args, timeoutMs = TIMEOUT_MS) {
|
|
5449
|
+
if (typeof repo === "string") return execFileAsync3(cmd, cmd === "git" ? ["-C", repo, ...args] : args, { cwd: repo, maxBuffer: MAX_BUFFER, timeout: timeoutMs });
|
|
5450
|
+
await writeFile4(join6(repo.dir, ".git"), `gitdir: ${repo.admin}
|
|
5451
|
+
`);
|
|
5452
|
+
await writeFile4(join6(repo.admin, "commondir"), repo.commondir);
|
|
5453
|
+
await rm3(join6(repo.admin, "config.worktree"), { force: true });
|
|
5454
|
+
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 });
|
|
5455
|
+
}
|
|
5456
|
+
async function git(repo, args, timeoutMs = TIMEOUT_MS) {
|
|
5457
|
+
return (await exec("git", repo, args, timeoutMs)).stdout.trim();
|
|
5458
|
+
}
|
|
5459
|
+
var RepoError = class extends Error {
|
|
5460
|
+
};
|
|
5461
|
+
async function commitAll(cwd, message, note) {
|
|
5462
|
+
await git(cwd, ["add", "-A"]);
|
|
5463
|
+
const staged = await git(cwd, ["diff", "--cached", "--name-only"]).catch(() => "");
|
|
5464
|
+
if (!staged) return null;
|
|
5465
|
+
await git(cwd, ["commit", "--no-verify", "-m", message, ...note ? ["-m", note] : [], "-m", `Written by NanoPM. Review before merging.
|
|
5466
|
+
|
|
5467
|
+
${BUILD_TRAILER}`]);
|
|
5468
|
+
const sha = await git(cwd, ["rev-parse", "--short", "HEAD"]);
|
|
5469
|
+
return { sha, ...await diffStat(cwd) };
|
|
5470
|
+
}
|
|
5471
|
+
async function diffStat(cwd) {
|
|
5472
|
+
const numstat = await git(cwd, ["show", "--numstat", "--format=", "HEAD"]).catch(() => "");
|
|
5473
|
+
let files = 0;
|
|
5474
|
+
let insertions = 0;
|
|
5475
|
+
let deletions = 0;
|
|
5476
|
+
for (const line of numstat.split("\n").filter(Boolean)) {
|
|
5477
|
+
const [add2 = "", del = ""] = line.split(" ");
|
|
5478
|
+
files += 1;
|
|
5479
|
+
insertions += Number(add2) || 0;
|
|
5480
|
+
deletions += Number(del) || 0;
|
|
5481
|
+
}
|
|
5482
|
+
return { files, insertions, deletions };
|
|
5483
|
+
}
|
|
5484
|
+
async function pushBranch(cwd, name) {
|
|
5485
|
+
if (!await originUrl(cwd)) return { pushed: false, reason: "this repository has no origin remote" };
|
|
5486
|
+
try {
|
|
5487
|
+
const { stderr } = await exec("git", cwd, ["push", "--no-verify", "--set-upstream", "origin", name], 12e4);
|
|
5488
|
+
return { pushed: true, reviewUrl: reviewUrlFrom(stderr) };
|
|
5489
|
+
} catch (err) {
|
|
5490
|
+
const text2 = errorMessage(err);
|
|
5491
|
+
return { pushed: false, reason: text2.split("\n").slice(-3).join(" ").trim() || "git push failed" };
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
function reviewUrlFrom(pushOutput) {
|
|
5495
|
+
const match = /https?:\/\/\S+/.exec(pushOutput);
|
|
5496
|
+
return match?.[0]?.replace(/[).,]+$/, "");
|
|
5284
5497
|
}
|
|
5285
5498
|
async function defaultBranch(cwd) {
|
|
5286
5499
|
try {
|
|
@@ -5290,9 +5503,78 @@ async function defaultBranch(cwd) {
|
|
|
5290
5503
|
return git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "HEAD");
|
|
5291
5504
|
}
|
|
5292
5505
|
}
|
|
5506
|
+
async function originUrl(cwd) {
|
|
5507
|
+
return git(cwd, ["remote", "get-url", "origin"]).catch(() => "");
|
|
5508
|
+
}
|
|
5509
|
+
async function baseRef(cwd, branch) {
|
|
5510
|
+
if (!await originUrl(cwd)) return branch;
|
|
5511
|
+
await git(cwd, ["fetch", "--quiet", "origin", branch], 12e4).catch(() => "");
|
|
5512
|
+
return await git(cwd, ["rev-parse", "--verify", "--quiet", `origin/${branch}`]).catch(() => "") ? `origin/${branch}` : branch;
|
|
5513
|
+
}
|
|
5514
|
+
async function addWorktree(clone, dir, branch) {
|
|
5515
|
+
if (!await git(clone, ["rev-parse", "HEAD"]).catch(() => "")) throw new RepoError("this repository has no commits yet; make the first commit, then build");
|
|
5516
|
+
const existing = await git(clone, ["branch", "--list", branch]).catch(() => "");
|
|
5517
|
+
if (existing.trim()) throw new RepoError(`branch ${branch} already exists. Delete it or finish what is on it first.`);
|
|
5518
|
+
const base = await defaultBranch(clone);
|
|
5519
|
+
const onto = await baseRef(clone, base);
|
|
5520
|
+
await git(clone, ["worktree", "add", "-b", branch, dir, onto]);
|
|
5521
|
+
const admin = await git(dir, ["rev-parse", "--absolute-git-dir"]);
|
|
5522
|
+
const worktree = { dir, admin, commondir: await readFile2(join6(admin, "commondir"), "utf8") };
|
|
5523
|
+
return { base, fork: await git(worktree, ["rev-parse", "HEAD"]), worktree };
|
|
5524
|
+
}
|
|
5525
|
+
async function removeWorktree(clone, dir) {
|
|
5526
|
+
await git(clone, ["worktree", "remove", "--force", dir]).catch(() => "");
|
|
5527
|
+
}
|
|
5528
|
+
async function commonGitDir(cwd) {
|
|
5529
|
+
return git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
5530
|
+
}
|
|
5531
|
+
async function commitsSinceFork(cwd, fork) {
|
|
5532
|
+
return Number(await git(cwd, ["rev-list", "--count", `${fork}..HEAD`]).catch(() => "0")) || 0;
|
|
5533
|
+
}
|
|
5534
|
+
async function rebaseOnto(cwd, base) {
|
|
5535
|
+
const onto = await baseRef(cwd, base);
|
|
5536
|
+
try {
|
|
5537
|
+
await git(cwd, ["rebase", "--no-verify", onto], 12e4);
|
|
5538
|
+
return { ok: true };
|
|
5539
|
+
} catch {
|
|
5540
|
+
const files = (await git(cwd, ["diff", "--name-only", "--diff-filter=U"]).catch(() => "")).split("\n").filter(Boolean);
|
|
5541
|
+
await git(cwd, ["rebase", "--abort"]).catch(() => "");
|
|
5542
|
+
return { ok: false, files };
|
|
5543
|
+
}
|
|
5544
|
+
}
|
|
5545
|
+
async function gh(cwd, args) {
|
|
5546
|
+
return (await exec("gh", cwd, args, 12e4)).stdout.trim();
|
|
5547
|
+
}
|
|
5548
|
+
async function ghReady(cwd) {
|
|
5549
|
+
if (!/github\.com[:/]/.test(await originUrl(cwd))) return "this repository has no GitHub origin, so there is nowhere to open a pull request";
|
|
5550
|
+
try {
|
|
5551
|
+
await gh(cwd, ["auth", "status"]);
|
|
5552
|
+
return null;
|
|
5553
|
+
} catch (err) {
|
|
5554
|
+
return /ENOENT/.test(errorMessage(err)) ? "the GitHub CLI is not installed on this machine: brew install gh, then gh auth login" : "the GitHub CLI is not signed in on this machine: run gh auth login in your terminal";
|
|
5555
|
+
}
|
|
5556
|
+
}
|
|
5557
|
+
async function openPullRequest(cwd, pr) {
|
|
5558
|
+
try {
|
|
5559
|
+
const out = await gh(cwd, ["pr", "create", "--draft", "--head", pr.branch, "--base", pr.base, "--title", pr.title, "--body", pr.body]);
|
|
5560
|
+
const url = reviewUrlFrom(out) ?? out.split("\n").at(-1) ?? "";
|
|
5561
|
+
return { ok: true, url, number: Number(/\/pull\/(\d+)/.exec(url)?.[1] ?? 0) };
|
|
5562
|
+
} catch (err) {
|
|
5563
|
+
return { ok: false, reason: errorMessage(err).split("\n").filter(Boolean).slice(-2).join(" ") || "gh pr create failed" };
|
|
5564
|
+
}
|
|
5565
|
+
}
|
|
5566
|
+
|
|
5567
|
+
// ../daemon/src/deploys.ts
|
|
5568
|
+
var execFileAsync4 = promisify4(execFile4);
|
|
5569
|
+
var SETTLE_HOURS = 3;
|
|
5570
|
+
var MIN_COMMITS = 3;
|
|
5571
|
+
async function git2(cwd, args) {
|
|
5572
|
+
const { stdout } = await execFileAsync4("git", ["-C", cwd, ...args], { timeout: 2e4, maxBuffer: 1024 * 1024 });
|
|
5573
|
+
return stdout.trim();
|
|
5574
|
+
}
|
|
5293
5575
|
async function commitsSince(cwd, since) {
|
|
5294
5576
|
const branch = await defaultBranch(cwd);
|
|
5295
|
-
const out = await
|
|
5577
|
+
const out = await git2(cwd, ["log", branch, `--since=${since.toISOString()}`, "--format=%cI"]);
|
|
5296
5578
|
const stamps = out.split("\n").filter(Boolean);
|
|
5297
5579
|
return { count: stamps.length, newestAt: stamps[0] ? new Date(stamps[0]) : null };
|
|
5298
5580
|
}
|
|
@@ -5346,6 +5628,275 @@ async function reviewDeploys(client, daemonId, now = /* @__PURE__ */ new Date())
|
|
|
5346
5628
|
return results;
|
|
5347
5629
|
}
|
|
5348
5630
|
|
|
5631
|
+
// ../daemon/src/build-job.ts
|
|
5632
|
+
import { existsSync as existsSync2 } from "fs";
|
|
5633
|
+
import { mkdir as mkdir2, readdir, readFile as readFile3, writeFile as writeFile5 } from "fs/promises";
|
|
5634
|
+
import { homedir as homedir3 } from "os";
|
|
5635
|
+
import { basename, join as join7 } from "path";
|
|
5636
|
+
|
|
5637
|
+
// ../daemon/src/usage.ts
|
|
5638
|
+
function sumUsage(a, b) {
|
|
5639
|
+
if (!a) return { ...b };
|
|
5640
|
+
return {
|
|
5641
|
+
...b,
|
|
5642
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
5643
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
5644
|
+
cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
|
|
5645
|
+
cacheWriteTokens: (a.cacheWriteTokens ?? 0) + (b.cacheWriteTokens ?? 0),
|
|
5646
|
+
costUsd: (a.costUsd ?? 0) + (b.costUsd ?? 0),
|
|
5647
|
+
turns: a.turns + b.turns,
|
|
5648
|
+
durationMs: a.durationMs + b.durationMs,
|
|
5649
|
+
...a.contextPeakTokens || b.contextPeakTokens ? { contextPeakTokens: Math.max(a.contextPeakTokens ?? 0, b.contextPeakTokens ?? 0) } : {},
|
|
5650
|
+
...a.reads || b.reads ? { reads: sumReads(a.reads, b.reads) } : {}
|
|
5651
|
+
};
|
|
5652
|
+
}
|
|
5653
|
+
function sumReads(a, b) {
|
|
5654
|
+
const calls = { ...a?.calls ?? {} };
|
|
5655
|
+
for (const [tool, n] of Object.entries(b?.calls ?? {})) calls[tool] = (calls[tool] ?? 0) + n;
|
|
5656
|
+
return { files: (a?.files ?? 0) + (b?.files ?? 0), calls };
|
|
5657
|
+
}
|
|
5658
|
+
|
|
5659
|
+
// ../daemon/src/build-job.ts
|
|
5660
|
+
var PLAN_TOOL = "mcp__nanopm__build_plan";
|
|
5661
|
+
var ASK_TOOL = "mcp__nanopm__ask_user";
|
|
5662
|
+
var STEP_TOOL = "mcp__nanopm__build_step";
|
|
5663
|
+
var CLAUDE_CODE_MIN = "2.1.283";
|
|
5664
|
+
async function prepareBuild(client, job, clone, adapter) {
|
|
5665
|
+
const params = job.params ?? {};
|
|
5666
|
+
const dir = buildDir(homedir3(), job.id);
|
|
5667
|
+
let made = false;
|
|
5668
|
+
try {
|
|
5669
|
+
const [solution, machine, history] = await Promise.all([
|
|
5670
|
+
loadSolution(client, job.project_id, String(params.solution_id ?? "")),
|
|
5671
|
+
machineRefusal(adapter),
|
|
5672
|
+
loadHistory(client, job)
|
|
5673
|
+
]);
|
|
5674
|
+
const refused = buildRefusal(solution, { anyway: params.anyway === true || Boolean(history.place) }) ?? machine;
|
|
5675
|
+
if (refused) return { ok: false, outcome: failed(refused) };
|
|
5676
|
+
const network = async (at) => buildNetwork(await readdir(at));
|
|
5677
|
+
if (history.place) {
|
|
5678
|
+
const { place } = history;
|
|
5679
|
+
if (!existsSync2(place.dir)) return { ok: false, outcome: failed(`this build's copy of the code is gone (${place.dir}): build it again`) };
|
|
5680
|
+
const worktree2 = { dir: place.dir, admin: join7(await commonGitDir(clone), "worktrees", basename(place.dir)), commondir: "../..\n" };
|
|
5681
|
+
await startSolution(client, job.project_id, solution.ref, job.id);
|
|
5682
|
+
return { ok: true, build: { solution, clone, ...place, worktree: worktree2, network: await network(place.dir), history } };
|
|
5683
|
+
}
|
|
5684
|
+
const branch = buildBranch(solution.ref, solution.title);
|
|
5685
|
+
await mkdir2(join7(dir, ".."), { recursive: true, mode: 448 });
|
|
5686
|
+
const { base, fork, worktree } = await addWorktree(clone, dir, branch);
|
|
5687
|
+
made = true;
|
|
5688
|
+
await appendJobEvent(client, job.id, "worktree", { dir, branch, base, fork }, "nanopm");
|
|
5689
|
+
await handOverTheSpec(worktree, solution.agent_spec);
|
|
5690
|
+
await startSolution(client, job.project_id, solution.ref, job.id);
|
|
5691
|
+
return { ok: true, build: { solution, clone, dir, worktree, branch, base, fork, network: await network(dir), history } };
|
|
5692
|
+
} catch (err) {
|
|
5693
|
+
if (made) await removeWorktree(clone, dir);
|
|
5694
|
+
return { ok: false, outcome: failed(errorMessage(err)) };
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5697
|
+
async function runBuildJob(input) {
|
|
5698
|
+
const { client, job, adapter, spec: spec2, events, log, verifyCommand: verifyCommand2 } = input;
|
|
5699
|
+
const { solution, clone, dir, worktree, branch, base, fork, history } = input.build;
|
|
5700
|
+
const was = wasOf(job.params);
|
|
5701
|
+
let plan = history.plan;
|
|
5702
|
+
const done = new Set(history.done);
|
|
5703
|
+
let usage;
|
|
5704
|
+
let said = "";
|
|
5705
|
+
const stop2 = async (message, opts = { keep: true }) => {
|
|
5706
|
+
await unstartSolution(client, job.project_id, solution.ref, was).catch(() => void 0);
|
|
5707
|
+
if (opts.keep) await commitAll(worktree, `${solution.ref}: unfinished \u2014 ${solution.title}`).catch(() => null);
|
|
5708
|
+
else await removeWorktree(clone, dir);
|
|
5709
|
+
return { ...failed(opts.keep ? `${message}. What was done is on ${branch}, in ${dir}.` : message), usage };
|
|
5710
|
+
};
|
|
5711
|
+
const park = async (why) => {
|
|
5712
|
+
if ("ask" in why) {
|
|
5713
|
+
await events.write("question", { ...why.ask, ...plan ? {} : { about: "plan" } });
|
|
5714
|
+
await log("build.ask", `${solution.ref} waits on the founder`);
|
|
5715
|
+
} else if (why.resetsAt) {
|
|
5716
|
+
await events.write("paused", { why: "limit", resets_at: new Date(why.resetsAt * 1e3).toISOString() });
|
|
5717
|
+
await log("build.pause", `${solution.ref} paused until the Claude limit resets`);
|
|
5718
|
+
} else {
|
|
5719
|
+
await events.write("question", { question: "Your Claude usage is used up. Get more in Claude, or wait for it to come back, then answer to carry on.", options: ["Carry on"], about: "usage" });
|
|
5720
|
+
}
|
|
5721
|
+
return { status: "waiting", usage };
|
|
5722
|
+
};
|
|
5723
|
+
const session2 = async (prompt, resume3) => {
|
|
5724
|
+
let end = { stop: failed("the agent produced no result") };
|
|
5725
|
+
for await (const event of adapter.run({ ...spec2, prompt, resumeSessionId: resume3 ? spec2.sessionId : void 0 })) {
|
|
5726
|
+
await input.onEvent(event);
|
|
5727
|
+
if (event.type === "tool_use" && event.tool === PLAN_TOOL && !plan) {
|
|
5728
|
+
plan = stepsOf(event.input);
|
|
5729
|
+
if (plan) await events.write("plan", { steps: plan });
|
|
5730
|
+
} else if (event.type === "tool_result" && event.tool === STEP_TOOL && !event.isError) {
|
|
5731
|
+
const { step, summary } = event.input ?? {};
|
|
5732
|
+
const index = Number(step) - 1;
|
|
5733
|
+
if (Number.isInteger(index) && index >= 0 && !done.has(index)) {
|
|
5734
|
+
const says = plan?.[index] ?? `step ${index + 1}`;
|
|
5735
|
+
const commit = await commitAll(worktree, `${solution.ref}: ${says}`, summary);
|
|
5736
|
+
done.add(index);
|
|
5737
|
+
await events.write("step", { step: index + 1, says, commit: commit?.sha ?? null, files: commit?.files ?? 0 });
|
|
5738
|
+
}
|
|
5739
|
+
} else if (event.type === "finished") {
|
|
5740
|
+
usage = sumUsage(usage, event.usage);
|
|
5741
|
+
said = event.result;
|
|
5742
|
+
end = null;
|
|
5743
|
+
} else if (event.type === "deferred" && event.tool === ASK_TOOL) {
|
|
5744
|
+
usage = sumUsage(usage, event.usage);
|
|
5745
|
+
end = { park: { ask: askOf(event.input) } };
|
|
5746
|
+
} else if (event.type === "failed") {
|
|
5747
|
+
end = event.code === "rate_limit" ? { park: { resetsAt: event.resetsAt } } : { stop: { status: "failed", error: { code: event.code, message: event.message } } };
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5750
|
+
return end;
|
|
5751
|
+
};
|
|
5752
|
+
const resume2 = history.resume;
|
|
5753
|
+
const check = verifyCommand2 ? `
|
|
5754
|
+
|
|
5755
|
+
The project's own check is \`${verifyCommand2}\`. Run it before your last build_step, fix what it reports, and give its result under *How I checked it*.` : "";
|
|
5756
|
+
const finished = () => Boolean(plan && !stepsLeft(plan, done).length);
|
|
5757
|
+
if (!resume2) {
|
|
5758
|
+
await log("build.start", `building ${solution.ref} on ${branch}, in a copy of your code`);
|
|
5759
|
+
await events.write("text", { text: `Working on ${branch}, in a copy of your code. Your own checkout is not touched.` });
|
|
5760
|
+
} else {
|
|
5761
|
+
await events.write("text", { text: AGAIN[resume2.why].said });
|
|
5762
|
+
if (resume2.why === "answer" && spec2.answerFile) await writeFile5(spec2.answerFile, answerForAgent(resume2.answer), { mode: 384 });
|
|
5763
|
+
}
|
|
5764
|
+
let turn = !resume2 ? await session2(spec2.prompt + check, false) : resume2.why === "answer" ? await session2("", true) : finished() ? null : await session2(`${AGAIN[resume2.why].lead} ${carryOn(plan, done)}`, true);
|
|
5765
|
+
for (let resumes = 0; !turn && !finished(); resumes += 1) {
|
|
5766
|
+
if (resumes >= BUILD_RESUMES_MAX) {
|
|
5767
|
+
const left = stepsLeft(plan, done).length;
|
|
5768
|
+
return stop2(plan ? `Nano stopped with ${left} step${left === 1 ? "" : "s"} of its plan left` : "Nano stopped without saying its plan");
|
|
5769
|
+
}
|
|
5770
|
+
turn = await session2(carryOn(plan, done), true);
|
|
5771
|
+
}
|
|
5772
|
+
if (turn && "park" in turn) return park(turn.park);
|
|
5773
|
+
if (turn) return stop2(turn.stop.error?.message ?? "the build stopped");
|
|
5774
|
+
await commitAll(worktree, `${solution.ref}: ${solution.title}`);
|
|
5775
|
+
if (!await commitsSinceFork(worktree, fork)) return stop2("the build finished without changing anything", { keep: false });
|
|
5776
|
+
const rebased = await rebaseOnto(worktree, base);
|
|
5777
|
+
if (!rebased.ok) return stop2(`${base} moved while Nano was building, and the same files changed on both sides: ${rebased.files.join(", ") || "see git status"}`);
|
|
5778
|
+
const [pushed, noPr, page] = await Promise.all([pushBranch(worktree, branch), ghReady(worktree), solutionPage(client, job.project_id, solution.ref)]);
|
|
5779
|
+
if (!pushed.pushed) return stop2(`the push failed: ${pushed.reason}`);
|
|
5780
|
+
const pr = noPr ? null : await openPullRequest(worktree, { branch, base, title: `${solution.ref}: ${solution.title}`, body: prBody(said, page) });
|
|
5781
|
+
if (pr && !pr.ok) return { ...failed(`the branch ${branch} is pushed, but GitHub refused the pull request: ${pr.reason}${pushed.reviewUrl ? `. Open it yourself: ${pushed.reviewUrl}` : ""}`), usage };
|
|
5782
|
+
if (pr) {
|
|
5783
|
+
await events.write("pr", { number: pr.number, url: pr.url });
|
|
5784
|
+
await log("build.pr", `draft pull request #${pr.number} for ${solution.ref}`);
|
|
5785
|
+
}
|
|
5786
|
+
await removeWorktree(clone, dir);
|
|
5787
|
+
return {
|
|
5788
|
+
status: "succeeded",
|
|
5789
|
+
result: [
|
|
5790
|
+
said.trim(),
|
|
5791
|
+
"",
|
|
5792
|
+
pr ? `Draft pull request #${pr.number} \xB7 ${pr.url}` : `Pushed ${branch}. No pull request: ${noPr}.${pushed.reviewUrl ? ` ${pushed.reviewUrl}` : ""}`
|
|
5793
|
+
].filter(Boolean).join("\n"),
|
|
5794
|
+
data: { branch, session: spec2.sessionId, ...pr ? { pr: { number: pr.number, url: pr.url } } : { review_url: pushed.reviewUrl ?? null } },
|
|
5795
|
+
usage
|
|
5796
|
+
};
|
|
5797
|
+
}
|
|
5798
|
+
var failed = (message) => ({ status: "failed", error: { code: "harness_error", message } });
|
|
5799
|
+
var AGAIN = {
|
|
5800
|
+
answer: { said: "Your answer is in: carrying on.", lead: "" },
|
|
5801
|
+
limit: { said: "Your Claude limit has reset: carrying on.", lead: "Your Claude limit has reset." },
|
|
5802
|
+
lost: { said: "Picked up again where it stopped.", lead: "You were interrupted: the machine stopped." }
|
|
5803
|
+
};
|
|
5804
|
+
function askOf(input) {
|
|
5805
|
+
const { question, options, why } = input ?? {};
|
|
5806
|
+
return {
|
|
5807
|
+
question: String(question ?? ""),
|
|
5808
|
+
...Array.isArray(options) ? { options: options.map(String) } : {},
|
|
5809
|
+
...typeof why === "string" ? { why } : {}
|
|
5810
|
+
};
|
|
5811
|
+
}
|
|
5812
|
+
async function loadHistory(client, job) {
|
|
5813
|
+
const named = (job.params ?? {}).origin;
|
|
5814
|
+
const origin = typeof named === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(named) ? named : job.id;
|
|
5815
|
+
const { data: runs } = await client.from("jobs").select("id").eq("project_id", job.project_id).eq("skill", BUILD_SKILL).or(`id.eq.${origin},params->>origin.eq.${origin}`);
|
|
5816
|
+
return loadBuildHistory(client, [.../* @__PURE__ */ new Set([origin, job.id, ...(runs ?? []).map((r) => r.id)])], { home: homedir3(), jobId: origin });
|
|
5817
|
+
}
|
|
5818
|
+
async function loadSolution(client, projectId, id) {
|
|
5819
|
+
const { data, error } = await client.from("solutions").select(BUILD_COLUMNS).eq("id", id).eq("project_id", projectId).is("superseded_by", null).maybeSingle();
|
|
5820
|
+
if (error) throw new Error(`could not read the solution: ${error.message}`);
|
|
5821
|
+
if (!data) throw new Error("that solution is not on this project any more");
|
|
5822
|
+
return data;
|
|
5823
|
+
}
|
|
5824
|
+
async function machineRefusal(adapter) {
|
|
5825
|
+
const found = await adapter.detect();
|
|
5826
|
+
const version = /\d+\.\d+\.\d+/.exec(found.version ?? "")?.[0];
|
|
5827
|
+
if (version && !newer(CLAUDE_CODE_MIN, version)) return null;
|
|
5828
|
+
return `a build needs Claude Code ${CLAUDE_CODE_MIN} or later on this machine${found.version ? ` (it has ${found.version})` : ""}: run claude update`;
|
|
5829
|
+
}
|
|
5830
|
+
async function handOverTheSpec(worktree, agentSpec) {
|
|
5831
|
+
await mkdir2(join7(worktree.dir, ".nanopm"), { recursive: true });
|
|
5832
|
+
await writeFile5(join7(worktree.dir, ".nanopm", "SPEC.md"), agentSpec);
|
|
5833
|
+
const exclude = join7(await commonGitDir(worktree), "info", "exclude");
|
|
5834
|
+
const current = await readFile3(exclude, "utf8").catch(() => "");
|
|
5835
|
+
if (!current.split("\n").includes(".nanopm/")) {
|
|
5836
|
+
await mkdir2(join7(exclude, ".."), { recursive: true });
|
|
5837
|
+
await writeFile5(exclude, `${current}${current && !current.endsWith("\n") ? "\n" : ""}.nanopm/
|
|
5838
|
+
`);
|
|
5839
|
+
}
|
|
5840
|
+
}
|
|
5841
|
+
function stepsOf(input) {
|
|
5842
|
+
const steps = input?.steps;
|
|
5843
|
+
return Array.isArray(steps) && steps.length && steps.every((s) => typeof s === "string") ? steps : null;
|
|
5844
|
+
}
|
|
5845
|
+
function carryOn(plan, done) {
|
|
5846
|
+
if (!plan) return "You stopped before saying your plan. Call build_plan with it now, then build it step by step, calling build_step after each.";
|
|
5847
|
+
const left = stepsLeft(plan, done);
|
|
5848
|
+
return [
|
|
5849
|
+
`Steps ${left.map((i) => i + 1).join(", ")} of your plan are not done yet:`,
|
|
5850
|
+
...left.map((i) => `${i + 1}. ${plan[i]}`),
|
|
5851
|
+
"",
|
|
5852
|
+
"Check git log and git status to see where you are, then carry on: finish each step, run its checks, and call build_step after it. Do not run anything in the background."
|
|
5853
|
+
].join("\n");
|
|
5854
|
+
}
|
|
5855
|
+
async function solutionPage(client, projectId, ref) {
|
|
5856
|
+
const { data } = await client.from("projects").select("slug").eq("id", projectId).maybeSingle();
|
|
5857
|
+
return data?.slug ? `${webUrl()}/p/${data.slug}/roadmap/${ref}` : null;
|
|
5858
|
+
}
|
|
5859
|
+
function prBody(said, page) {
|
|
5860
|
+
return [
|
|
5861
|
+
said.trim(),
|
|
5862
|
+
page ? `
|
|
5863
|
+
The solution and its spec: ${page}` : "",
|
|
5864
|
+
"\n\u2014 Built by NanoPM with Claude Code. A draft: review before merging."
|
|
5865
|
+
].filter(Boolean).join("\n");
|
|
5866
|
+
}
|
|
5867
|
+
async function startSolution(client, projectId, ref, jobId) {
|
|
5868
|
+
const { error } = await client.rpc("start_solution", { p_project_id: projectId, p_ref: ref, p_job_id: jobId });
|
|
5869
|
+
if (error) throw new Error(pgMessage(error));
|
|
5870
|
+
}
|
|
5871
|
+
async function unstartSolution(client, projectId, ref, was) {
|
|
5872
|
+
await client.rpc("unstart_solution", { p_project_id: projectId, p_ref: ref, p_to: was });
|
|
5873
|
+
}
|
|
5874
|
+
async function requeueLostBuild(client, job) {
|
|
5875
|
+
const params = job.params ?? {};
|
|
5876
|
+
if ((job.attempt ?? 1) < 2) {
|
|
5877
|
+
const { data } = await client.from("jobs").update({ status: "queued", attempt: (job.attempt ?? 1) + 1, assigned_daemon_id: null }).eq("id", job.id).eq("status", "running").select("id");
|
|
5878
|
+
if (data?.length) return true;
|
|
5879
|
+
}
|
|
5880
|
+
const { data: solution } = await client.from("solutions").select("ref").eq("id", params.solution_id ?? "").maybeSingle();
|
|
5881
|
+
if (job.project_id && solution) await unstartSolution(client, job.project_id, solution.ref, wasOf(job.params)).catch(() => void 0);
|
|
5882
|
+
return false;
|
|
5883
|
+
}
|
|
5884
|
+
async function resumePausedBuilds(client, daemonId, now = Date.now()) {
|
|
5885
|
+
const { data: parked } = await client.from("jobs").select("id").eq("skill", BUILD_SKILL).eq("status", "waiting").eq("assigned_daemon_id", daemonId);
|
|
5886
|
+
const ids = (parked ?? []).map((j) => j.id);
|
|
5887
|
+
if (!ids.length) return [];
|
|
5888
|
+
const { data: stops } = await client.from("job_events").select("job_id, kind, payload").in("job_id", ids).in("kind", ["paused", "question"]).order("id", { ascending: false });
|
|
5889
|
+
const last = /* @__PURE__ */ new Map();
|
|
5890
|
+
for (const e of stops ?? []) if (!last.has(e.job_id)) last.set(e.job_id, e);
|
|
5891
|
+
const due = ids.filter((id) => {
|
|
5892
|
+
const stop2 = last.get(id);
|
|
5893
|
+
return stop2?.kind === "paused" && Date.parse(String(stop2.payload?.resets_at ?? "")) <= now;
|
|
5894
|
+
});
|
|
5895
|
+
if (!due.length) return [];
|
|
5896
|
+
const { data } = await client.from("jobs").update({ status: "queued", assigned_daemon_id: null }).in("id", due).eq("status", "waiting").select("id");
|
|
5897
|
+
return (data ?? []).map((j) => j.id);
|
|
5898
|
+
}
|
|
5899
|
+
|
|
5349
5900
|
// ../daemon/src/review-preflight.ts
|
|
5350
5901
|
async function reviewWithoutData(client, job) {
|
|
5351
5902
|
if (job.skill !== "review" || !job.project_id) return null;
|
|
@@ -5375,17 +5926,17 @@ async function reviewWithoutData(client, job) {
|
|
|
5375
5926
|
}
|
|
5376
5927
|
|
|
5377
5928
|
// ../daemon/src/skills.ts
|
|
5378
|
-
import { existsSync as
|
|
5379
|
-
import { readFile as
|
|
5380
|
-
import { dirname as dirname2, join as
|
|
5929
|
+
import { existsSync as existsSync3, readdirSync } from "fs";
|
|
5930
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
5931
|
+
import { dirname as dirname2, join as join8, resolve as resolve2 } from "path";
|
|
5381
5932
|
function findSkillsDir(env = process.env) {
|
|
5382
5933
|
if (env.NANOPM_SKILLS_DIR) return env.NANOPM_SKILLS_DIR;
|
|
5383
5934
|
const candidates = [
|
|
5384
|
-
|
|
5935
|
+
join8(process.cwd(), "skills"),
|
|
5385
5936
|
...process.argv[1] ? [resolve2(dirname2(process.argv[1]), "..", "skills")] : []
|
|
5386
5937
|
];
|
|
5387
5938
|
for (const dir of candidates) {
|
|
5388
|
-
if (
|
|
5939
|
+
if (existsSync3(join8(dir, "onboard", "skill.json"))) return dir;
|
|
5389
5940
|
}
|
|
5390
5941
|
throw new Error(`Cannot find the skills directory (tried ${candidates.join(", ")}). Set NANOPM_SKILLS_DIR.`);
|
|
5391
5942
|
}
|
|
@@ -5395,9 +5946,9 @@ function skillDirs(skillsDir, env = process.env) {
|
|
|
5395
5946
|
}
|
|
5396
5947
|
async function loadSkill(name, skillsDir = findSkillsDir()) {
|
|
5397
5948
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new Error(`Invalid skill name "${name}"`);
|
|
5398
|
-
const dir = skillDirs(skillsDir).map((d) =>
|
|
5949
|
+
const dir = skillDirs(skillsDir).map((d) => join8(d, name)).find((d) => existsSync3(join8(d, "skill.json")));
|
|
5399
5950
|
if (!dir) throw new Error(`Unknown skill "${name}"`);
|
|
5400
|
-
const raw = JSON.parse(await
|
|
5951
|
+
const raw = JSON.parse(await readFile4(join8(dir, "skill.json"), "utf8"));
|
|
5401
5952
|
let manifest;
|
|
5402
5953
|
try {
|
|
5403
5954
|
manifest = parseSkillManifest(raw);
|
|
@@ -5406,7 +5957,7 @@ async function loadSkill(name, skillsDir = findSkillsDir()) {
|
|
|
5406
5957
|
}
|
|
5407
5958
|
const tainted = taintedByTheWeb(manifest);
|
|
5408
5959
|
if (tainted) throw new Error(`the "${name}" skill cannot be loaded: ${tainted}`);
|
|
5409
|
-
const body = await
|
|
5960
|
+
const body = await readFile4(join8(dir, "SKILL.md"), "utf8");
|
|
5410
5961
|
return { manifest, body, dir };
|
|
5411
5962
|
}
|
|
5412
5963
|
var NOT_WITH_READ_WEB = ["write_memory", "problems", "propose", "review", "write_repo", "run_commands"];
|
|
@@ -5441,7 +5992,7 @@ function renderPrompt(template, params) {
|
|
|
5441
5992
|
var NEEDS_THE_REQUESTERS_CLONE = ["write_repo", "run_commands"];
|
|
5442
5993
|
var NEEDS_THE_REQUESTERS_IDENTITY = /* @__PURE__ */ new Set(["want", "start"]);
|
|
5443
5994
|
async function clonelessSkills(skillsDir = findSkillsDir()) {
|
|
5444
|
-
const names = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() &&
|
|
5995
|
+
const names = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync3(join8(skillsDir, d.name, "skill.json"))).map((d) => d.name);
|
|
5445
5996
|
const out = [];
|
|
5446
5997
|
for (const name of names) {
|
|
5447
5998
|
try {
|
|
@@ -5495,30 +6046,6 @@ ${JSON.stringify(mappingOutputSchema(input.role))}`,
|
|
|
5495
6046
|
|
|
5496
6047
|
// ../daemon/src/problem-mapping.ts
|
|
5497
6048
|
import { createHash as createHash2 } from "crypto";
|
|
5498
|
-
|
|
5499
|
-
// ../daemon/src/usage.ts
|
|
5500
|
-
function sumUsage(a, b) {
|
|
5501
|
-
if (!a) return { ...b };
|
|
5502
|
-
return {
|
|
5503
|
-
...b,
|
|
5504
|
-
inputTokens: a.inputTokens + b.inputTokens,
|
|
5505
|
-
outputTokens: a.outputTokens + b.outputTokens,
|
|
5506
|
-
cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
|
|
5507
|
-
cacheWriteTokens: (a.cacheWriteTokens ?? 0) + (b.cacheWriteTokens ?? 0),
|
|
5508
|
-
costUsd: (a.costUsd ?? 0) + (b.costUsd ?? 0),
|
|
5509
|
-
turns: a.turns + b.turns,
|
|
5510
|
-
durationMs: a.durationMs + b.durationMs,
|
|
5511
|
-
...a.contextPeakTokens || b.contextPeakTokens ? { contextPeakTokens: Math.max(a.contextPeakTokens ?? 0, b.contextPeakTokens ?? 0) } : {},
|
|
5512
|
-
...a.reads || b.reads ? { reads: sumReads(a.reads, b.reads) } : {}
|
|
5513
|
-
};
|
|
5514
|
-
}
|
|
5515
|
-
function sumReads(a, b) {
|
|
5516
|
-
const calls = { ...a?.calls ?? {} };
|
|
5517
|
-
for (const [tool, n] of Object.entries(b?.calls ?? {})) calls[tool] = (calls[tool] ?? 0) + n;
|
|
5518
|
-
return { files: (a?.files ?? 0) + (b?.files ?? 0), calls };
|
|
5519
|
-
}
|
|
5520
|
-
|
|
5521
|
-
// ../daemon/src/problem-mapping.ts
|
|
5522
6049
|
var MAPPING_TURN_LIMIT = 40;
|
|
5523
6050
|
var MAPPING_TIMEOUT_MS = 20 * 6e4;
|
|
5524
6051
|
async function runProblemMapping(ports) {
|
|
@@ -5568,9 +6095,9 @@ async function runProblemMapping(ports) {
|
|
|
5568
6095
|
if (cycle === 1 && ports.collect) {
|
|
5569
6096
|
const maxTurns = Math.min(8, MAPPING_TURN_LIMIT - (usage?.turns ?? 0) - 2);
|
|
5570
6097
|
await ports.checkpoint("collector.started", { maxTurns: Math.max(0, maxTurns), startedAt: Date.now() });
|
|
5571
|
-
let
|
|
6098
|
+
let next = null;
|
|
5572
6099
|
try {
|
|
5573
|
-
|
|
6100
|
+
next = await ports.collect(proposal.investigation, signal, maxTurns);
|
|
5574
6101
|
} catch (err) {
|
|
5575
6102
|
signal.throwIfAborted();
|
|
5576
6103
|
failure2 = errorMessage(err);
|
|
@@ -5578,13 +6105,13 @@ async function runProblemMapping(ports) {
|
|
|
5578
6105
|
usage = sumUsage(usage, spent);
|
|
5579
6106
|
await ports.checkpoint("collector", { usage: spent, failed: failure2 });
|
|
5580
6107
|
}
|
|
5581
|
-
if (!
|
|
6108
|
+
if (!next && !failure2) await ports.checkpoint("collector", { unavailable: true });
|
|
5582
6109
|
signal.throwIfAborted();
|
|
5583
|
-
if (
|
|
5584
|
-
if (
|
|
5585
|
-
await ports.checkpoint("collector", { usage:
|
|
6110
|
+
if (next) {
|
|
6111
|
+
if (next.usage.turns > 0) usage = sumUsage(usage, next.usage);
|
|
6112
|
+
await ports.checkpoint("collector", { usage: next.usage });
|
|
5586
6113
|
if ((usage?.turns ?? 0) >= MAPPING_TURN_LIMIT) throw new Error("Collection exhausted the shared turn budget.");
|
|
5587
|
-
snapshot3 = copySnapshot(
|
|
6114
|
+
snapshot3 = copySnapshot(next.snapshot);
|
|
5588
6115
|
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
6116
|
await ports.checkpoint("collection", { snapshot_hash: mappingHash(snapshot3), request: proposal.investigation });
|
|
5590
6117
|
continue;
|
|
@@ -5770,8 +6297,8 @@ async function askAgainIfUnanswered(attempt7, say, role7) {
|
|
|
5770
6297
|
var UNANSWERED = /structured[ _]output|returned no result/i;
|
|
5771
6298
|
|
|
5772
6299
|
// ../daemon/src/site.ts
|
|
5773
|
-
import { readFile as
|
|
5774
|
-
import { join as
|
|
6300
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
6301
|
+
import { join as join9 } from "path";
|
|
5775
6302
|
var SITE_PATHS = ["/", "/about", "/pricing"];
|
|
5776
6303
|
var PAGE_CHARS = 6e3;
|
|
5777
6304
|
var SITE_TIMEOUT_MS = 2e4;
|
|
@@ -5894,7 +6421,7 @@ function siteFromSearch(name, hits) {
|
|
|
5894
6421
|
}
|
|
5895
6422
|
async function read(repoRoot, name) {
|
|
5896
6423
|
try {
|
|
5897
|
-
const text2 = await
|
|
6424
|
+
const text2 = await readFile5(join9(repoRoot, name), "utf8");
|
|
5898
6425
|
return text2.length > 2e5 ? null : text2;
|
|
5899
6426
|
} catch {
|
|
5900
6427
|
return null;
|
|
@@ -5948,8 +6475,8 @@ async function fromReadme(repoRoot) {
|
|
|
5948
6475
|
}
|
|
5949
6476
|
|
|
5950
6477
|
// ../daemon/src/repo-docs.ts
|
|
5951
|
-
import { readFile as
|
|
5952
|
-
import { join as
|
|
6478
|
+
import { readFile as readFile6, readdir as readdir2, stat } from "fs/promises";
|
|
6479
|
+
import { join as join10, relative } from "path";
|
|
5953
6480
|
var WRITTEN = /(persona|audience|customer|user-research|research\/|icp|segment)/i;
|
|
5954
6481
|
var ABOUT_PEOPLE = /(persona|audience|customer|user-research|research\/|interview|feedback|survey|verbatim|voice|icp|segment)/i;
|
|
5955
6482
|
var DOC = /\.(md|mdx|txt)$/i;
|
|
@@ -5962,12 +6489,12 @@ async function readDocs(cwd, opts = {}) {
|
|
|
5962
6489
|
if (depth > 3 || found.length >= DOCS_MAX * 4) return;
|
|
5963
6490
|
let entries;
|
|
5964
6491
|
try {
|
|
5965
|
-
entries = await
|
|
6492
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
5966
6493
|
} catch {
|
|
5967
6494
|
return;
|
|
5968
6495
|
}
|
|
5969
6496
|
for (const entry of entries) {
|
|
5970
|
-
const full =
|
|
6497
|
+
const full = join10(dir, entry.name);
|
|
5971
6498
|
if (SKIP.test(`/${relative(cwd, full)}`)) continue;
|
|
5972
6499
|
if (entry.isDirectory()) await walk(full, depth + 1);
|
|
5973
6500
|
else if (DOC.test(entry.name) && (!opts.onlyAboutPeople || ABOUT_PEOPLE.test(relative(cwd, full)))) found.push(full);
|
|
@@ -5979,7 +6506,7 @@ async function readDocs(cwd, opts = {}) {
|
|
|
5979
6506
|
for (const path of found.slice(0, DOCS_MAX)) {
|
|
5980
6507
|
try {
|
|
5981
6508
|
if ((await stat(path)).size > 4e5) continue;
|
|
5982
|
-
out.push({ path: relative(cwd, path), text: (await
|
|
6509
|
+
out.push({ path: relative(cwd, path), text: (await readFile6(path, "utf8")).slice(0, DOC_CHARS) });
|
|
5983
6510
|
} catch {
|
|
5984
6511
|
}
|
|
5985
6512
|
}
|
|
@@ -6451,10 +6978,10 @@ function rank(candidates, readings, verdict, faults) {
|
|
|
6451
6978
|
const order = (lands) => DIRECTION_SHAPE_ORDER.indexOf(lands);
|
|
6452
6979
|
return candidates.filter((c) => readings.some((r) => r.lands === c.lands)).map((candidate) => {
|
|
6453
6980
|
const v = verdict.verdicts.find((x) => x.lands === candidate.lands);
|
|
6454
|
-
const
|
|
6455
|
-
const fatal =
|
|
6981
|
+
const failed2 = v?.failed ?? [];
|
|
6982
|
+
const fatal = failed2.filter((t) => MUST_PASS_TESTS.includes(t.test));
|
|
6456
6983
|
const gates = faults.get(candidate.lands) ?? [];
|
|
6457
|
-
const answered = /* @__PURE__ */ new Set([...v?.passed ?? [], ...
|
|
6984
|
+
const answered = /* @__PURE__ */ new Set([...v?.passed ?? [], ...failed2.map((t) => t.test)]);
|
|
6458
6985
|
const unanswered = MUST_PASS_TESTS.filter((t) => !answered.has(t));
|
|
6459
6986
|
const reservations = [
|
|
6460
6987
|
...fatal.map((t) => `${t.test} \u2014 ${t.because}`),
|
|
@@ -6527,17 +7054,17 @@ async function publish2(client, projectId, job, winner, current, because, confid
|
|
|
6527
7054
|
for (const row of DIRECTION_ROWS) {
|
|
6528
7055
|
const content = winner[row].trim();
|
|
6529
7056
|
const base = current.find((r) => r.topic === row && r.key === directionKey(row));
|
|
6530
|
-
const
|
|
7057
|
+
const next = current.find((r) => r.topic === row && r.key === directionKey(row, true));
|
|
6531
7058
|
const sources2 = [
|
|
6532
7059
|
...winner.restsOn.map((ref) => ({ kind: "memory", ref, note: because })),
|
|
6533
7060
|
...runnerUp && runnerUp[row]?.trim() && runnerUp[row].trim() !== content ? [{ kind: CONSIDERED, ref: runnerUp.lands, note: runnerUp[row].trim() }] : []
|
|
6534
7061
|
];
|
|
6535
7062
|
if (base?.provenance === "stated") {
|
|
6536
7063
|
if (base.content.trim() === content) {
|
|
6537
|
-
if (
|
|
7064
|
+
if (next) await retire(client, projectId, next.id, "withdrawn: the crew now agrees with your line");
|
|
6538
7065
|
continue;
|
|
6539
7066
|
}
|
|
6540
|
-
await write2(client, projectId, jobId, row, { key: directionKey(row, true), content, confidence, sources: sources2, supersedes:
|
|
7067
|
+
await write2(client, projectId, jobId, row, { key: directionKey(row, true), content, confidence, sources: sources2, supersedes: next?.id ?? null });
|
|
6541
7068
|
proposed.push(row);
|
|
6542
7069
|
} else {
|
|
6543
7070
|
if (base && base.content.trim() === content) continue;
|
|
@@ -6758,17 +7285,17 @@ async function publish3(client, projectId, job, winner, current, because, runner
|
|
|
6758
7285
|
for (const line of PITCH_LINE_KEYS) {
|
|
6759
7286
|
const content = winner[line].trim();
|
|
6760
7287
|
const base = current.find((r) => r.key === pitchKey(line));
|
|
6761
|
-
const
|
|
7288
|
+
const next = current.find((r) => r.key === pitchKey(line, true));
|
|
6762
7289
|
const sources2 = [
|
|
6763
7290
|
...winner.restsOn.map((ref) => ({ kind: "memory", ref, note: because })),
|
|
6764
7291
|
...runnerUp && !sameLine(runnerUp[line], content) ? [{ kind: CONSIDERED, ref: runnerUp.form, note: runnerUp[line].trim() }] : []
|
|
6765
7292
|
];
|
|
6766
7293
|
if (base?.provenance === "stated") {
|
|
6767
7294
|
if (sameLine(base.content, content)) {
|
|
6768
|
-
if (
|
|
7295
|
+
if (next) await retire2(client, projectId, next.id, "withdrawn: the crew now agrees with your line");
|
|
6769
7296
|
continue;
|
|
6770
7297
|
}
|
|
6771
|
-
await write3(client, projectId, jobId, { key: pitchKey(line, true), title: PITCH_LINE_TITLE[line], content, confidence: winner.confidence, sources: sources2, supersedes:
|
|
7298
|
+
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
7299
|
proposed.push(line);
|
|
6773
7300
|
} else {
|
|
6774
7301
|
if (base && sameLine(base.content, content)) continue;
|
|
@@ -7023,10 +7550,10 @@ async function runMarketJob({ client, job, adapter, spec: spec2, onEvent }) {
|
|
|
7023
7550
|
}
|
|
7024
7551
|
async function writeRow(client, projectId, job, rows2, item) {
|
|
7025
7552
|
const base = rows2.find((r) => r.key === item.key);
|
|
7026
|
-
const
|
|
7553
|
+
const next = rows2.find((r) => r.key === `${item.key}-next`);
|
|
7027
7554
|
if (base && base.content.trim() === item.content) return { kind: "written" };
|
|
7028
7555
|
if (base?.provenance === "stated") {
|
|
7029
|
-
await write4(client, projectId, job.id, { ...item, key: `${item.key}-next`, supersedes:
|
|
7556
|
+
await write4(client, projectId, job.id, { ...item, key: `${item.key}-next`, supersedes: next?.id ?? null });
|
|
7030
7557
|
return { kind: "written" };
|
|
7031
7558
|
}
|
|
7032
7559
|
const held = keepsTheLine(job, base, item.confidence);
|
|
@@ -7781,8 +8308,8 @@ ${body}
|
|
|
7781
8308
|
|
|
7782
8309
|
// ../daemon/src/crew.ts
|
|
7783
8310
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
7784
|
-
import { mkdir as
|
|
7785
|
-
import { join as
|
|
8311
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
8312
|
+
import { join as join11 } from "path";
|
|
7786
8313
|
var READ_ONLY = {
|
|
7787
8314
|
read_repo: /^(Read|Glob|Grep)$/,
|
|
7788
8315
|
read_data: /(^|__)(list_connections|describe_source|query)$/
|
|
@@ -7797,8 +8324,8 @@ async function attempt6({ adapter, spec: spec2, onEvent, skill: skillName, name,
|
|
|
7797
8324
|
if (skill.manifest.capabilities.some((c) => !holds.includes(c))) throw new Error(`The ${name} may not hold tools.`);
|
|
7798
8325
|
const refused = grant.find((c) => !READ_ONLY[c] || !skill.manifest.capabilities.includes(c));
|
|
7799
8326
|
if (refused) throw new Error(`The ${name} may not hold ${refused}.`);
|
|
7800
|
-
const runDir = dir ?
|
|
7801
|
-
if (dir) await
|
|
8327
|
+
const runDir = dir ? join11(spec2.runDir, dir) : spec2.runDir;
|
|
8328
|
+
if (dir) await mkdir3(runDir, { recursive: true, mode: 448 });
|
|
7802
8329
|
let result;
|
|
7803
8330
|
let usage = null;
|
|
7804
8331
|
await onEvent({ type: "text", text: say });
|
|
@@ -8595,7 +9122,7 @@ function critiqueWording(o, v, reworked) {
|
|
|
8595
9122
|
return { problem: problem && problem.length <= PROBLEM_MAX ? problem : o.problem, description: v.description?.trim() || o.description };
|
|
8596
9123
|
}
|
|
8597
9124
|
function shape(o, input) {
|
|
8598
|
-
const
|
|
9125
|
+
const resolve5 = (ids, stance) => ids.map((id) => {
|
|
8599
9126
|
const source = input.cites.get(id.trim().toUpperCase());
|
|
8600
9127
|
if (!source) input.unresolved?.add(id.trim());
|
|
8601
9128
|
return source;
|
|
@@ -8607,7 +9134,7 @@ function shape(o, input) {
|
|
|
8607
9134
|
if (source) quoted.push({ ...source, note: clip3(stripContactDetails(e.quote), 140), stance: "supports" });
|
|
8608
9135
|
}
|
|
8609
9136
|
const quotedRefs = new Set(quoted.map((q) => q.ref));
|
|
8610
|
-
const cited = [...
|
|
9137
|
+
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
9138
|
const all = [...quoted, ...cited];
|
|
8612
9139
|
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
9140
|
if (!sources2.some((s) => s.stance === "supports")) sources2 = [{ kind: "inference", ref: "nanopm", note: null, stance: "supports" }, ...sources2];
|
|
@@ -8897,11 +9424,11 @@ async function runSolutionsJob({ client, job, adapter, spec: spec2, onEvent }) {
|
|
|
8897
9424
|
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
9425
|
}
|
|
8899
9426
|
const recorded = results.filter((r) => "kept" in r);
|
|
8900
|
-
const
|
|
9427
|
+
const failed2 = results.filter((r) => "failed" in r);
|
|
8901
9428
|
const took = `Took ${duration(Date.now() - started)}${timings.length ? `: ${timings.map((t) => `${t.what} ${duration(t.ms)}`).join(", ")}` : ""}.`;
|
|
8902
9429
|
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 &&
|
|
9430
|
+
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"}.`,
|
|
9431
|
+
!deep && failed2.length ? `Not recorded: ${failed2.map((f) => `${f.leaf.ref} (${f.failed})`).join("; ")}.` : "",
|
|
8905
9432
|
skipped.length ? `Skipped: ${skipped.join("; ")}.` : "",
|
|
8906
9433
|
took
|
|
8907
9434
|
].filter(Boolean).join(" ");
|
|
@@ -9071,9 +9598,9 @@ async function flashBranch(ctx, w, n, places) {
|
|
|
9071
9598
|
const why = clip3(error instanceof Error ? error.message : String(error), 300);
|
|
9072
9599
|
for (const leaf of w.leaves) if (!out.some((o) => o.leaf === leaf)) out.push({ leaf, failed: why });
|
|
9073
9600
|
}
|
|
9074
|
-
const
|
|
9075
|
-
if (
|
|
9076
|
-
await ctx.journal("solutions.failed", `\u201C${name}\u201D could not finish: ${
|
|
9601
|
+
const failed2 = out.filter((o) => "failed" in o);
|
|
9602
|
+
if (failed2.length) {
|
|
9603
|
+
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
9604
|
}
|
|
9078
9605
|
return out;
|
|
9079
9606
|
}
|
|
@@ -9438,32 +9965,32 @@ function specOutputSchema(role7) {
|
|
|
9438
9965
|
}
|
|
9439
9966
|
|
|
9440
9967
|
// ../daemon/src/git.ts
|
|
9441
|
-
import { execFile as
|
|
9442
|
-
import { promisify as
|
|
9443
|
-
var
|
|
9444
|
-
var
|
|
9968
|
+
import { execFile as execFile5 } from "child_process";
|
|
9969
|
+
import { promisify as promisify5 } from "util";
|
|
9970
|
+
var execFileAsync5 = promisify5(execFile5);
|
|
9971
|
+
var MAX_BUFFER2 = 4 * 1024 * 1024;
|
|
9445
9972
|
var WIKI_DIR = ".nanopm/wiki/docs";
|
|
9446
|
-
async function
|
|
9447
|
-
const { stdout } = await
|
|
9973
|
+
async function git3(cwd, args) {
|
|
9974
|
+
const { stdout } = await execFileAsync5("git", ["-C", cwd, ...args], { maxBuffer: MAX_BUFFER2, timeout: 2e4 });
|
|
9448
9975
|
return stdout.trim();
|
|
9449
9976
|
}
|
|
9450
9977
|
async function listTrackedFiles(cwd) {
|
|
9451
|
-
const lines = (args) =>
|
|
9978
|
+
const lines = (args) => git3(cwd, args).then((out) => out ? out.split("\n") : [], () => []);
|
|
9452
9979
|
const [files, wiki] = await Promise.all([lines(["ls-files"]), lines(["ls-files", "--others", "--ignored", "--exclude-standard", "--", WIKI_DIR])]);
|
|
9453
9980
|
return { files, wiki };
|
|
9454
9981
|
}
|
|
9455
9982
|
async function headCommit(cwd) {
|
|
9456
|
-
return
|
|
9983
|
+
return git3(cwd, ["rev-parse", "--short", "HEAD"]).then((sha) => sha || null, () => null);
|
|
9457
9984
|
}
|
|
9458
9985
|
async function collectGitDigest(cwd, recent = 30) {
|
|
9459
9986
|
try {
|
|
9460
|
-
await
|
|
9987
|
+
await git3(cwd, ["rev-parse", "--git-dir"]);
|
|
9461
9988
|
} catch {
|
|
9462
9989
|
return null;
|
|
9463
9990
|
}
|
|
9464
9991
|
const safe = async (args) => {
|
|
9465
9992
|
try {
|
|
9466
|
-
return await
|
|
9993
|
+
return await git3(cwd, args);
|
|
9467
9994
|
} catch {
|
|
9468
9995
|
return "";
|
|
9469
9996
|
}
|
|
@@ -9853,13 +10380,13 @@ var told = (r) => ({
|
|
|
9853
10380
|
});
|
|
9854
10381
|
|
|
9855
10382
|
// ../daemon/src/objective-definitions.ts
|
|
9856
|
-
import { execFile as
|
|
10383
|
+
import { execFile as execFile6 } from "child_process";
|
|
9857
10384
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
9858
|
-
import { existsSync as
|
|
9859
|
-
import { mkdir as
|
|
9860
|
-
import { join as
|
|
9861
|
-
import { promisify as
|
|
9862
|
-
var
|
|
10385
|
+
import { existsSync as existsSync4 } from "fs";
|
|
10386
|
+
import { mkdir as mkdir4, readFile as readFile7 } from "fs/promises";
|
|
10387
|
+
import { join as join12 } from "path";
|
|
10388
|
+
import { promisify as promisify6 } from "util";
|
|
10389
|
+
var execFileAsync6 = promisify6(execFile6);
|
|
9863
10390
|
var DEFINITIONS_TIMEOUT_MS = 9e4;
|
|
9864
10391
|
var DEFINITIONS_THINKING_TOKENS = 2e3;
|
|
9865
10392
|
var READ_TOOLS3 = /* @__PURE__ */ new Set(["Read", "Glob", "Grep", "StructuredOutput"]);
|
|
@@ -9868,7 +10395,7 @@ var COUNTING_LINES_MAX = 80;
|
|
|
9868
10395
|
var PER_FILE = 4;
|
|
9869
10396
|
async function countingLines(cwd) {
|
|
9870
10397
|
try {
|
|
9871
|
-
const { stdout } = await
|
|
10398
|
+
const { stdout } = await execFileAsync6("git", ["grep", "-n", "-I", "-i", "-E", COUNTING, "--", ".", ":!*lock*", ":!*.min.*", ":!*.map", ":!*.svg", ":!*.snap"], {
|
|
9872
10399
|
cwd,
|
|
9873
10400
|
maxBuffer: 16 * 1024 * 1024,
|
|
9874
10401
|
timeout: 5e3
|
|
@@ -9893,7 +10420,7 @@ function pickLines(raw) {
|
|
|
9893
10420
|
}
|
|
9894
10421
|
async function killRecorded(runDir) {
|
|
9895
10422
|
try {
|
|
9896
|
-
const pid = Number((await
|
|
10423
|
+
const pid = Number((await readFile7(join12(runDir, "harness.pid"), "utf8")).trim());
|
|
9897
10424
|
if (pid > 1) process.kill(pid, "SIGKILL");
|
|
9898
10425
|
} catch {
|
|
9899
10426
|
}
|
|
@@ -9913,8 +10440,8 @@ async function readDefinitions({ adapter, spec: spec2, publish: publish5, journa
|
|
|
9913
10440
|
if (!hasRepo) return give({}, "No repository to read: the card offers the common definitions.", "ok");
|
|
9914
10441
|
try {
|
|
9915
10442
|
const skill = await loadSkill("define-objective");
|
|
9916
|
-
const runDir =
|
|
9917
|
-
await
|
|
10443
|
+
const runDir = join12(spec2.runDir, "definitions");
|
|
10444
|
+
await mkdir4(runDir, { recursive: true, mode: 448 });
|
|
9918
10445
|
const schema = definitionsOutputSchema();
|
|
9919
10446
|
const [lines, listed] = await Promise.all([countingLines(spec2.cwd), listTrackedFiles(spec2.cwd)]);
|
|
9920
10447
|
const signal = AbortSignal.any([spec2.signal, AbortSignal.timeout(timeoutMs)]);
|
|
@@ -9975,7 +10502,7 @@ ${renderTrackedFiles(listed.files, void 0, listed.wiki)}
|
|
|
9975
10502
|
if (!result) throw new Error("no answer");
|
|
9976
10503
|
const parsed = DefinitionsAnswer.safeParse(JSON.parse(result));
|
|
9977
10504
|
if (!parsed.success) throw new Error("an answer in the wrong shape");
|
|
9978
|
-
const checked = checkProposals(parsed.data, (file) =>
|
|
10505
|
+
const checked = checkProposals(parsed.data, (file) => existsSync4(join12(spec2.cwd, file)));
|
|
9979
10506
|
const definitions2 = lever ? checked[lever] ? { [lever]: checked[lever] } : {} : checked;
|
|
9980
10507
|
const kept = Object.values(definitions2).reduce((n, list) => n + (list?.length ?? 0), 0);
|
|
9981
10508
|
const read6 = Object.values(definitions2).flat().filter((d) => d?.found).length;
|
|
@@ -10248,10 +10775,10 @@ function localDate(timezone, at) {
|
|
|
10248
10775
|
|
|
10249
10776
|
// ../daemon/src/run-job.ts
|
|
10250
10777
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
10251
|
-
import { existsSync as
|
|
10252
|
-
import { mkdir as
|
|
10253
|
-
import { homedir as
|
|
10254
|
-
import { join as
|
|
10778
|
+
import { existsSync as existsSync5 } from "fs";
|
|
10779
|
+
import { mkdir as mkdir5, readFile as readFile8, rm as rm4 } from "fs/promises";
|
|
10780
|
+
import { homedir as homedir4, tmpdir } from "os";
|
|
10781
|
+
import { join as join13, resolve as resolve4 } from "path";
|
|
10255
10782
|
|
|
10256
10783
|
// ../daemon/src/events.ts
|
|
10257
10784
|
var JobEventWriter = class {
|
|
@@ -10331,320 +10858,28 @@ function lighten(value) {
|
|
|
10331
10858
|
return value ?? null;
|
|
10332
10859
|
}
|
|
10333
10860
|
|
|
10334
|
-
// ../daemon/src/
|
|
10335
|
-
import {
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
}
|
|
10346
|
-
|
|
10347
|
-
const slug = ref.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
10348
|
-
if (!slug) throw new RepoError(`"${ref}" has no letters or digits to make a branch name from`);
|
|
10349
|
-
return `nanopm/${slug}`;
|
|
10861
|
+
// ../daemon/src/run-job.ts
|
|
10862
|
+
import { hostname } from "os";
|
|
10863
|
+
|
|
10864
|
+
// ../daemon/src/site-ask.ts
|
|
10865
|
+
async function askAboutSite(opts) {
|
|
10866
|
+
const host = opts.candidate ? new URL(opts.candidate).hostname : null;
|
|
10867
|
+
await opts.events.write("question", {
|
|
10868
|
+
question: host ? `Is ${host} your site?` : "What is your product's website?",
|
|
10869
|
+
holds: false,
|
|
10870
|
+
// The terminal leaves this one to the browser (`run.ts`), and the page draws its own card.
|
|
10871
|
+
about: "site",
|
|
10872
|
+
...host ? { site: host } : {}
|
|
10873
|
+
});
|
|
10350
10874
|
}
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
|
|
10356
|
-
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
git3(cwd, ["rev-parse", "HEAD"]).catch(() => ""),
|
|
10360
|
-
git3(cwd, ["status", "--porcelain"]).catch(() => ""),
|
|
10361
|
-
git3(cwd, ["remote", "get-url", "origin"]).catch(() => "")
|
|
10362
|
-
]);
|
|
10363
|
-
const dirty = status2.split("\n").filter(Boolean).map((l) => l.slice(3));
|
|
10364
|
-
return { branch, head, clean: dirty.length === 0, dirty, hasRemote: Boolean(remoteUrl), remoteUrl: remoteUrl || void 0 };
|
|
10365
|
-
}
|
|
10366
|
-
async function ensureCleanTree(cwd) {
|
|
10367
|
-
const state = await readState(cwd);
|
|
10368
|
-
if (!state.head) {
|
|
10369
|
-
throw new RepoError("this repository has no commits yet; make the first commit, then run nanopm next");
|
|
10370
|
-
}
|
|
10371
|
-
if (!state.clean) {
|
|
10372
|
-
const shown = state.dirty.slice(0, 5).join(", ");
|
|
10373
|
-
const more = state.dirty.length > 5 ? `, and ${state.dirty.length - 5} more` : "";
|
|
10374
|
-
throw new RepoError(
|
|
10375
|
-
`the working tree has uncommitted changes (${shown}${more}). NanoPM commits only its own work, so commit or stash yours first.`
|
|
10376
|
-
);
|
|
10377
|
-
}
|
|
10378
|
-
return state;
|
|
10379
|
-
}
|
|
10380
|
-
async function createBranch(cwd, name) {
|
|
10381
|
-
const existing = await git3(cwd, ["branch", "--list", name]).catch(() => "");
|
|
10382
|
-
if (existing.trim()) {
|
|
10383
|
-
throw new RepoError(`branch ${name} already exists. Delete it or finish what is on it first.`);
|
|
10384
|
-
}
|
|
10385
|
-
await git3(cwd, ["checkout", "-b", name]);
|
|
10386
|
-
}
|
|
10387
|
-
async function commitAll(cwd, message) {
|
|
10388
|
-
await git3(cwd, ["add", "-A"]);
|
|
10389
|
-
const staged = await git3(cwd, ["diff", "--cached", "--name-only"]).catch(() => "");
|
|
10390
|
-
if (!staged) return null;
|
|
10391
|
-
await git3(cwd, ["commit", "-m", message, "-m", "Written by NanoPM. Review before merging."]);
|
|
10392
|
-
const sha = await git3(cwd, ["rev-parse", "--short", "HEAD"]);
|
|
10393
|
-
return { sha, ...await diffStat(cwd) };
|
|
10394
|
-
}
|
|
10395
|
-
async function diffStat(cwd) {
|
|
10396
|
-
const numstat = await git3(cwd, ["show", "--numstat", "--format=", "HEAD"]).catch(() => "");
|
|
10397
|
-
let files = 0;
|
|
10398
|
-
let insertions = 0;
|
|
10399
|
-
let deletions = 0;
|
|
10400
|
-
for (const line of numstat.split("\n").filter(Boolean)) {
|
|
10401
|
-
const [add2 = "", del = ""] = line.split(" ");
|
|
10402
|
-
files += 1;
|
|
10403
|
-
insertions += Number(add2) || 0;
|
|
10404
|
-
deletions += Number(del) || 0;
|
|
10405
|
-
}
|
|
10406
|
-
return { files, insertions, deletions };
|
|
10407
|
-
}
|
|
10408
|
-
async function pushBranch(cwd, name) {
|
|
10409
|
-
const state = await readState(cwd);
|
|
10410
|
-
if (!state.hasRemote) return { pushed: false, reason: "this repository has no origin remote" };
|
|
10411
|
-
try {
|
|
10412
|
-
const { stderr } = await execFileAsync6("git", ["-C", cwd, "push", "--set-upstream", "origin", name], {
|
|
10413
|
-
maxBuffer: MAX_BUFFER2,
|
|
10414
|
-
timeout: 12e4
|
|
10415
|
-
});
|
|
10416
|
-
return { pushed: true, reviewUrl: reviewUrlFrom(stderr) };
|
|
10417
|
-
} catch (err) {
|
|
10418
|
-
const text2 = errorMessage(err);
|
|
10419
|
-
return { pushed: false, reason: text2.split("\n").slice(-3).join(" ").trim() || "git push failed" };
|
|
10420
|
-
}
|
|
10421
|
-
}
|
|
10422
|
-
function reviewUrlFrom(pushOutput) {
|
|
10423
|
-
const match = /https?:\/\/\S+/.exec(pushOutput);
|
|
10424
|
-
return match?.[0]?.replace(/[).,]+$/, "");
|
|
10425
|
-
}
|
|
10426
|
-
async function checkout(cwd, ref) {
|
|
10427
|
-
await git3(cwd, ["checkout", ref]);
|
|
10428
|
-
}
|
|
10429
|
-
async function deleteBranch(cwd, name) {
|
|
10430
|
-
await git3(cwd, ["branch", "-D", name]).catch(() => "");
|
|
10431
|
-
}
|
|
10432
|
-
async function changedFiles(cwd) {
|
|
10433
|
-
const status2 = await git3(cwd, ["status", "--porcelain"]).catch(() => "");
|
|
10434
|
-
return status2.split("\n").filter(Boolean).map((line) => line.slice(3).trim()).map((path) => path.includes(" -> ") ? path.split(" -> ")[1].trim() : path).map((path) => path.replace(/^"|"$/g, ""));
|
|
10435
|
-
}
|
|
10436
|
-
|
|
10437
|
-
// ../daemon/src/next-run.ts
|
|
10438
|
-
async function loadAcceptedSolution(client, projectId, solutionId) {
|
|
10439
|
-
const { data, error } = await client.from("solutions").select("id, ref, title, reasoning, status, assumption, eval, sources").eq("id", solutionId).eq("project_id", projectId).is("superseded_by", null).maybeSingle();
|
|
10440
|
-
if (error) throw new Error(`could not read the solution: ${error.message}`);
|
|
10441
|
-
if (!data) throw new Error("that solution is not on this project any more");
|
|
10442
|
-
if (data.status !== "accepted" && data.status !== "building") {
|
|
10443
|
-
throw new Error(`${data.ref} is ${data.status}. Only a solution you accepted can be built.`);
|
|
10444
|
-
}
|
|
10445
|
-
return { ...data, sources: data.sources ?? [] };
|
|
10446
|
-
}
|
|
10447
|
-
async function startSolution(client, projectId, ref, jobId) {
|
|
10448
|
-
const { error } = await client.rpc("start_solution", { p_project_id: projectId, p_ref: ref, p_job_id: jobId });
|
|
10449
|
-
if (error && !/only an accepted solution/.test(error.message)) throw new Error(pgMessage(error));
|
|
10450
|
-
}
|
|
10451
|
-
async function unstartSolution(client, projectId, ref) {
|
|
10452
|
-
await client.rpc("unstart_solution", { p_project_id: projectId, p_ref: ref });
|
|
10453
|
-
}
|
|
10454
|
-
function renderSolution(solution) {
|
|
10455
|
-
const lines = [`${solution.ref} ${solution.title}`, "", `Why: ${solution.reasoning}`];
|
|
10456
|
-
if (solution.assumption) lines.push(`Bets on: ${solution.assumption}`);
|
|
10457
|
-
if (solution.eval) lines.push(`We'll know: ${solution.eval}`);
|
|
10458
|
-
if (solution.sources.length) {
|
|
10459
|
-
lines.push(`Rests on: ${solution.sources.map((s) => `${s.kind}:${s.ref}`).join(", ")}`);
|
|
10460
|
-
}
|
|
10461
|
-
return lines.join("\n");
|
|
10462
|
-
}
|
|
10463
|
-
async function openRepoSession(cwd, ref) {
|
|
10464
|
-
const state = await ensureCleanTree(cwd);
|
|
10465
|
-
if (!state.branch) {
|
|
10466
|
-
throw new RepoError("this repository has a detached HEAD; check out a branch first");
|
|
10467
|
-
}
|
|
10468
|
-
const branch = branchName(ref);
|
|
10469
|
-
await createBranch(cwd, branch);
|
|
10470
|
-
return { branch, baseBranch: state.branch };
|
|
10471
|
-
}
|
|
10472
|
-
async function closeRepoSession(cwd, session2, item) {
|
|
10473
|
-
return closeBranch(cwd, session2, `${item.ref}: ${item.title}`, { commitErrorThrows: true });
|
|
10474
|
-
}
|
|
10475
|
-
async function abandonRepoSession(cwd, session2, item) {
|
|
10476
|
-
return closeBranch(cwd, session2, item ? `${item.ref}: ${item.title}` : "WIP: nanopm did not finish", { commitErrorThrows: false });
|
|
10477
|
-
}
|
|
10478
|
-
async function closeBranch(cwd, session2, message, { commitErrorThrows }) {
|
|
10479
|
-
let commit;
|
|
10480
|
-
try {
|
|
10481
|
-
commit = await commitAll(cwd, message);
|
|
10482
|
-
} catch (err) {
|
|
10483
|
-
await checkout(cwd, session2.baseBranch).catch(() => void 0);
|
|
10484
|
-
if (commitErrorThrows) throw err;
|
|
10485
|
-
commit = null;
|
|
10486
|
-
}
|
|
10487
|
-
if (!commit) {
|
|
10488
|
-
await checkout(cwd, session2.baseBranch).catch(() => void 0);
|
|
10489
|
-
await deleteBranch(cwd, session2.branch);
|
|
10490
|
-
return { branch: session2.branch, pushed: false, note: "nothing was changed, so no branch was kept" };
|
|
10491
|
-
}
|
|
10492
|
-
const push = await pushBranch(cwd, session2.branch);
|
|
10493
|
-
await checkout(cwd, session2.baseBranch).catch(() => void 0);
|
|
10494
|
-
return {
|
|
10495
|
-
branch: session2.branch,
|
|
10496
|
-
commit,
|
|
10497
|
-
pushed: push.pushed,
|
|
10498
|
-
reviewUrl: push.reviewUrl,
|
|
10499
|
-
note: push.pushed ? void 0 : `not pushed: ${push.reason}`
|
|
10500
|
-
};
|
|
10501
|
-
}
|
|
10502
|
-
async function finishSolution(client, job, solution, result) {
|
|
10503
|
-
const sources2 = [{ kind: "branch", ref: result.branch }];
|
|
10504
|
-
if (result.commit?.sha) sources2.push({ kind: "git", ref: result.commit.sha });
|
|
10505
|
-
if (result.reviewUrl) sources2.push({ kind: "url", ref: result.reviewUrl });
|
|
10506
|
-
sources2.push({ kind: "job", ref: job.id });
|
|
10507
|
-
const { error } = await client.rpc("finish_solution", {
|
|
10508
|
-
p_project_id: job.project_id,
|
|
10509
|
-
p_ref: solution.ref,
|
|
10510
|
-
p_job_id: job.id,
|
|
10511
|
-
p_sources: sources2
|
|
10512
|
-
});
|
|
10513
|
-
if (error) throw new Error(`the work is committed on ${result.branch}, but recording it failed: ${error.message}`);
|
|
10514
|
-
}
|
|
10515
|
-
function renderRepoResult(result) {
|
|
10516
|
-
if (!result.commit) return result.note ?? "nothing changed";
|
|
10517
|
-
const { files, insertions, deletions } = result.commit;
|
|
10518
|
-
const stat2 = `${files} file${files === 1 ? "" : "s"} changed, ${insertions} insertion${insertions === 1 ? "" : "s"}, ${deletions} deletion${deletions === 1 ? "" : "s"}`;
|
|
10519
|
-
const lines = [`${result.branch} \xB7 ${result.commit.sha} \xB7 ${stat2}`];
|
|
10520
|
-
if (result.reviewUrl) lines.push(result.reviewUrl);
|
|
10521
|
-
else if (result.note) lines.push(result.note);
|
|
10522
|
-
return lines.join("\n");
|
|
10523
|
-
}
|
|
10524
|
-
|
|
10525
|
-
// ../daemon/src/run-job.ts
|
|
10526
|
-
import { hostname } from "os";
|
|
10527
|
-
|
|
10528
|
-
// ../daemon/src/site-ask.ts
|
|
10529
|
-
async function askAboutSite(opts) {
|
|
10530
|
-
const host = opts.candidate ? new URL(opts.candidate).hostname : null;
|
|
10531
|
-
await opts.events.write("question", {
|
|
10532
|
-
question: host ? `Is ${host} your site?` : "What is your product's website?",
|
|
10533
|
-
holds: false,
|
|
10534
|
-
// The terminal leaves this one to the browser (`run.ts`), and the page draws its own card.
|
|
10535
|
-
about: "site",
|
|
10536
|
-
...host ? { site: host } : {}
|
|
10537
|
-
});
|
|
10538
|
-
}
|
|
10539
|
-
|
|
10540
|
-
// ../daemon/src/verify.ts
|
|
10541
|
-
import { spawn as spawn3 } from "child_process";
|
|
10542
|
-
var VERIFY_TIMEOUT_MS = 10 * 6e4;
|
|
10543
|
-
var OUTPUT_TAIL = 4e3;
|
|
10544
|
-
var DEFINES_THE_COMMAND = [
|
|
10545
|
-
/(^|\/)package\.json$/,
|
|
10546
|
-
/(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb)$/,
|
|
10547
|
-
/(^|\/)(Makefile|justfile|Justfile|Taskfile\.ya?ml)$/,
|
|
10548
|
-
/(^|\/)\.husky\//,
|
|
10549
|
-
/(^|\/)\.github\//,
|
|
10550
|
-
/(^|\/)(pyproject\.toml|uv\.lock|Cargo\.toml|Cargo\.lock|Gemfile|Gemfile\.lock)$/
|
|
10551
|
-
];
|
|
10552
|
-
function pathsNamedIn(command) {
|
|
10553
|
-
return command.split(/[\s;&|()<>]+/).filter((t) => /[/.]/.test(t) && !/^-/.test(t) && t.length > 2).map((t) => t.replace(/^\.\//, "").replace(/^["']|["']$/g, ""));
|
|
10554
|
-
}
|
|
10555
|
-
function guardVerify(changed, command) {
|
|
10556
|
-
for (const file of changed) {
|
|
10557
|
-
const pattern = DEFINES_THE_COMMAND.find((p) => p.test(file));
|
|
10558
|
-
if (pattern) {
|
|
10559
|
-
return `the change touched ${file}, which decides what the verify command does, so the command was not run`;
|
|
10560
|
-
}
|
|
10561
|
-
}
|
|
10562
|
-
const named = pathsNamedIn(command);
|
|
10563
|
-
for (const file of changed) {
|
|
10564
|
-
if (named.some((n) => file === n || file.endsWith(`/${n}`) || n.endsWith(file))) {
|
|
10565
|
-
return `the change touched ${file}, which the verify command runs, so the command was not run`;
|
|
10566
|
-
}
|
|
10567
|
-
}
|
|
10568
|
-
return null;
|
|
10569
|
-
}
|
|
10570
|
-
async function verify(opts) {
|
|
10571
|
-
const skipped = guardVerify(opts.changed, opts.command);
|
|
10572
|
-
if (skipped) return { ran: false, passed: false, skipped, output: "", durationMs: 0, timedOut: false };
|
|
10573
|
-
return runCommand(opts);
|
|
10574
|
-
}
|
|
10575
|
-
async function runCommand(opts) {
|
|
10576
|
-
const timeoutMs = opts.timeoutMs ?? VERIFY_TIMEOUT_MS;
|
|
10577
|
-
const startedAt = Date.now();
|
|
10578
|
-
const child = spawn3("sh", ["-c", opts.command], {
|
|
10579
|
-
cwd: opts.cwd,
|
|
10580
|
-
env: opts.env,
|
|
10581
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
10582
|
-
});
|
|
10583
|
-
let output = "";
|
|
10584
|
-
const collect6 = (chunk) => {
|
|
10585
|
-
output += chunk.toString();
|
|
10586
|
-
if (output.length > OUTPUT_TAIL * 4) output = output.slice(-OUTPUT_TAIL * 2);
|
|
10587
|
-
};
|
|
10588
|
-
child.stdout.on("data", collect6);
|
|
10589
|
-
child.stderr.on("data", collect6);
|
|
10590
|
-
let timedOut = false;
|
|
10591
|
-
const timer = setTimeout(() => {
|
|
10592
|
-
timedOut = true;
|
|
10593
|
-
child.kill("SIGTERM");
|
|
10594
|
-
setTimeout(() => child.kill("SIGKILL"), 5e3).unref();
|
|
10595
|
-
}, timeoutMs);
|
|
10596
|
-
const onAbort = () => child.kill("SIGTERM");
|
|
10597
|
-
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
10598
|
-
const exitCode = await new Promise((resolve6) => {
|
|
10599
|
-
child.on("close", (code) => resolve6(code));
|
|
10600
|
-
child.on("error", () => resolve6(127));
|
|
10601
|
-
});
|
|
10602
|
-
clearTimeout(timer);
|
|
10603
|
-
opts.signal?.removeEventListener("abort", onAbort);
|
|
10604
|
-
return {
|
|
10605
|
-
ran: true,
|
|
10606
|
-
passed: exitCode === 0 && !timedOut,
|
|
10607
|
-
output: tail(output, OUTPUT_TAIL),
|
|
10608
|
-
exitCode: exitCode ?? void 0,
|
|
10609
|
-
durationMs: Date.now() - startedAt,
|
|
10610
|
-
timedOut
|
|
10611
|
-
};
|
|
10612
|
-
}
|
|
10613
|
-
function tail(text2, max) {
|
|
10614
|
-
const trimmed = text2.trimEnd();
|
|
10615
|
-
if (trimmed.length <= max) return trimmed;
|
|
10616
|
-
return `\u2026 ${trimmed.slice(-max)}`;
|
|
10617
|
-
}
|
|
10618
|
-
function renderVerifyResult(result, command) {
|
|
10619
|
-
if (!result.ran) return result.skipped ?? "no verify command is set for this project";
|
|
10620
|
-
const secs = (result.durationMs / 1e3).toFixed(0);
|
|
10621
|
-
if (result.timedOut) return `the check NanoPM could not run itself, \`${command}\`, did not finish within ${secs}s and was stopped`;
|
|
10622
|
-
return result.passed ? `the check NanoPM could not run itself, \`${command}\`, passed in ${secs}s` : `the check NanoPM could not run itself, \`${command}\`, failed (exit ${result.exitCode}) after ${secs}s`;
|
|
10623
|
-
}
|
|
10624
|
-
function retryPrompt(result, command) {
|
|
10625
|
-
return [
|
|
10626
|
-
`Your change does not pass this project's own check.`,
|
|
10627
|
-
``,
|
|
10628
|
-
`The daemon ran \`${command}\` in the repository root and it failed with exit ${result.exitCode}.`,
|
|
10629
|
-
`You did not run this and cannot run it; this output is the only evidence you get.`,
|
|
10630
|
-
``,
|
|
10631
|
-
"```",
|
|
10632
|
-
result.output || "(the command produced no output)",
|
|
10633
|
-
"```",
|
|
10634
|
-
``,
|
|
10635
|
-
`Fix what this reports, in the files you already changed where possible. Do not disable`,
|
|
10636
|
-
`the check, do not edit what the command runs, and do not change unrelated code to make`,
|
|
10637
|
-
`it pass. If the failure is not caused by your change, say so plainly and stop.`
|
|
10638
|
-
].join("\n");
|
|
10639
|
-
}
|
|
10640
|
-
|
|
10641
|
-
// ../daemon/src/run-job.ts
|
|
10642
|
-
var QUICK_RUNS = { reword: runRewordJob, talk: runTalkJob };
|
|
10643
|
-
async function runJob(job, ctx) {
|
|
10644
|
-
const startedAt = Date.now();
|
|
10645
|
-
let events;
|
|
10646
|
-
events = new JobEventWriter(job.id, supabaseEventInsert(ctx.client, (seq) => events.bump(seq)));
|
|
10647
|
-
let outcome;
|
|
10875
|
+
|
|
10876
|
+
// ../daemon/src/run-job.ts
|
|
10877
|
+
var QUICK_RUNS = { reword: runRewordJob, talk: runTalkJob };
|
|
10878
|
+
async function runJob(job, ctx) {
|
|
10879
|
+
const startedAt = Date.now();
|
|
10880
|
+
let events;
|
|
10881
|
+
events = new JobEventWriter(job.id, supabaseEventInsert(ctx.client, (seq) => events.bump(seq)));
|
|
10882
|
+
let outcome;
|
|
10648
10883
|
try {
|
|
10649
10884
|
await events.status("running", { daemon_id: ctx.daemonId, hostname: hostname() });
|
|
10650
10885
|
const skill = await loadSkill(job.skill);
|
|
@@ -10657,11 +10892,13 @@ async function runJob(job, ctx) {
|
|
|
10657
10892
|
};
|
|
10658
10893
|
}
|
|
10659
10894
|
const durationMs = Date.now() - startedAt;
|
|
10895
|
+
const over = outcome.status !== "waiting";
|
|
10660
10896
|
await events.status(outcome.status, outcome.error ? { error: outcome.error } : crewRun(job) && outcome.result ? { text: outcome.result } : {});
|
|
10661
10897
|
await withRetry(
|
|
10662
10898
|
() => ctx.client.from("jobs").update({
|
|
10663
10899
|
status: outcome.status,
|
|
10664
|
-
|
|
10900
|
+
// A parked job is not over: it waits, with no heartbeat, for the founder or the limit.
|
|
10901
|
+
finished_at: over ? (/* @__PURE__ */ new Date()).toISOString() : null,
|
|
10665
10902
|
// A mapping run's root row belongs to SQL: checkpoints, receipt and text land there
|
|
10666
10903
|
// with the publication. A resumed job's own row is free to carry the text.
|
|
10667
10904
|
...job.skill === "problems" && !job.resume_of ? {} : { result: outcome.result !== void 0 ? { text: outcome.result, ...outcome.data } : null },
|
|
@@ -10674,7 +10911,7 @@ async function runJob(job, ctx) {
|
|
|
10674
10911
|
ctx,
|
|
10675
10912
|
job,
|
|
10676
10913
|
"job.end",
|
|
10677
|
-
outcome.status === "succeeded" ? `finished ${job.skill}` : outcome.status === "cancelled" ? `${job.skill} cancelled by the user` : `${job.skill} failed: ${outcome.error?.message}`,
|
|
10914
|
+
outcome.status === "succeeded" ? `finished ${job.skill}` : outcome.status === "waiting" ? `${job.skill} waits, with nothing running` : outcome.status === "cancelled" ? `${job.skill} cancelled by the user` : `${job.skill} failed: ${outcome.error?.message}`,
|
|
10678
10915
|
{
|
|
10679
10916
|
usage: {
|
|
10680
10917
|
tokens: usageTokens(outcome.usage),
|
|
@@ -10683,15 +10920,15 @@ async function runJob(job, ctx) {
|
|
|
10683
10920
|
...outcome.usage?.contextPeakTokens ? { context_peak: outcome.usage.contextPeakTokens } : {},
|
|
10684
10921
|
...outcome.usage?.reads ? { files_read: outcome.usage.reads.files, calls: outcome.usage.reads.calls } : {}
|
|
10685
10922
|
},
|
|
10686
|
-
outcome: outcome.status === "succeeded" ? "ok" : "error"
|
|
10923
|
+
outcome: outcome.status === "succeeded" || !over ? "ok" : "error"
|
|
10687
10924
|
}
|
|
10688
10925
|
);
|
|
10689
10926
|
return outcome;
|
|
10690
10927
|
}
|
|
10691
10928
|
async function execute(job, skill, ctx, events) {
|
|
10692
|
-
const runDir =
|
|
10693
|
-
await
|
|
10694
|
-
const resumeSessionId = job.resume_of ? await sessionOf(ctx.client, job.resume_of) : void 0;
|
|
10929
|
+
const runDir = join13(nanopmHome(), "run", job.id);
|
|
10930
|
+
await mkdir5(runDir, { recursive: true, mode: 448 });
|
|
10931
|
+
const resumeSessionId = job.resume_of ? await sessionOf(ctx.client, job.resume_of) : job.harness_session_id ?? void 0;
|
|
10695
10932
|
const sessionId = resumeSessionId ?? randomUUID12();
|
|
10696
10933
|
await ctx.client.from("jobs").update({ harness_session_id: sessionId }).eq("id", job.id);
|
|
10697
10934
|
const adapter = ctx.adapter ?? new ClaudeCodeAdapter();
|
|
@@ -10734,14 +10971,14 @@ async function execute(job, skill, ctx, events) {
|
|
|
10734
10971
|
clearTimeout(timer);
|
|
10735
10972
|
clearInterval(heartbeat2);
|
|
10736
10973
|
clearInterval(cancelPoll);
|
|
10737
|
-
await
|
|
10974
|
+
await rm4(runDir, { recursive: true, force: true });
|
|
10738
10975
|
}
|
|
10739
10976
|
}
|
|
10740
10977
|
const quickRun2 = QUICK_RUNS[job.skill];
|
|
10741
10978
|
if (quickRun2) {
|
|
10742
10979
|
const cwd2 = scratchDir(job.id);
|
|
10743
10980
|
try {
|
|
10744
|
-
await
|
|
10981
|
+
await mkdir5(cwd2, { recursive: true, mode: 448 });
|
|
10745
10982
|
const caps = skill.manifest.capabilities;
|
|
10746
10983
|
return await quickRun2({
|
|
10747
10984
|
client: ctx.client,
|
|
@@ -10761,13 +10998,22 @@ async function execute(job, skill, ctx, events) {
|
|
|
10761
10998
|
clearTimeout(timer);
|
|
10762
10999
|
clearInterval(heartbeat2);
|
|
10763
11000
|
clearInterval(cancelPoll);
|
|
10764
|
-
await
|
|
10765
|
-
await
|
|
11001
|
+
await rm4(runDir, { recursive: true, force: true });
|
|
11002
|
+
await rm4(cwd2, { recursive: true, force: true });
|
|
10766
11003
|
}
|
|
10767
11004
|
}
|
|
10768
|
-
const capabilities = capCapabilities(skill.manifest.capabilities, autonomy2);
|
|
11005
|
+
const capabilities = capCapabilities(skill.manifest.capabilities, autonomy2, { asked: skill.manifest.asked && job.requested_by === ctx.userId });
|
|
10769
11006
|
const harnessWeb = await claudeSearches(searchProvider);
|
|
10770
|
-
|
|
11007
|
+
let cwd = await resolveCwd(ctx, job, params, scratchDir(job.id), capabilities.includes("write_repo"));
|
|
11008
|
+
const prepared = job.skill === BUILD_SKILL ? await prepareBuild(ctx.client, job, cwd, adapter) : null;
|
|
11009
|
+
if (prepared && !prepared.ok) {
|
|
11010
|
+
clearTimeout(timer);
|
|
11011
|
+
clearInterval(heartbeat2);
|
|
11012
|
+
clearInterval(cancelPoll);
|
|
11013
|
+
return prepared.outcome;
|
|
11014
|
+
}
|
|
11015
|
+
const build2 = prepared?.build;
|
|
11016
|
+
if (build2) cwd = build2.dir;
|
|
10771
11017
|
const definitionsRead = job.skill === "onboard" && !job.resume_of ? readDefinitions({
|
|
10772
11018
|
adapter,
|
|
10773
11019
|
spec: { cwd, runDir, systemPrompt: "", prompt: "", capabilities: [], mcp: [], maxTurns: 1, sessionId, env: harnessEnv(timeoutMs), signal: abort.signal },
|
|
@@ -10791,29 +11037,6 @@ async function execute(job, skill, ctx, events) {
|
|
|
10791
11037
|
const read6 = site.pages.length ? `read ${site.pages.map((p) => new URL(p.url).pathname).join(", ")} on ${site.host}` : `could not read ${site.host}`;
|
|
10792
11038
|
await logActivity(ctx, job, "scan.site", site.missed.length ? `${read6} (${site.missed.length} not read)` : read6);
|
|
10793
11039
|
}
|
|
10794
|
-
let item;
|
|
10795
|
-
let session2;
|
|
10796
|
-
if (capabilities.includes("write_repo")) {
|
|
10797
|
-
const solutionId = params.solution_id;
|
|
10798
|
-
if (typeof solutionId !== "string" || !solutionId) {
|
|
10799
|
-
clearTimeout(timer);
|
|
10800
|
-
clearInterval(heartbeat2);
|
|
10801
|
-
clearInterval(cancelPoll);
|
|
10802
|
-
return { status: "failed", error: { code: "harness_error", message: "this job names no solution to build" } };
|
|
10803
|
-
}
|
|
10804
|
-
try {
|
|
10805
|
-
item = await loadAcceptedSolution(ctx.client, job.project_id, solutionId);
|
|
10806
|
-
session2 = await openRepoSession(cwd, item.ref);
|
|
10807
|
-
await startSolution(ctx.client, job.project_id, item.ref, job.id);
|
|
10808
|
-
params.item = renderSolution(item);
|
|
10809
|
-
await logActivity(ctx, job, "repo.branch", `working on ${session2.branch}`);
|
|
10810
|
-
} catch (err) {
|
|
10811
|
-
clearTimeout(timer);
|
|
10812
|
-
clearInterval(heartbeat2);
|
|
10813
|
-
clearInterval(cancelPoll);
|
|
10814
|
-
return { status: "failed", error: { code: "harness_error", message: errorMessage(err) } };
|
|
10815
|
-
}
|
|
10816
|
-
}
|
|
10817
11040
|
const spec2 = {
|
|
10818
11041
|
cwd,
|
|
10819
11042
|
runDir,
|
|
@@ -10833,20 +11056,22 @@ async function execute(job, skill, ctx, events) {
|
|
|
10833
11056
|
].filter(Boolean).join("\n\n"),
|
|
10834
11057
|
capabilities,
|
|
10835
11058
|
harnessWeb,
|
|
11059
|
+
network: build2?.network,
|
|
11060
|
+
answerFile: build2 ? buildAnswerFile(job.id) : void 0,
|
|
10836
11061
|
withoutTools: skill.manifest.withoutTools,
|
|
10837
11062
|
// The workspace reaches the memory server only for a job that changes files; that is
|
|
10838
|
-
// what lets
|
|
11063
|
+
// what lets the build's tools refuse everywhere else.
|
|
10839
11064
|
mcp: ctx.mcpServers?.(job, timeoutMs, capabilities.includes("write_repo") ? cwd : void 0) ?? [],
|
|
10840
11065
|
model: skill.manifest.model,
|
|
10841
11066
|
maxTurns: skill.manifest.maxTurns,
|
|
10842
11067
|
sessionId,
|
|
10843
11068
|
resumeSessionId,
|
|
10844
|
-
|
|
11069
|
+
// Print mode kills a background task when the turn ends; a build waits on its commands (§9).
|
|
11070
|
+
env: build2 ? { ...harnessEnv(timeoutMs), CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" } : harnessEnv(timeoutMs),
|
|
10845
11071
|
signal: abort.signal
|
|
10846
11072
|
};
|
|
10847
11073
|
const verifyCommand2 = project?.preferences?.verify_command?.trim() || null;
|
|
10848
11074
|
let outcome;
|
|
10849
|
-
let verified;
|
|
10850
11075
|
let webCalls = 0;
|
|
10851
11076
|
const onHarnessEvent = async (event) => {
|
|
10852
11077
|
await events.harness(event);
|
|
@@ -10864,6 +11089,24 @@ async function execute(job, skill, ctx, events) {
|
|
|
10864
11089
|
return result;
|
|
10865
11090
|
};
|
|
10866
11091
|
try {
|
|
11092
|
+
if (build2) {
|
|
11093
|
+
try {
|
|
11094
|
+
return await runBuildJob({
|
|
11095
|
+
client: ctx.client,
|
|
11096
|
+
job,
|
|
11097
|
+
adapter,
|
|
11098
|
+
build: build2,
|
|
11099
|
+
spec: spec2,
|
|
11100
|
+
verifyCommand: verifyCommand2,
|
|
11101
|
+
events,
|
|
11102
|
+
onEvent: onHarnessEvent,
|
|
11103
|
+
log: (step, summary, result) => logActivity(ctx, job, step, summary, result ? { outcome: result } : {})
|
|
11104
|
+
});
|
|
11105
|
+
} catch (error) {
|
|
11106
|
+
if (cancelled) return { status: "cancelled" };
|
|
11107
|
+
return { status: "failed", error: { code: timedOut ? "timeout" : "harness_error", message: errorMessage(error) } };
|
|
11108
|
+
}
|
|
11109
|
+
}
|
|
10867
11110
|
if (job.skill === "personas") {
|
|
10868
11111
|
try {
|
|
10869
11112
|
return await runPersonaJob({ client: ctx.client, job, adapter, spec: spec2, onEvent: onHarnessEvent });
|
|
@@ -10944,83 +11187,23 @@ async function execute(job, skill, ctx, events) {
|
|
|
10944
11187
|
return { status: "failed", error: { code: timedOut ? "timeout" : "harness_error", message: errorMessage(error) } };
|
|
10945
11188
|
}
|
|
10946
11189
|
}
|
|
10947
|
-
for (
|
|
10948
|
-
|
|
10949
|
-
|
|
10950
|
-
|
|
10951
|
-
|
|
10952
|
-
|
|
10953
|
-
if (event.type === "finished") {
|
|
10954
|
-
attemptOutcome = { status: "succeeded", result: event.result, usage: event.usage };
|
|
10955
|
-
} else if (event.type === "failed") {
|
|
10956
|
-
attemptOutcome = { status: "failed", error: { code: event.code, message: event.message } };
|
|
10957
|
-
}
|
|
11190
|
+
for await (const event of adapter.run(spec2)) {
|
|
11191
|
+
await onHarnessEvent(event);
|
|
11192
|
+
if (event.type === "finished") {
|
|
11193
|
+
outcome = { status: "succeeded", result: event.result, usage: event.usage };
|
|
11194
|
+
} else if (event.type === "failed") {
|
|
11195
|
+
outcome = { status: "failed", error: { code: event.code, message: event.message } };
|
|
10958
11196
|
}
|
|
10959
|
-
if (attemptOutcome) await noteIfItNeverSearched(attemptOutcome, capabilities.includes("read_web"));
|
|
10960
|
-
outcome = mergeAttempts(outcome, attemptOutcome);
|
|
10961
|
-
if (attemptOutcome?.status !== "succeeded" || !session2 || !verifyCommand2) break;
|
|
10962
|
-
verified = await verify({
|
|
10963
|
-
cwd,
|
|
10964
|
-
command: verifyCommand2,
|
|
10965
|
-
env: harnessEnv(timeoutMs),
|
|
10966
|
-
changed: await changedFiles(cwd),
|
|
10967
|
-
signal: abort.signal
|
|
10968
|
-
});
|
|
10969
|
-
await logActivity(ctx, job, "repo.verify", renderVerifyResult(verified, verifyCommand2), {
|
|
10970
|
-
outcome: verified.passed ? "ok" : "error",
|
|
10971
|
-
usage: { duration_ms: verified.durationMs }
|
|
10972
|
-
});
|
|
10973
|
-
await events.write("text", { text: renderVerifyResult(verified, verifyCommand2) });
|
|
10974
|
-
if (verified.passed || !verified.ran || attempt7 === 2) break;
|
|
10975
11197
|
}
|
|
11198
|
+
if (outcome) await noteIfItNeverSearched(outcome, capabilities.includes("read_web"));
|
|
10976
11199
|
} finally {
|
|
10977
11200
|
clearTimeout(timer);
|
|
10978
11201
|
clearInterval(heartbeat2);
|
|
10979
11202
|
clearInterval(cancelPoll);
|
|
10980
11203
|
await definitionsRead;
|
|
10981
|
-
await
|
|
10982
|
-
}
|
|
10983
|
-
if (outcome?.status === "succeeded" && verified && verified.ran && !verified.passed) {
|
|
10984
|
-
outcome = {
|
|
10985
|
-
...outcome,
|
|
10986
|
-
status: "failed",
|
|
10987
|
-
error: {
|
|
10988
|
-
code: "harness_error",
|
|
10989
|
-
message: `${renderVerifyResult(verified, verifyCommand2)}. The work is on the branch; the solution stays accepted.`
|
|
10990
|
-
}
|
|
10991
|
-
};
|
|
10992
|
-
}
|
|
10993
|
-
if (outcome?.status === "succeeded") {
|
|
10994
|
-
if (!session2 || !item) return outcome;
|
|
10995
|
-
try {
|
|
10996
|
-
const repo = await closeRepoSession(cwd, session2, item);
|
|
10997
|
-
await logActivity(ctx, job, "repo.commit", renderRepoResult(repo).split("\n")[0]);
|
|
10998
|
-
if (!repo.commit) {
|
|
10999
|
-
await unstartSolution(ctx.client, job.project_id, item.ref).catch(() => void 0);
|
|
11000
|
-
return {
|
|
11001
|
-
...outcome,
|
|
11002
|
-
status: "failed",
|
|
11003
|
-
error: { code: "harness_error", message: "the skill finished without changing anything" }
|
|
11004
|
-
};
|
|
11005
|
-
}
|
|
11006
|
-
await finishSolution(ctx.client, job, item, repo);
|
|
11007
|
-
return { ...outcome, result: [outcome.result, "", renderRepoResult(repo)].filter((l) => l !== void 0).join("\n") };
|
|
11008
|
-
} catch (err) {
|
|
11009
|
-
return {
|
|
11010
|
-
...outcome,
|
|
11011
|
-
status: "failed",
|
|
11012
|
-
error: { code: "harness_error", message: errorMessage(err) }
|
|
11013
|
-
};
|
|
11014
|
-
}
|
|
11015
|
-
}
|
|
11016
|
-
if (session2) {
|
|
11017
|
-
const kept = await abandonRepoSession(cwd, session2, verified?.ran && !verified.passed ? item : void 0).catch(() => void 0);
|
|
11018
|
-
if (item) await unstartSolution(ctx.client, job.project_id, item.ref).catch(() => void 0);
|
|
11019
|
-
if (kept?.commit) {
|
|
11020
|
-
await logActivity(ctx, job, "repo.commit", renderRepoResult(kept).split("\n")[0], { outcome: "error" });
|
|
11021
|
-
if (outcome) outcome = { ...outcome, result: [outcome.result, "", renderRepoResult(kept)].filter(Boolean).join("\n") };
|
|
11022
|
-
}
|
|
11204
|
+
await rm4(runDir, { recursive: true, force: true });
|
|
11023
11205
|
}
|
|
11206
|
+
if (outcome?.status === "succeeded") return outcome;
|
|
11024
11207
|
if (cancelled) return { status: "cancelled" };
|
|
11025
11208
|
if (timedOut) {
|
|
11026
11209
|
return { status: "failed", error: { code: "timeout", message: `exceeded the ${skill.manifest.timeout} cap` } };
|
|
@@ -11031,10 +11214,6 @@ async function sessionOf(client, jobId) {
|
|
|
11031
11214
|
const { data } = await client.from("jobs").select("harness_session_id").eq("id", jobId).maybeSingle();
|
|
11032
11215
|
return data?.harness_session_id ?? void 0;
|
|
11033
11216
|
}
|
|
11034
|
-
function mergeAttempts(first, second) {
|
|
11035
|
-
if (!first?.usage || !second?.usage) return second ?? first;
|
|
11036
|
-
return { ...second, usage: sumUsage(first.usage, second.usage) };
|
|
11037
|
-
}
|
|
11038
11217
|
async function askForTheSite(ctx, job, project, events) {
|
|
11039
11218
|
const started = Date.now();
|
|
11040
11219
|
const search = await searchForSite(project.name);
|
|
@@ -11060,7 +11239,7 @@ function harnessWebFor(searchProvider, machineCanSearch) {
|
|
|
11060
11239
|
}
|
|
11061
11240
|
async function machineWebSearch() {
|
|
11062
11241
|
try {
|
|
11063
|
-
const parsed = JSON.parse(await
|
|
11242
|
+
const parsed = JSON.parse(await readFile8(join13(nanopmHome(), "daemon.json"), "utf8"));
|
|
11064
11243
|
return parsed.webSearch === true;
|
|
11065
11244
|
} catch {
|
|
11066
11245
|
return false;
|
|
@@ -11102,7 +11281,7 @@ function buildHeader(job, project, cwd, harnessWeb = false, memory = false) {
|
|
|
11102
11281
|
return lines.join("\n");
|
|
11103
11282
|
}
|
|
11104
11283
|
function scratchDir(jobId) {
|
|
11105
|
-
return
|
|
11284
|
+
return join13(tmpdir(), `nanopm-run-${jobId}`);
|
|
11106
11285
|
}
|
|
11107
11286
|
async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
|
|
11108
11287
|
const refuse2 = (why) => {
|
|
@@ -11111,12 +11290,12 @@ async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
|
|
|
11111
11290
|
const requested = params.repo_path;
|
|
11112
11291
|
if (typeof requested !== "string" || !requested) {
|
|
11113
11292
|
if (mustBeTheClone) refuse2("the job named no repository");
|
|
11114
|
-
await
|
|
11293
|
+
await mkdir5(fallback, { recursive: true, mode: 448 });
|
|
11115
11294
|
return fallback;
|
|
11116
11295
|
}
|
|
11117
11296
|
if (!job.project_id) {
|
|
11118
11297
|
if (mustBeTheClone) refuse2("the job belongs to no project");
|
|
11119
|
-
await
|
|
11298
|
+
await mkdir5(fallback, { recursive: true, mode: 448 });
|
|
11120
11299
|
return fallback;
|
|
11121
11300
|
}
|
|
11122
11301
|
const { data: clone } = await ctx.client.from("clones").select("repo_path").eq("project_id", job.project_id).eq("daemon_id", ctx.daemonId).maybeSingle();
|
|
@@ -11124,12 +11303,12 @@ async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
|
|
|
11124
11303
|
if (!allowed || resolve4(allowed) !== resolve4(requested)) {
|
|
11125
11304
|
if (mustBeTheClone) refuse2(`${requested} is not the clone recorded for this project on this machine. Run any \`nanopm\` command inside your clone here to record it`);
|
|
11126
11305
|
console.error(`job ${job.id}: repo_path ${requested} is not this machine's clone; running in a scratch directory instead`);
|
|
11127
|
-
await
|
|
11306
|
+
await mkdir5(fallback, { recursive: true, mode: 448 });
|
|
11128
11307
|
return fallback;
|
|
11129
11308
|
}
|
|
11130
|
-
if (
|
|
11309
|
+
if (existsSync5(allowed)) return resolve4(allowed);
|
|
11131
11310
|
if (mustBeTheClone) refuse2(`${allowed} is not on this machine any more`);
|
|
11132
|
-
await
|
|
11311
|
+
await mkdir5(fallback, { recursive: true, mode: 448 });
|
|
11133
11312
|
return fallback;
|
|
11134
11313
|
}
|
|
11135
11314
|
function asData8(label, body) {
|
|
@@ -11138,10 +11317,10 @@ ${body}
|
|
|
11138
11317
|
</${label}>`;
|
|
11139
11318
|
}
|
|
11140
11319
|
function harnessEnv(timeoutMs, source = process.env) {
|
|
11141
|
-
const home = source.HOME ??
|
|
11320
|
+
const home = source.HOME ?? homedir4();
|
|
11142
11321
|
const env = {
|
|
11143
11322
|
HOME: home,
|
|
11144
|
-
PATH: [source.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
11323
|
+
PATH: [source.PATH ?? "/usr/local/bin:/usr/bin:/bin", join13(home, ".local", "bin")].join(":"),
|
|
11145
11324
|
MCP_TOOL_TIMEOUT: String(timeoutMs)
|
|
11146
11325
|
};
|
|
11147
11326
|
for (const key of ["USER", "LANG", "TMPDIR", "SHELL", "ANTHROPIC_API_KEY", "CLAUDE_CONFIG_DIR"]) {
|
|
@@ -11221,6 +11400,8 @@ async function runDaemon(options = {}) {
|
|
|
11221
11400
|
const startedAt = Date.now();
|
|
11222
11401
|
let lastDeployCheck = 0;
|
|
11223
11402
|
const DEPLOY_CHECK_MS = 30 * 6e4;
|
|
11403
|
+
const PAUSE_CHECK_MS = 6e4;
|
|
11404
|
+
let lastPauseCheck = 0;
|
|
11224
11405
|
const UPDATE_CHECK_MS = 60 * 6e4;
|
|
11225
11406
|
const updatable = !options.once && installKind() === "installer";
|
|
11226
11407
|
let lastUpdateCheck = Date.now() - UPDATE_CHECK_MS + 5 * 6e4;
|
|
@@ -11278,6 +11459,10 @@ async function runDaemon(options = {}) {
|
|
|
11278
11459
|
continue;
|
|
11279
11460
|
}
|
|
11280
11461
|
if (options.once && Date.now() - startedAt > onceTimeoutMs) break;
|
|
11462
|
+
if (!options.once && Date.now() - lastPauseCheck > PAUSE_CHECK_MS) {
|
|
11463
|
+
lastPauseCheck = Date.now();
|
|
11464
|
+
for (const id of await resumePausedBuilds(auth.client, daemonId).catch(() => [])) log(`build ${id}: the Claude limit reset, carrying on`);
|
|
11465
|
+
}
|
|
11281
11466
|
if (!options.once && Date.now() - lastDeployCheck > DEPLOY_CHECK_MS) {
|
|
11282
11467
|
lastDeployCheck = Date.now();
|
|
11283
11468
|
for (const check of await reviewDeploys(auth.client, daemonId).catch(() => [])) {
|
|
@@ -11318,12 +11503,12 @@ async function chainAfter(auth, job, outcome, log) {
|
|
|
11318
11503
|
const parent = job.parent_job_id ? await parentOf(auth, job.parent_job_id) : null;
|
|
11319
11504
|
const own = job.params ?? {};
|
|
11320
11505
|
const deep = own.depth === "deep" && job.trigger === "hook";
|
|
11321
|
-
const
|
|
11322
|
-
if (!
|
|
11506
|
+
const next = daemonQueues(job.skill, 0, parent?.skill ?? null, deep);
|
|
11507
|
+
if (!next.length) return;
|
|
11323
11508
|
if (await chainDepth(auth, job) >= CHAIN_DEPTH_MAX) return;
|
|
11324
11509
|
const from = deep ? own.repo_path : parent?.params?.repo_path;
|
|
11325
11510
|
const repo = typeof from === "string" ? { repo_path: from } : null;
|
|
11326
|
-
for (const skill of
|
|
11511
|
+
for (const skill of next) {
|
|
11327
11512
|
const params = { ...repo ?? {}, ...deep ? { depth: "deep", background: true } : {} };
|
|
11328
11513
|
const { data, error } = await auth.client.from("jobs").insert({
|
|
11329
11514
|
project_id: job.project_id,
|
|
@@ -11406,7 +11591,7 @@ var heldInThisProcess = false;
|
|
|
11406
11591
|
function acquireLock() {
|
|
11407
11592
|
if (heldInThisProcess) throw new Error("a nanopm daemon is already running in this process");
|
|
11408
11593
|
mkdirSync(nanopmHome(), { recursive: true, mode: 448 });
|
|
11409
|
-
const path =
|
|
11594
|
+
const path = join14(nanopmHome(), "daemon.pid");
|
|
11410
11595
|
for (; ; ) {
|
|
11411
11596
|
try {
|
|
11412
11597
|
const fd = openSync(path, "wx");
|
|
@@ -11478,10 +11663,11 @@ async function claimNext(client, daemonId, skills) {
|
|
|
11478
11663
|
return rows2[0] ?? null;
|
|
11479
11664
|
}
|
|
11480
11665
|
async function reclaimOwnJobs(client, daemonId) {
|
|
11481
|
-
const { data: stale } = await client.from("jobs").select("id, project_id, skill").eq("assigned_daemon_id", daemonId).in("status", ["running", "waiting"]);
|
|
11666
|
+
const { data: stale } = await client.from("jobs").select("id, project_id, skill, status, attempt, params").eq("assigned_daemon_id", daemonId).in("status", ["running", "waiting"]);
|
|
11482
11667
|
const ids = [];
|
|
11483
11668
|
for (const job of stale ?? []) {
|
|
11484
11669
|
await killOrphanedHarness(job.id);
|
|
11670
|
+
if (job.skill === BUILD_SKILL && (job.status === "waiting" || await requeueLostBuild(client, job))) continue;
|
|
11485
11671
|
const error = { code: "interrupted", message: "the daemon stopped while this job was running" };
|
|
11486
11672
|
await client.from("jobs").update({ status: "failed", finished_at: (/* @__PURE__ */ new Date()).toISOString(), error }).eq("id", job.id).in("status", ["running", "waiting"]);
|
|
11487
11673
|
await client.from("job_events").insert({ job_id: job.id, seq: 0, kind: "status", payload: { status: "failed", error }, author_kind: "nanopm" });
|
|
@@ -11493,13 +11679,13 @@ async function reclaimOwnJobs(client, daemonId) {
|
|
|
11493
11679
|
async function harnessPids(runDir) {
|
|
11494
11680
|
const dirs = [runDir];
|
|
11495
11681
|
try {
|
|
11496
|
-
for (const d of await
|
|
11682
|
+
for (const d of await readdir3(runDir, { withFileTypes: true })) if (d.isDirectory()) dirs.push(join14(runDir, d.name));
|
|
11497
11683
|
} catch {
|
|
11498
11684
|
}
|
|
11499
11685
|
const pids = [];
|
|
11500
11686
|
for (const dir of dirs) {
|
|
11501
11687
|
try {
|
|
11502
|
-
const pid = Number((await
|
|
11688
|
+
const pid = Number((await readFile9(join14(dir, "harness.pid"), "utf8")).trim());
|
|
11503
11689
|
if (pid > 1) pids.push(pid);
|
|
11504
11690
|
} catch {
|
|
11505
11691
|
}
|
|
@@ -11507,20 +11693,20 @@ async function harnessPids(runDir) {
|
|
|
11507
11693
|
return pids;
|
|
11508
11694
|
}
|
|
11509
11695
|
async function killOrphanedHarness(jobId) {
|
|
11510
|
-
const runDir =
|
|
11696
|
+
const runDir = join14(nanopmHome(), "run", jobId);
|
|
11511
11697
|
for (const pid of await harnessPids(runDir)) {
|
|
11512
11698
|
try {
|
|
11513
11699
|
process.kill(pid, "SIGTERM");
|
|
11514
11700
|
} catch {
|
|
11515
11701
|
}
|
|
11516
11702
|
}
|
|
11517
|
-
await
|
|
11703
|
+
await rm5(runDir, { recursive: true, force: true });
|
|
11518
11704
|
}
|
|
11519
11705
|
async function registerDaemon(auth, version, opts = {}) {
|
|
11520
|
-
const path =
|
|
11706
|
+
const path = join14(nanopmHome(), "daemon.json");
|
|
11521
11707
|
let identity = null;
|
|
11522
11708
|
try {
|
|
11523
|
-
identity = JSON.parse(await
|
|
11709
|
+
identity = JSON.parse(await readFile9(path, "utf8"));
|
|
11524
11710
|
} catch {
|
|
11525
11711
|
identity = null;
|
|
11526
11712
|
}
|
|
@@ -11558,8 +11744,8 @@ async function registerDaemon(auth, version, opts = {}) {
|
|
|
11558
11744
|
return data.id;
|
|
11559
11745
|
}
|
|
11560
11746
|
async function rememberDaemon(id, shared = false) {
|
|
11561
|
-
await
|
|
11562
|
-
await
|
|
11747
|
+
await mkdir6(nanopmHome(), { recursive: true, mode: 448 });
|
|
11748
|
+
await writeFile6(join14(nanopmHome(), "daemon.json"), JSON.stringify({ id, shared }, null, 2) + "\n", { mode: 384 });
|
|
11563
11749
|
}
|
|
11564
11750
|
async function heartbeat(client, daemonId) {
|
|
11565
11751
|
await client.from("daemons").update({ status: "online", last_seen_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", daemonId);
|
|
@@ -11613,13 +11799,15 @@ function contextFromEnv(env = process.env) {
|
|
|
11613
11799
|
userId: env.NANOPM_USER_ID,
|
|
11614
11800
|
actor: env.NANOPM_ACTOR,
|
|
11615
11801
|
askTimeoutMs: Number(env.NANOPM_ASK_TIMEOUT_MS ?? 3 * 60 * 60 * 1e3),
|
|
11616
|
-
workspace: env.NANOPM_WORKSPACE || void 0
|
|
11802
|
+
workspace: env.NANOPM_WORKSPACE || void 0,
|
|
11803
|
+
answerFile: env.NANOPM_ANSWER_FILE || void 0
|
|
11617
11804
|
};
|
|
11618
11805
|
}
|
|
11619
11806
|
|
|
11620
11807
|
// ../memory-mcp/src/server.ts
|
|
11621
11808
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11622
11809
|
import { randomUUID as randomUUID13 } from "crypto";
|
|
11810
|
+
import { readFile as readFile10, unlink } from "fs/promises";
|
|
11623
11811
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
11624
11812
|
import { z as z21 } from "zod";
|
|
11625
11813
|
|
|
@@ -11910,48 +12098,6 @@ function refuseFetch(url, allowlist, open = false) {
|
|
|
11910
12098
|
return `${host} is not on this project's allowlist, so it was not read. The founder can allow it: if it is a competitor, \`nanopm market confirm <ref>\` on the entry that names them, or \`nanopm market add <name> ${host}\`. Until then, search results and the sites already allowed are what there is.`;
|
|
11911
12099
|
}
|
|
11912
12100
|
|
|
11913
|
-
// ../memory-mcp/src/repo-rules.ts
|
|
11914
|
-
import { lstat, unlink } from "fs/promises";
|
|
11915
|
-
import { relative as relative3, resolve as resolve5, sep as sep2 } from "path";
|
|
11916
|
-
var REMOVE_MAX = 50;
|
|
11917
|
-
function refuseRemove(workspace, requested) {
|
|
11918
|
-
const root2 = resolve5(workspace);
|
|
11919
|
-
const target = resolve5(root2, requested);
|
|
11920
|
-
const rel = relative3(root2, target);
|
|
11921
|
-
if (!rel || rel.startsWith("..") || rel.startsWith(`..${sep2}`)) return "outside the workspace";
|
|
11922
|
-
if (rel === ".git" || rel.startsWith(`.git${sep2}`)) return "inside .git";
|
|
11923
|
-
if (looksLikeASecret(rel)) return "looks like a secret, which the agent may neither read nor remove";
|
|
11924
|
-
return null;
|
|
11925
|
-
}
|
|
11926
|
-
async function removeFiles(workspace, paths) {
|
|
11927
|
-
const out = [];
|
|
11928
|
-
for (const requested of paths.slice(0, REMOVE_MAX)) {
|
|
11929
|
-
const refusal = refuseRemove(workspace, requested);
|
|
11930
|
-
if (refusal) {
|
|
11931
|
-
out.push({ path: requested, removed: false, reason: refusal });
|
|
11932
|
-
continue;
|
|
11933
|
-
}
|
|
11934
|
-
const target = resolve5(workspace, requested);
|
|
11935
|
-
try {
|
|
11936
|
-
const stat2 = await lstat(target);
|
|
11937
|
-
if (stat2.isSymbolicLink()) {
|
|
11938
|
-
out.push({ path: requested, removed: false, reason: "a symlink; what it points at is not touched" });
|
|
11939
|
-
continue;
|
|
11940
|
-
}
|
|
11941
|
-
if (stat2.isDirectory()) {
|
|
11942
|
-
out.push({ path: requested, removed: false, reason: "a directory; name the files in it" });
|
|
11943
|
-
continue;
|
|
11944
|
-
}
|
|
11945
|
-
await unlink(target);
|
|
11946
|
-
out.push({ path: requested, removed: true });
|
|
11947
|
-
} catch (err) {
|
|
11948
|
-
const code = err.code;
|
|
11949
|
-
out.push({ path: requested, removed: false, reason: code === "ENOENT" ? "not there" : err instanceof Error ? err.message : String(err) });
|
|
11950
|
-
}
|
|
11951
|
-
}
|
|
11952
|
-
return out;
|
|
11953
|
-
}
|
|
11954
|
-
|
|
11955
12101
|
// ../memory-mcp/src/problem-rules.ts
|
|
11956
12102
|
import { z as z20 } from "zod";
|
|
11957
12103
|
var STATEMENT_MAX = 300;
|
|
@@ -12187,6 +12333,8 @@ ${renderMap()}`,
|
|
|
12187
12333
|
}
|
|
12188
12334
|
},
|
|
12189
12335
|
async ({ question, options, why }, extra) => {
|
|
12336
|
+
const handed = ctx.answerFile ? await takeAnswer(ctx.answerFile) : null;
|
|
12337
|
+
if (handed !== null) return json({ answer: handed, skipped: false });
|
|
12190
12338
|
const answer = await askUser(ctx, { question, options, why }, extra.signal);
|
|
12191
12339
|
if (answer === null) return failure("No answer arrived in time, or the job was cancelled. Do not ask again; continue with what you have.");
|
|
12192
12340
|
return json({ answer, skipped: /^\s*(skip|passe?r?)\s*$/i.test(answer) || answer.trim() === "" });
|
|
@@ -12326,7 +12474,7 @@ ${renderMap()}`,
|
|
|
12326
12474
|
}
|
|
12327
12475
|
},
|
|
12328
12476
|
async ({ ref, outcome, note }) => {
|
|
12329
|
-
return json(await
|
|
12477
|
+
return json(await finishSolution(ctx, ref, outcome, note));
|
|
12330
12478
|
}
|
|
12331
12479
|
);
|
|
12332
12480
|
server.registerTool(
|
|
@@ -12426,20 +12574,20 @@ ${renderMap()}`,
|
|
|
12426
12574
|
}
|
|
12427
12575
|
);
|
|
12428
12576
|
server.registerTool(
|
|
12429
|
-
"
|
|
12577
|
+
"build_plan",
|
|
12430
12578
|
{
|
|
12431
|
-
description:
|
|
12432
|
-
inputSchema: {
|
|
12579
|
+
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.`,
|
|
12580
|
+
inputSchema: { steps: z21.array(z21.string().min(3).max(BUILD_STEP_MAX)).min(BUILD_STEPS_MIN).max(BUILD_STEPS_MAX) }
|
|
12433
12581
|
},
|
|
12434
|
-
async ({
|
|
12435
|
-
|
|
12436
|
-
|
|
12437
|
-
|
|
12438
|
-
|
|
12439
|
-
|
|
12440
|
-
}
|
|
12441
|
-
|
|
12442
|
-
}
|
|
12582
|
+
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")
|
|
12583
|
+
);
|
|
12584
|
+
server.registerTool(
|
|
12585
|
+
"build_step",
|
|
12586
|
+
{
|
|
12587
|
+
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.",
|
|
12588
|
+
inputSchema: { step: z21.number().int().min(1).max(BUILD_STEPS_MAX), summary: z21.string().min(3).max(300) }
|
|
12589
|
+
},
|
|
12590
|
+
async ({ step }) => ctx.workspace ? json({ step, note: "Committed by NanoPM. Go on with the next step, or stop when the plan is done." }) : failure("this job builds nothing")
|
|
12443
12591
|
);
|
|
12444
12592
|
server.registerTool(
|
|
12445
12593
|
"web_search",
|
|
@@ -12751,6 +12899,12 @@ async function writeContext(ctx, items, refused = []) {
|
|
|
12751
12899
|
await logActivity2(ctx, "memory.write", `wrote ${written.length} items under ${topics}: ${written.map((w) => w.key).join(", ")}${refusedNote}`);
|
|
12752
12900
|
return { written };
|
|
12753
12901
|
}
|
|
12902
|
+
async function takeAnswer(answerFile) {
|
|
12903
|
+
const taken = `${answerFile}.taken`;
|
|
12904
|
+
const answer = await readFile10(taken, "utf8").catch(() => null);
|
|
12905
|
+
if (answer !== null) await unlink(taken).catch(() => void 0);
|
|
12906
|
+
return answer;
|
|
12907
|
+
}
|
|
12754
12908
|
async function askUser(ctx, q, signal) {
|
|
12755
12909
|
const seq = await nextSeq(ctx);
|
|
12756
12910
|
const { error } = await ctx.client.from("job_events").insert({ job_id: ctx.jobId, seq, kind: "question", payload: { question: q.question, options: q.options ?? null, why: q.why ?? null }, author_kind: "nanopm" });
|
|
@@ -13226,7 +13380,7 @@ async function markFast(ctx, problemRef) {
|
|
|
13226
13380
|
await logActivity2(ctx, "problems.fast", `${problem.ref} is one small change`);
|
|
13227
13381
|
return { problem: problem.ref, fast: true };
|
|
13228
13382
|
}
|
|
13229
|
-
async function
|
|
13383
|
+
async function finishSolution(ctx, ref, outcome, note) {
|
|
13230
13384
|
if (ctx.actor === "mapping-review") throw new Error("A targeted collection cannot decide solution outcomes.");
|
|
13231
13385
|
const upper = ref.toUpperCase();
|
|
13232
13386
|
const { data: current } = await ctx.client.from("solutions").select("status").eq("project_id", ctx.projectId).eq("ref", upper).is("superseded_by", null).maybeSingle();
|
|
@@ -13524,7 +13678,8 @@ var MEMORY_TOOL_NAMES = [
|
|
|
13524
13678
|
"play_store_search",
|
|
13525
13679
|
"list_market",
|
|
13526
13680
|
"write_market",
|
|
13527
|
-
"
|
|
13681
|
+
"build_plan",
|
|
13682
|
+
"build_step",
|
|
13528
13683
|
// Talk to Nano (docs/✅nanopm-68-talk-to-nano.md): the map read, and changed as the founder asked.
|
|
13529
13684
|
...TALK_TOOLS
|
|
13530
13685
|
];
|
|
@@ -13535,19 +13690,19 @@ async function startMemoryServer(env = process.env) {
|
|
|
13535
13690
|
}
|
|
13536
13691
|
|
|
13537
13692
|
// src/commands/add.ts
|
|
13538
|
-
import { readFile as
|
|
13693
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
13539
13694
|
|
|
13540
13695
|
// src/project.ts
|
|
13541
13696
|
import { execFile as execFile7 } from "child_process";
|
|
13542
|
-
import { basename } from "path";
|
|
13697
|
+
import { basename as basename2 } from "path";
|
|
13543
13698
|
import { promisify as promisify7 } from "util";
|
|
13544
13699
|
|
|
13545
13700
|
// src/local.ts
|
|
13546
|
-
import { readFile as
|
|
13547
|
-
import { join as
|
|
13701
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
13702
|
+
import { join as join15 } from "path";
|
|
13548
13703
|
async function localDaemonId() {
|
|
13549
13704
|
try {
|
|
13550
|
-
const parsed = JSON.parse(await
|
|
13705
|
+
const parsed = JSON.parse(await readFile11(join15(nanopmHome(), "daemon.json"), "utf8"));
|
|
13551
13706
|
return parsed.id ?? null;
|
|
13552
13707
|
} catch {
|
|
13553
13708
|
return null;
|
|
@@ -13572,7 +13727,7 @@ async function detectRepo(cwd) {
|
|
|
13572
13727
|
} catch {
|
|
13573
13728
|
remote = null;
|
|
13574
13729
|
}
|
|
13575
|
-
const name = remote ? remote.split("/").pop() :
|
|
13730
|
+
const name = remote ? remote.split("/").pop() : basename2(root2);
|
|
13576
13731
|
return { root: root2, remote, name, empty: await repoIsEmpty(root2) };
|
|
13577
13732
|
}
|
|
13578
13733
|
async function repoIsEmpty(root2) {
|
|
@@ -13583,7 +13738,7 @@ async function repoIsEmpty(root2) {
|
|
|
13583
13738
|
}
|
|
13584
13739
|
const files = (await execFileAsync7("git", ["-C", root2, "ls-files"])).stdout.split("\n").filter(Boolean);
|
|
13585
13740
|
return files.every((f) => {
|
|
13586
|
-
const base =
|
|
13741
|
+
const base = basename2(f);
|
|
13587
13742
|
return base.startsWith(".") || /^(readme|licen[cs]e)(\.|$)/i.test(base);
|
|
13588
13743
|
});
|
|
13589
13744
|
}
|
|
@@ -13682,7 +13837,7 @@ async function add(file, opts) {
|
|
|
13682
13837
|
text2 = await readStdin();
|
|
13683
13838
|
} else {
|
|
13684
13839
|
try {
|
|
13685
|
-
text2 = await
|
|
13840
|
+
text2 = await readFile12(file, "utf8");
|
|
13686
13841
|
} catch {
|
|
13687
13842
|
throw new Error(`could not read ${file}`);
|
|
13688
13843
|
}
|
|
@@ -13697,7 +13852,7 @@ async function add(file, opts) {
|
|
|
13697
13852
|
}
|
|
13698
13853
|
|
|
13699
13854
|
// src/commands/run.ts
|
|
13700
|
-
import { readFile as
|
|
13855
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
13701
13856
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
13702
13857
|
import { setTimeout as sleep3 } from "timers/promises";
|
|
13703
13858
|
|
|
@@ -13864,7 +14019,7 @@ function phaseOf(tool) {
|
|
|
13864
14019
|
if (name === "query") return "data";
|
|
13865
14020
|
if (name === "web_search" || name === "web_fetch") return "web";
|
|
13866
14021
|
if (REVIEW_WRITES.has(name)) return "review";
|
|
13867
|
-
if (name === "
|
|
14022
|
+
if (name === "build_plan" || name === "build_step") return "build";
|
|
13868
14023
|
return null;
|
|
13869
14024
|
}
|
|
13870
14025
|
function touched(tool, input) {
|
|
@@ -14029,7 +14184,7 @@ function questionBlock(p) {
|
|
|
14029
14184
|
|
|
14030
14185
|
// src/commands/run.ts
|
|
14031
14186
|
async function answersFromFile(path) {
|
|
14032
|
-
const raw = JSON.parse(await
|
|
14187
|
+
const raw = JSON.parse(await readFile13(path, "utf8"));
|
|
14033
14188
|
const entries = raw.map((a) => typeof a === "string" ? { answer: a } : a);
|
|
14034
14189
|
return scriptedAnswerer(entries);
|
|
14035
14190
|
}
|
|
@@ -14113,6 +14268,7 @@ async function streamJob(client, jobId, opts) {
|
|
|
14113
14268
|
const shownAnswers = /* @__PURE__ */ new Set();
|
|
14114
14269
|
for (; ; ) {
|
|
14115
14270
|
const { data: events } = await client.from("job_events").select("id, seq, kind, payload, author_kind").eq("job_id", jobId).gt("id", lastId).order("id", { ascending: true });
|
|
14271
|
+
for (const ev of events ?? []) if (ev.kind === "answer") answered.add(ev.seq);
|
|
14116
14272
|
for (const ev of events ?? []) {
|
|
14117
14273
|
lastId = Math.max(lastId, Number(ev.id));
|
|
14118
14274
|
const payload = ev.payload ?? {};
|
|
@@ -14251,16 +14407,16 @@ async function askOne(auth, projectId, question, daemonId, opts, out) {
|
|
|
14251
14407
|
|
|
14252
14408
|
// src/commands/daemon-service.ts
|
|
14253
14409
|
import { execFile as execFile8 } from "child_process";
|
|
14254
|
-
import { existsSync as
|
|
14255
|
-
import { mkdir as
|
|
14256
|
-
import { homedir as
|
|
14257
|
-
import { join as
|
|
14410
|
+
import { existsSync as existsSync6 } from "fs";
|
|
14411
|
+
import { mkdir as mkdir7, readFile as readFile14, rm as rm6, writeFile as writeFile7 } from "fs/promises";
|
|
14412
|
+
import { homedir as homedir5 } from "os";
|
|
14413
|
+
import { join as join16 } from "path";
|
|
14258
14414
|
import { promisify as promisify8 } from "util";
|
|
14259
14415
|
|
|
14260
14416
|
// package.json
|
|
14261
14417
|
var package_default = {
|
|
14262
14418
|
name: "@nanopm/cli",
|
|
14263
|
-
version: "0.1.
|
|
14419
|
+
version: "0.1.12",
|
|
14264
14420
|
description: "An autonomous product manager for builders who run several small products. Runs on your machine, drives your own coding harness.",
|
|
14265
14421
|
type: "module",
|
|
14266
14422
|
license: "MIT",
|
|
@@ -14312,10 +14468,10 @@ var VERSION = package_default.version;
|
|
|
14312
14468
|
var execFileAsync8 = promisify8(execFile8);
|
|
14313
14469
|
var LAUNCHD_LABEL = "pm.nano.daemon";
|
|
14314
14470
|
function plistPath() {
|
|
14315
|
-
return
|
|
14471
|
+
return join16(homedir5(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
14316
14472
|
}
|
|
14317
14473
|
function daemonLogPath() {
|
|
14318
|
-
return
|
|
14474
|
+
return join16(nanopmHome(), "daemon.log");
|
|
14319
14475
|
}
|
|
14320
14476
|
function renderPlist(opts) {
|
|
14321
14477
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -14374,13 +14530,13 @@ async function installDaemon(out = (l) => console.log(l), opts = {}) {
|
|
|
14374
14530
|
out(web.ok ? "Claude's own search answers; searches go through your subscription." : `Claude's search is not available on this machine (${web.message}); searches use DuckDuckGo.`);
|
|
14375
14531
|
const bin = process.argv[1];
|
|
14376
14532
|
if (!bin) throw new Error("cannot locate the nanopm binary");
|
|
14377
|
-
const home =
|
|
14378
|
-
const path = [process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
14379
|
-
await
|
|
14380
|
-
await
|
|
14533
|
+
const home = homedir5();
|
|
14534
|
+
const path = [process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", join16(home, ".local", "bin")].join(":");
|
|
14535
|
+
await mkdir7(join16(home, "Library", "LaunchAgents"), { recursive: true });
|
|
14536
|
+
await mkdir7(nanopmHome(), { recursive: true, mode: 448 });
|
|
14381
14537
|
if (opts.shared !== void 0) await rememberShared(opts.shared);
|
|
14382
14538
|
await found({ ...detection, authenticated: true });
|
|
14383
|
-
await
|
|
14539
|
+
await writeFile7(plistPath(), renderPlist({ node: process.execPath, bin, home, path, logPath: daemonLogPath(), nanopmHome: nanopmHome() }), { mode: 420 });
|
|
14384
14540
|
const target = guiTarget();
|
|
14385
14541
|
await execFileAsync8("launchctl", ["bootout", target, plistPath()]).catch(() => void 0);
|
|
14386
14542
|
await execFileAsync8("launchctl", ["bootstrap", target, plistPath()]);
|
|
@@ -14393,7 +14549,7 @@ function guiTarget() {
|
|
|
14393
14549
|
return `gui/${process.getuid?.() ?? 501}`;
|
|
14394
14550
|
}
|
|
14395
14551
|
async function restartDaemon() {
|
|
14396
|
-
if (!
|
|
14552
|
+
if (!existsSync6(plistPath())) return false;
|
|
14397
14553
|
await execFileAsync8("launchctl", ["kickstart", "-k", `${guiTarget()}/${LAUNCHD_LABEL}`]);
|
|
14398
14554
|
return true;
|
|
14399
14555
|
}
|
|
@@ -14401,12 +14557,12 @@ async function uninstallDaemon(out = (l) => console.log(l)) {
|
|
|
14401
14557
|
requireMac();
|
|
14402
14558
|
const target = guiTarget();
|
|
14403
14559
|
await execFileAsync8("launchctl", ["bootout", target, plistPath()]).catch(() => void 0);
|
|
14404
|
-
await
|
|
14560
|
+
await rm6(plistPath(), { force: true });
|
|
14405
14561
|
out(`Removed ${LAUNCHD_LABEL}.`);
|
|
14406
14562
|
}
|
|
14407
14563
|
async function daemonStatus(out = (l) => console.log(l)) {
|
|
14408
14564
|
const id = await localDaemonId();
|
|
14409
|
-
const installed = process.platform === "darwin" &&
|
|
14565
|
+
const installed = process.platform === "darwin" && existsSync6(plistPath());
|
|
14410
14566
|
out(`launchd: ${installed ? "installed" : "not installed"}`);
|
|
14411
14567
|
if (!id) {
|
|
14412
14568
|
out("daemon: never registered on this machine");
|
|
@@ -14428,7 +14584,7 @@ async function daemonStatus(out = (l) => console.log(l)) {
|
|
|
14428
14584
|
}
|
|
14429
14585
|
async function daemonLogs(lines = 50) {
|
|
14430
14586
|
try {
|
|
14431
|
-
const text2 = await
|
|
14587
|
+
const text2 = await readFile14(daemonLogPath(), "utf8");
|
|
14432
14588
|
return text2.trimEnd().split("\n").slice(-lines).join("\n");
|
|
14433
14589
|
} catch {
|
|
14434
14590
|
return `no log at ${daemonLogPath()} yet`;
|
|
@@ -14436,41 +14592,41 @@ async function daemonLogs(lines = 50) {
|
|
|
14436
14592
|
}
|
|
14437
14593
|
async function localWebSearch() {
|
|
14438
14594
|
try {
|
|
14439
|
-
const parsed = JSON.parse(await
|
|
14595
|
+
const parsed = JSON.parse(await readFile14(join16(nanopmHome(), "daemon.json"), "utf8"));
|
|
14440
14596
|
return parsed.webSearch === true;
|
|
14441
14597
|
} catch {
|
|
14442
14598
|
return false;
|
|
14443
14599
|
}
|
|
14444
14600
|
}
|
|
14445
14601
|
async function rememberWebSearch(webSearch2) {
|
|
14446
|
-
const path =
|
|
14602
|
+
const path = join16(nanopmHome(), "daemon.json");
|
|
14447
14603
|
let parsed = {};
|
|
14448
14604
|
try {
|
|
14449
|
-
parsed = JSON.parse(await
|
|
14605
|
+
parsed = JSON.parse(await readFile14(path, "utf8"));
|
|
14450
14606
|
} catch {
|
|
14451
14607
|
parsed = {};
|
|
14452
14608
|
}
|
|
14453
|
-
await
|
|
14454
|
-
await
|
|
14609
|
+
await mkdir7(nanopmHome(), { recursive: true, mode: 448 });
|
|
14610
|
+
await writeFile7(path, JSON.stringify({ ...parsed, webSearch: webSearch2 }, null, 2) + "\n", { mode: 384 });
|
|
14455
14611
|
}
|
|
14456
14612
|
async function localShared() {
|
|
14457
14613
|
try {
|
|
14458
|
-
const parsed = JSON.parse(await
|
|
14614
|
+
const parsed = JSON.parse(await readFile14(join16(nanopmHome(), "daemon.json"), "utf8"));
|
|
14459
14615
|
return Boolean(parsed.shared);
|
|
14460
14616
|
} catch {
|
|
14461
14617
|
return false;
|
|
14462
14618
|
}
|
|
14463
14619
|
}
|
|
14464
14620
|
async function rememberShared(shared) {
|
|
14465
|
-
const path =
|
|
14621
|
+
const path = join16(nanopmHome(), "daemon.json");
|
|
14466
14622
|
let parsed = {};
|
|
14467
14623
|
try {
|
|
14468
|
-
parsed = JSON.parse(await
|
|
14624
|
+
parsed = JSON.parse(await readFile14(path, "utf8"));
|
|
14469
14625
|
} catch {
|
|
14470
14626
|
parsed = {};
|
|
14471
14627
|
}
|
|
14472
|
-
await
|
|
14473
|
-
await
|
|
14628
|
+
await mkdir7(nanopmHome(), { recursive: true, mode: 448 });
|
|
14629
|
+
await writeFile7(path, JSON.stringify({ ...parsed, shared }, null, 2) + "\n", { mode: 384 });
|
|
14474
14630
|
}
|
|
14475
14631
|
|
|
14476
14632
|
// src/format/log.ts
|
|
@@ -14713,702 +14869,772 @@ The install line does the same: curl -fsSL https://nanopm.app/install | sh`);
|
|
|
14713
14869
|
out(await restartDaemon() ? `Updated ${VERSION} \u2192 ${latest}; the daemon restarted on it.` : `Updated ${VERSION} \u2192 ${latest}.`);
|
|
14714
14870
|
}
|
|
14715
14871
|
|
|
14716
|
-
// src/
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
|
|
14721
|
-
|
|
14722
|
-
|
|
14723
|
-
|
|
14872
|
+
// src/commands/autonomy.ts
|
|
14873
|
+
async function readAutonomy(client, projectId) {
|
|
14874
|
+
const { data } = await client.from("project_preferences").select("autonomy").eq("project_id", projectId).maybeSingle();
|
|
14875
|
+
return data?.autonomy ?? "propose";
|
|
14876
|
+
}
|
|
14877
|
+
var EXPLAINED = {
|
|
14878
|
+
propose: "NanoPM reads and proposes. It never changes a file.",
|
|
14879
|
+
execute_after_validation: "NanoPM changes files, on its own branch, after showing you what it will do and waiting for a yes.",
|
|
14880
|
+
execute_then_report: "NanoPM acts first and tells you after. Not available yet."
|
|
14724
14881
|
};
|
|
14725
|
-
|
|
14726
|
-
|
|
14727
|
-
const
|
|
14728
|
-
const
|
|
14729
|
-
|
|
14730
|
-
|
|
14731
|
-
|
|
14732
|
-
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
["rank", String(item.rank)],
|
|
14737
|
-
["who", item.actor === "founder" ? "you" : "NanoPM"],
|
|
14738
|
-
["bets on", item.assumption ?? "\u2014"],
|
|
14739
|
-
["we'll know", item.eval ?? "\u2014"]
|
|
14740
|
-
];
|
|
14741
|
-
for (const [k, v] of facts) out.push(paint(ANSI.dim, ` ${k.padEnd(9)} ${v}`));
|
|
14742
|
-
const sources2 = item.sources ?? [];
|
|
14743
|
-
const restsOn = sources2.filter((s) => !WORK_KINDS.has(s.kind));
|
|
14744
|
-
if (restsOn.length) {
|
|
14745
|
-
out.push(paint(ANSI.dim, ` ${"rests on".padEnd(9)} ${restsOn.map((s) => `${s.kind}:${s.ref}`).join(", ")}`));
|
|
14882
|
+
async function autonomy(level, opts) {
|
|
14883
|
+
const auth = await authenticate();
|
|
14884
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
14885
|
+
const current = await readAutonomy(auth.client, project.id);
|
|
14886
|
+
if (!level) {
|
|
14887
|
+
const lines = [`${project.name} is on "${current}".`, ""];
|
|
14888
|
+
for (const l of AUTONOMY_LEVELS) {
|
|
14889
|
+
lines.push(` ${l === current ? "\u25CF" : "\u25CB"} ${l.padEnd(26)} ${EXPLAINED[l]}`);
|
|
14890
|
+
}
|
|
14891
|
+
lines.push("", " nanopm autonomy <level>");
|
|
14892
|
+
return lines.join("\n");
|
|
14746
14893
|
}
|
|
14747
|
-
const
|
|
14748
|
-
if (
|
|
14749
|
-
|
|
14750
|
-
}
|
|
14751
|
-
|
|
14752
|
-
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14894
|
+
const wanted = level.trim();
|
|
14895
|
+
if (!AUTONOMY_LEVELS.includes(wanted)) {
|
|
14896
|
+
throw new Error(`"${level}" is not a level. Pick one of: ${AUTONOMY_LEVELS.join(", ")}.`);
|
|
14897
|
+
}
|
|
14898
|
+
if (wanted === "execute_then_report") {
|
|
14899
|
+
throw new Error(
|
|
14900
|
+
"execute_then_report is not available yet. NanoPM asks before it changes anything until the review loop has been used in anger."
|
|
14901
|
+
);
|
|
14902
|
+
}
|
|
14903
|
+
if (wanted === current) return `${project.name} is already on "${current}".`;
|
|
14904
|
+
const { error } = await auth.client.from("project_preferences").update({ autonomy: wanted }).eq("project_id", project.id);
|
|
14905
|
+
if (error) {
|
|
14906
|
+
throw new Error(`could not change the dial: ${error.message}`);
|
|
14907
|
+
}
|
|
14908
|
+
return [`${project.name} is now on "${wanted}".`, EXPLAINED[wanted]].join("\n");
|
|
14756
14909
|
}
|
|
14757
|
-
|
|
14758
|
-
|
|
14759
|
-
|
|
14760
|
-
|
|
14761
|
-
|
|
14762
|
-
|
|
14763
|
-
|
|
14764
|
-
|
|
14765
|
-
|
|
14766
|
-
|
|
14767
|
-
|
|
14768
|
-
|
|
14769
|
-
|
|
14770
|
-
|
|
14771
|
-
|
|
14772
|
-
const paint = painter(opts.color);
|
|
14773
|
-
if (!rows2.length) return "No decisions yet.";
|
|
14774
|
-
const out = [];
|
|
14775
|
-
for (const row of rows2) {
|
|
14776
|
-
const when = row.created_at.slice(0, 10);
|
|
14777
|
-
const verb = VERB[row.kind] ?? { text: row.kind, colour: ANSI.blue };
|
|
14778
|
-
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() : "";
|
|
14779
|
-
const by = row.by ? paint(ANSI.dim, ` \xB7 by ${row.by}`) : "";
|
|
14780
|
-
out.push(`${paint(ANSI.dim, when)} ${paint(verb.colour, verb.text)} ${about}${by}`.trimEnd());
|
|
14781
|
-
if (row.body) {
|
|
14782
|
-
for (const line of wrap(row.body, 72)) out.push(paint(ANSI.dim, ` ${line}`));
|
|
14910
|
+
async function verifyCommand(command, opts) {
|
|
14911
|
+
const auth = await authenticate();
|
|
14912
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
14913
|
+
const { data } = await auth.client.from("project_preferences").select("verify_command").eq("project_id", project.id).maybeSingle();
|
|
14914
|
+
const current = data?.verify_command ?? null;
|
|
14915
|
+
if (!command && !opts.clear) {
|
|
14916
|
+
if (!current) {
|
|
14917
|
+
return [
|
|
14918
|
+
`${project.name} has no check, so nothing verifies what NanoPM writes.`,
|
|
14919
|
+
"",
|
|
14920
|
+
' nanopm verify "npm run lint && npm test"',
|
|
14921
|
+
"",
|
|
14922
|
+
"A build runs it in its sandboxed copy of the repository before its last step,",
|
|
14923
|
+
"fixes what it reports, and says the result in the pull request."
|
|
14924
|
+
].join("\n");
|
|
14783
14925
|
}
|
|
14926
|
+
return [`${project.name} verifies with:`, "", ` ${current}`, "", ' nanopm verify "<command>" \xB7 nanopm verify --clear'].join("\n");
|
|
14784
14927
|
}
|
|
14785
|
-
|
|
14786
|
-
|
|
14787
|
-
|
|
14788
|
-
|
|
14789
|
-
|
|
14790
|
-
}
|
|
14791
|
-
function sizeWord(size, paint) {
|
|
14792
|
-
return size === "big" ? `${paint(ANSI.bold, "\u25AA")} ${SIZE_WORD.big}` : size === "small" ? paint(ANSI.dim, SIZE_WORD.small) : "";
|
|
14928
|
+
const next = opts.clear ? null : command.trim();
|
|
14929
|
+
if (next !== null && !next) throw new Error('a check needs a command: nanopm verify "npm test"');
|
|
14930
|
+
const { error } = await auth.client.from("project_preferences").update({ verify_command: next }).eq("project_id", project.id);
|
|
14931
|
+
if (error) throw new Error(`could not save the check: ${error.message}`);
|
|
14932
|
+
if (next === null) return `${project.name} no longer runs a check. Nothing will verify what NanoPM writes.`;
|
|
14933
|
+
return [`${project.name} will verify with:`, "", ` ${next}`, "", "A build runs it in its sandboxed copy of the repository before its last step."].join("\n");
|
|
14793
14934
|
}
|
|
14794
|
-
|
|
14935
|
+
|
|
14936
|
+
// src/commands/build.ts
|
|
14937
|
+
import { execFile as execFile10 } from "child_process";
|
|
14938
|
+
import { existsSync as existsSync7 } from "fs";
|
|
14939
|
+
import { homedir as homedir6 } from "os";
|
|
14940
|
+
import { promisify as promisify9 } from "util";
|
|
14941
|
+
|
|
14942
|
+
// src/format/opportunities.ts
|
|
14943
|
+
function renderOpportunities(rows2, lens, opts) {
|
|
14795
14944
|
const paint = painter(opts.color);
|
|
14796
14945
|
const width = Math.max(40, columns());
|
|
14797
|
-
const
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14803
|
-
|
|
14804
|
-
|
|
14805
|
-
const last = lines.at(-1);
|
|
14806
|
-
if (last !== void 0 && visibleLength(last) + 3 + visibleLength(chip) <= width) lines[lines.length - 1] = `${last} ${chip}`;
|
|
14807
|
-
else lines.push(chip);
|
|
14808
|
-
return lines;
|
|
14809
|
-
}, []);
|
|
14810
|
-
out.push("", ...legend);
|
|
14811
|
-
if (!board.cards.some((c) => c.pick.placed_by === "founder")) out.push(paint(ANSI.dim, "Nano placed these in the order of your opportunity map. Move them where you want them."));
|
|
14812
|
-
const refWidth = Math.max(5, ...board.cards.map((c) => c.pick.ref.length));
|
|
14813
|
-
const gutter = " ".repeat(refWidth + 4);
|
|
14814
|
-
const titleWidth = Math.min(Math.max(...board.cards.map((c) => c.pick.title.length)), width - gutter.length - 8);
|
|
14815
|
-
for (const h of HORIZONS) {
|
|
14816
|
-
const column = board.cards.filter((c) => c.horizon === h);
|
|
14817
|
-
out.push("", `${paint(ANSI.bold, HORIZON_WORD[h])} ${paint(ANSI.dim, String(column.length))}`);
|
|
14818
|
-
if (!column.length) out.push(paint(ANSI.dim, ` Move a card here: nanopm roadmap move <ref> ${h}`));
|
|
14819
|
-
for (const c of column) {
|
|
14820
|
-
const t = themes.get(c.branch.id);
|
|
14821
|
-
out.push(` ${paint(ANSI.dim, c.pick.ref.padEnd(refWidth))} ${paint(t.colour, "\u25CF")} ${paint(ANSI.dim, t.name)}`);
|
|
14822
|
-
out.push(`${gutter}${clip(c.pick.title, titleWidth).padEnd(titleWidth)} ${sizeWord(c.pick.size, paint)}`.trimEnd());
|
|
14823
|
-
const depth = board.working.get(c.leaf.id);
|
|
14824
|
-
if (depth) out.push(`${gutter}${paint(ANSI.blue, "\u25CF")} ${paint(ANSI.dim, WORK_WORD[depth])}`);
|
|
14825
|
-
}
|
|
14946
|
+
const out = [];
|
|
14947
|
+
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`.";
|
|
14948
|
+
out.push(`${paint(ANSI.bold, "Opportunities")} ${paint(ANSI.dim, `${lens.lever} \u2014 ${lens.sentence}`)}`);
|
|
14949
|
+
out.push(paint(ANSI.dim, lens.definition ? `Counts: ${lens.definition}` : "What it counts is not defined yet: drawn against the lever and your sentence."));
|
|
14950
|
+
if (opts.previous) out.push(paint(ANSI.amber, "Drawn for your previous objective. `nanopm opportunities run` draws it for this one."));
|
|
14951
|
+
if (!rows2.length) {
|
|
14952
|
+
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.");
|
|
14953
|
+
return out.join("\n");
|
|
14826
14954
|
}
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
)
|
|
14955
|
+
const tree = liveTree(rows2);
|
|
14956
|
+
const live = rows2.filter(isLive);
|
|
14957
|
+
const evidenced = live.filter((r) => r.evidence_status === "evidence_backed").length;
|
|
14958
|
+
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.`));
|
|
14959
|
+
const who = (r) => r.personas.map((p) => opts.names?.get(p) ?? p).join(", ");
|
|
14960
|
+
const one = (r, place) => {
|
|
14961
|
+
const indent = " ".repeat(r.depth - 1);
|
|
14962
|
+
const yours = r.status === "confirmed" ? paint(ANSI.green, " yours") : "";
|
|
14963
|
+
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)}`);
|
|
14964
|
+
const pad = `${indent} `;
|
|
14965
|
+
const facts = [PRIORITY_WORD[r.priority], EVIDENCE_STATUS_WORD[r.evidence_status], NEXT_ACTION_WORD[r.next_action], who(r)].filter(Boolean).join(" \xB7 ");
|
|
14966
|
+
out.push(`${pad}${paint(ANSI.dim, facts)}`);
|
|
14967
|
+
const gist = r.description ? paragraphs(r.description)[0] : null;
|
|
14968
|
+
if (gist) for (const l of wrap(gist, width - pad.length)) out.push(`${pad}${l}`);
|
|
14969
|
+
if (r.learn) for (const l of wrap(`To learn more: ${r.learn}`, width - pad.length)) out.push(`${pad}${paint(ANSI.dim, l)}`);
|
|
14970
|
+
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}.`)}`);
|
|
14971
|
+
if (r.reconsider) for (const l of wrap(`Worth reconsidering: ${r.reconsider}`, width - pad.length)) out.push(`${pad}${paint(ANSI.amber, l)}`);
|
|
14972
|
+
for (const c of r.children) one(c, `${place}.${c.rank}`);
|
|
14973
|
+
};
|
|
14974
|
+
for (const r of tree) one(r, String(r.rank));
|
|
14975
|
+
const aside = rows2.filter((r) => r.status === "set_aside");
|
|
14976
|
+
if (aside.length) {
|
|
14977
|
+
out.push("", paint(ANSI.bold, `Set aside (${aside.length})`));
|
|
14978
|
+
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}`) : ""}`);
|
|
14979
|
+
}
|
|
14980
|
+
out.push("", paint(ANSI.dim, '`nanopm opportunities confirm <ref>`, `set-aside <ref> "why"`, `move <ref> <place>`, `reword <ref> "\u2026"`, `brainstorm <ref> [--deep]`, `run [--deep] [--note "\u2026"] [--use signals,data,repo,market] [--skip \u2026]`.'));
|
|
14833
14981
|
return out.join("\n");
|
|
14834
14982
|
}
|
|
14835
|
-
|
|
14983
|
+
|
|
14984
|
+
// src/format/needs.ts
|
|
14985
|
+
var MARK = { observed: "\u25CF", stated: "\u2013", inferred: "\u25CB" };
|
|
14986
|
+
function renderNeeds(persona, tree, opts) {
|
|
14836
14987
|
const paint = painter(opts.color);
|
|
14837
14988
|
const width = Math.max(40, columns());
|
|
14838
|
-
const theme = at.branch ? themesOf(at.tree).get(at.branch.id) : void 0;
|
|
14839
14989
|
const out = [];
|
|
14840
|
-
|
|
14841
|
-
|
|
14842
|
-
|
|
14843
|
-
|
|
14844
|
-
|
|
14845
|
-
heading("Problem we are solving");
|
|
14846
|
-
if (at.leaf) {
|
|
14847
|
-
const solves = wrap(at.leaf.problem, width - 6 - at.leaf.ref.length - 2);
|
|
14848
|
-
solves[solves.length - 1] += ` ${paint(ANSI.dim, at.leaf.ref)}`;
|
|
14849
|
-
for (const line of solves) out.push(` ${line}`);
|
|
14850
|
-
if (at.branch && at.branch.id !== at.leaf.id) {
|
|
14851
|
-
for (const line of wrap(`Part of: ${at.branch.problem} ${at.branch.ref}`, width - 6)) out.push(` ${paint(ANSI.dim, line)}`);
|
|
14852
|
-
}
|
|
14853
|
-
} else {
|
|
14854
|
-
out.push(paint(ANSI.dim, " The problem it answered is no longer on the map."));
|
|
14855
|
-
}
|
|
14856
|
-
const described = paragraphs(s.description);
|
|
14857
|
-
if (described.length) {
|
|
14858
|
-
heading("Proposed solution");
|
|
14859
|
-
described.forEach((p, i) => {
|
|
14860
|
-
if (i) out.push("");
|
|
14861
|
-
for (const line of wrap(p, width - 6)) out.push(` ${line}`);
|
|
14862
|
-
});
|
|
14863
|
-
}
|
|
14864
|
-
const changelog = parseChangelog(s.changelog);
|
|
14865
|
-
if (changelog) {
|
|
14866
|
-
heading("Future changelog");
|
|
14867
|
-
for (const line of wrap(changelog.headline, width - 6)) out.push(` ${paint(ANSI.bold, line)}`);
|
|
14868
|
-
for (const point of changelog.points) wrap(point, width - 8).forEach((line, i) => out.push(` ${i ? " " : "\u2022"} ${line}`));
|
|
14869
|
-
}
|
|
14870
|
-
if (s.steps.length) {
|
|
14871
|
-
heading("Plan");
|
|
14872
|
-
s.steps.forEach((step, i) => {
|
|
14873
|
-
const who = STEP_WHO_WORD[step.who] ?? step.who;
|
|
14874
|
-
wrap(step.do, width - pad.length - 8).forEach((line, j) => out.push(`${j ? pad : ` \u2610 ${who.padEnd(6)}`}${line}`));
|
|
14875
|
-
if (step.checks) out.push(pad + paint(ANSI.amber, i === 0 ? "checks the problem first" : "checks the problem"));
|
|
14876
|
-
});
|
|
14990
|
+
const counts = needCounts(tree);
|
|
14991
|
+
out.push(`${paint(ANSI.bold, `${persona.name}\u2019s needs`)} ${paint(ANSI.dim, `${counts.needs} needs \xB7 ${counts.difficulties + counts.precise} difficulties`)}`);
|
|
14992
|
+
if (!tree.length) {
|
|
14993
|
+
out.push("", `Nothing mapped yet. \`nanopm needs --refresh ${persona.slug}\` maps it in depth; add --short for a quick pass.`);
|
|
14994
|
+
return out.join("\n");
|
|
14877
14995
|
}
|
|
14878
|
-
const
|
|
14879
|
-
|
|
14880
|
-
|
|
14881
|
-
|
|
14882
|
-
|
|
14883
|
-
const
|
|
14884
|
-
const
|
|
14885
|
-
out.push(paint(ANSI.dim,
|
|
14886
|
-
|
|
14887
|
-
|
|
14888
|
-
|
|
14889
|
-
|
|
14890
|
-
out.push(
|
|
14996
|
+
const line = (n, depth) => {
|
|
14997
|
+
const indent = " ".repeat(depth);
|
|
14998
|
+
const lead = n.level === 1 ? `${n.emoji ?? " "} ` : `${MARK[n.provenance]} `;
|
|
14999
|
+
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"}`) : "";
|
|
15000
|
+
const state = n.status === "dropped" ? paint(ANSI.dim, " dropped") : n.status === "confirmed" || n.author_kind === "user" ? "" : paint(ANSI.amber, " to review");
|
|
15001
|
+
const label = n.status === "dropped" ? paint(ANSI.dim, n.label) : n.level === 1 ? paint(ANSI.bold, n.label) : n.label;
|
|
15002
|
+
const body = wrap(`${label}${where}${state}${n.proposal ? paint(ANSI.amber, " \xB7 NanoPM suggests a new wording") : ""}`, width - indent.length - 12);
|
|
15003
|
+
out.push(`${indent}${lead}${paint(ANSI.dim, n.ref.padEnd(8))} ${body[0] ?? ""}`);
|
|
15004
|
+
for (const more of body.slice(1)) out.push(`${indent} ${more}`);
|
|
15005
|
+
for (const c of n.children) line(c, depth + 1);
|
|
15006
|
+
};
|
|
15007
|
+
for (const need of tree) {
|
|
15008
|
+
out.push("");
|
|
15009
|
+
line(need, 0);
|
|
14891
15010
|
}
|
|
15011
|
+
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>\`.`));
|
|
15012
|
+
if (opts.others?.length) out.push(paint(ANSI.dim, `Other personas: ${opts.others.join(", ")}.`));
|
|
14892
15013
|
return out.join("\n");
|
|
14893
15014
|
}
|
|
14894
15015
|
|
|
14895
|
-
// src/
|
|
14896
|
-
|
|
14897
|
-
|
|
14898
|
-
|
|
14899
|
-
|
|
14900
|
-
|
|
14901
|
-
};
|
|
14902
|
-
|
|
14903
|
-
const paint = painter(opts.color);
|
|
14904
|
-
if (problem.importance_basis === "evidence") {
|
|
14905
|
-
return paint(ANSI.bold, String(Math.round(problem.importance)).padStart(3));
|
|
14906
|
-
}
|
|
14907
|
-
const value = problem.importance;
|
|
14908
|
-
const word = value >= 67 ? "high" : value >= 34 ? "med" : value > 0 ? "low" : "\u2014";
|
|
14909
|
-
return paint(ANSI.dim, `~${word}`.padStart(5));
|
|
15016
|
+
// src/commands/needs.ts
|
|
15017
|
+
async function people(client, projectId) {
|
|
15018
|
+
const { data, error } = await client.from("memory").select("key, title, content").eq("project_id", projectId).eq("topic", PERSONA_TOPIC).is("superseded_by", null);
|
|
15019
|
+
if (error) throw new Error(`could not read the personas: ${error.message}`);
|
|
15020
|
+
return (data ?? []).filter((r) => PERSONA_KEY.test(r.key)).map((r) => {
|
|
15021
|
+
const p = parsePersona(r.key, r.content);
|
|
15022
|
+
return { slug: p.slug, name: personaName(r.title, p.slug), primary: p.primary };
|
|
15023
|
+
}).sort((a, b) => Number(b.primary) - Number(a.primary));
|
|
14910
15024
|
}
|
|
14911
|
-
function
|
|
14912
|
-
|
|
14913
|
-
if (!
|
|
14914
|
-
|
|
14915
|
-
|
|
14916
|
-
|
|
14917
|
-
paint(ANSI.dim, " nanopm review reads your data, then works out what they say about your users"),
|
|
14918
|
-
paint(ANSI.dim, " nanopm opportunities what to work on, for your objective")
|
|
14919
|
-
].join("\n");
|
|
14920
|
-
}
|
|
14921
|
-
const refWidth = Math.max(...problems2.map((p) => p.ref.length));
|
|
14922
|
-
const out = [];
|
|
14923
|
-
const map = groupProblemMap(problems2.map((p) => ({ ...p, parentRef: p.parent_ref })));
|
|
14924
|
-
const ordered = [...map.branches.flatMap((b) => [b.parent, ...b.children]), ...map.unplaced];
|
|
14925
|
-
for (const problem of ordered) {
|
|
14926
|
-
const shape2 = SHAPE2[problem.status] ?? { mark: "\xB7", colour: ANSI.dim, label: problem.status };
|
|
14927
|
-
const score = renderImportance(problem, opts);
|
|
14928
|
-
const head = `${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, problem.ref.padEnd(refWidth))} ${score} ${problem.parent_ref ? paint(ANSI.dim, "\u21B3 ") : ""}${clip(firstSentence(problem.statement), Math.max(40, columns() - refWidth - 14))}`;
|
|
14929
|
-
out.push(head);
|
|
14930
|
-
if (problem.mapping) out.push(paint(ANSI.dim, ` ${problem.mapping.level === 1 ? "Need" : "Difficulty"} \xB7 ${problem.mapping.knowledge} \xB7 ${problem.mapping.origin === "expert_inference" ? "expert inference" : "source analysis"}`));
|
|
14931
|
-
const indent = ` ${" ".repeat(refWidth)} `;
|
|
14932
|
-
const tail2 = problem.proofs > 0 ? `${problem.proofs} proof${problem.proofs === 1 ? "" : "s"}${problem.proof_kinds?.length ? ` \xB7 ${problem.proof_kinds.join(", ")}` : ""}` : problem.importance_note ? `no proof yet \xB7 ${problem.importance_note}` : "no proof yet";
|
|
14933
|
-
out.push(paint(ANSI.dim, indent + clip(tail2, Math.max(40, columns() - indent.length))));
|
|
14934
|
-
}
|
|
14935
|
-
const unproven = problems2.filter((p) => p.proofs === 0).length;
|
|
14936
|
-
if (unproven) {
|
|
14937
|
-
out.push("");
|
|
14938
|
-
out.push(
|
|
14939
|
-
paint(
|
|
14940
|
-
ANSI.dim,
|
|
14941
|
-
`${unproven} of ${problems2.length} rest on judgement, not proof. A tilde marks those.`
|
|
14942
|
-
)
|
|
14943
|
-
);
|
|
14944
|
-
}
|
|
14945
|
-
if (duplicates.length) {
|
|
14946
|
-
out.push("");
|
|
14947
|
-
for (const pair of duplicates.slice(0, 3)) {
|
|
14948
|
-
const why = pair.flagged ? `NanoPM is not sure ${pair.a} and ${pair.b} are different problems` : `${pair.a} and ${pair.b} read alike`;
|
|
14949
|
-
out.push(paint(ANSI.amber, `? ${why}`));
|
|
14950
|
-
}
|
|
14951
|
-
out.push(paint(ANSI.dim, ` nanopm decisions why \xB7 nanopm problems merge ${duplicates[0].b} ${duplicates[0].a}`));
|
|
14952
|
-
}
|
|
14953
|
-
if (opts.manualMerges !== void 0) {
|
|
14954
|
-
out.push("");
|
|
14955
|
-
out.push(
|
|
14956
|
-
paint(
|
|
14957
|
-
ANSI.dim,
|
|
14958
|
-
`${opts.manualMerges} merge${opts.manualMerges === 1 ? "" : "s"} you have had to make by hand.`
|
|
14959
|
-
)
|
|
14960
|
-
);
|
|
14961
|
-
}
|
|
14962
|
-
out.push("");
|
|
14963
|
-
out.push(paint(ANSI.dim, " nanopm problems <ref> \xB7 nanopm problems merge <from> <into> \xB7 nanopm problems weight <ref> <0-100>"));
|
|
14964
|
-
return out.join("\n");
|
|
15025
|
+
function pick2(list, named) {
|
|
15026
|
+
if (!list.length) throw new Error("there is no persona on the business card yet. `nanopm personas --refresh` works out who this is for.");
|
|
15027
|
+
if (!named) return list[0];
|
|
15028
|
+
const one = list.find((p) => p.slug === named.toLowerCase() || p.name.toLowerCase() === named.toLowerCase());
|
|
15029
|
+
if (!one) throw new Error(`no persona "${named}". The personas are: ${list.map((p) => p.slug).join(", ")}.`);
|
|
15030
|
+
return one;
|
|
14965
15031
|
}
|
|
14966
|
-
function
|
|
14967
|
-
|
|
14968
|
-
|
|
14969
|
-
const
|
|
14970
|
-
|
|
14971
|
-
|
|
14972
|
-
|
|
14973
|
-
|
|
14974
|
-
|
|
14975
|
-
const
|
|
14976
|
-
const
|
|
14977
|
-
|
|
14978
|
-
|
|
14979
|
-
|
|
14980
|
-
|
|
14981
|
-
|
|
14982
|
-
|
|
14983
|
-
|
|
14984
|
-
|
|
14985
|
-
|
|
14986
|
-
|
|
14987
|
-
|
|
14988
|
-
|
|
14989
|
-
|
|
14990
|
-
|
|
14991
|
-
|
|
15032
|
+
async function rows(client, projectId, persona) {
|
|
15033
|
+
let q = client.from("need_nodes").select(NEED_NODE_COLUMNS).eq("project_id", projectId);
|
|
15034
|
+
if (persona) q = q.eq("persona", persona);
|
|
15035
|
+
const { data, error } = await q.order("created_at");
|
|
15036
|
+
if (error) throw new Error(`could not read the needs map: ${error.message}`);
|
|
15037
|
+
return data ?? [];
|
|
15038
|
+
}
|
|
15039
|
+
async function needs(persona, opts) {
|
|
15040
|
+
const auth = await authenticate();
|
|
15041
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15042
|
+
const list = await people(auth.client, project.id);
|
|
15043
|
+
const who = pick2(list, persona);
|
|
15044
|
+
const tree = needTree(await rows(auth.client, project.id, who.slug));
|
|
15045
|
+
return renderNeeds(who, tree, { color: opts.color ?? useColor(), others: list.filter((p) => p.slug !== who.slug).map((p) => p.slug) });
|
|
15046
|
+
}
|
|
15047
|
+
async function mapNeeds(target, opts) {
|
|
15048
|
+
const auth = await authenticate();
|
|
15049
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15050
|
+
const list = await people(auth.client, project.id);
|
|
15051
|
+
let persona;
|
|
15052
|
+
let scope = "map";
|
|
15053
|
+
if (target.ref) {
|
|
15054
|
+
const ref = target.ref.toUpperCase();
|
|
15055
|
+
const node = (await rows(auth.client, project.id)).find((r) => r.ref === ref);
|
|
15056
|
+
if (!node) throw new Error(`no node ${target.ref} on any needs map. \`nanopm needs\` shows the refs.`);
|
|
15057
|
+
persona = pick2(list, node.persona);
|
|
15058
|
+
scope = ref;
|
|
15059
|
+
} else persona = pick2(list, target.persona);
|
|
15060
|
+
const [{ data: active }, tree] = await Promise.all([
|
|
15061
|
+
auth.client.from("jobs").select("params").eq("project_id", project.id).eq("skill", "needs").in("status", [...ACTIVE_STATUSES]),
|
|
15062
|
+
rows(auth.client, project.id, persona.slug).then(needTree)
|
|
14992
15063
|
]);
|
|
14993
|
-
|
|
14994
|
-
|
|
14995
|
-
|
|
14996
|
-
|
|
14997
|
-
|
|
14998
|
-
|
|
14999
|
-
|
|
15000
|
-
|
|
15001
|
-
|
|
15002
|
-
|
|
15003
|
-
|
|
15004
|
-
|
|
15005
|
-
|
|
15006
|
-
|
|
15007
|
-
|
|
15008
|
-
|
|
15009
|
-
|
|
15010
|
-
|
|
15011
|
-
|
|
15012
|
-
|
|
15013
|
-
|
|
15014
|
-
|
|
15015
|
-
|
|
15064
|
+
const lineage = (ref) => {
|
|
15065
|
+
const walk = (l) => {
|
|
15066
|
+
for (const n of l) {
|
|
15067
|
+
if (n.ref === ref.toUpperCase()) return n.path.map((p) => p.ref);
|
|
15068
|
+
const deeper = walk(n.children);
|
|
15069
|
+
if (deeper) return deeper;
|
|
15070
|
+
}
|
|
15071
|
+
return null;
|
|
15072
|
+
};
|
|
15073
|
+
return walk(tree) ?? [ref];
|
|
15074
|
+
};
|
|
15075
|
+
const clash = needsRunConflict((active ?? []).map((j) => runScopeOf(j.params)), { persona: persona.slug, scope }, { primary: list.find((p) => p.primary)?.slug ?? list[0]?.slug ?? null, lineage, name: (s) => list.find((p) => p.slug === s)?.name ?? s });
|
|
15076
|
+
if (clash) throw new Error(clash);
|
|
15077
|
+
const repo = await detectRepo(opts.cwd);
|
|
15078
|
+
if (repo) await rememberClone(auth.client, project.id, repo.root);
|
|
15079
|
+
const params = {
|
|
15080
|
+
persona: persona.slug,
|
|
15081
|
+
...scope === "map" ? {} : { scope },
|
|
15082
|
+
depth: opts.short ? "short" : "deep",
|
|
15083
|
+
...opts.note?.trim() ? { note: opts.note.trim().slice(0, 600) } : {},
|
|
15084
|
+
...repo ? { repo_path: repo.root } : {}
|
|
15085
|
+
};
|
|
15086
|
+
return runSkill("needs", { project: project.id, params, cancelSignal: opts.cancelSignal });
|
|
15087
|
+
}
|
|
15088
|
+
async function confirmNeed(ref, opts) {
|
|
15089
|
+
const auth = await authenticate();
|
|
15090
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15091
|
+
const { data, error } = await auth.client.from("need_nodes").update({ status: "confirmed" }).eq("project_id", project.id).eq("ref", ref.toUpperCase()).eq("status", "proposed").select("ref, label");
|
|
15092
|
+
if (error) throw new Error(pgMessage(error));
|
|
15093
|
+
if (!data?.length) throw new Error(`${ref} is not a node waiting on your word. \`nanopm needs\` shows which are.`);
|
|
15094
|
+
return `Confirmed ${data[0].ref}: ${data[0].label}. A run proposes beside it from now on, never over it.`;
|
|
15095
|
+
}
|
|
15096
|
+
async function dropNeed(ref, why, opts) {
|
|
15097
|
+
const auth = await authenticate();
|
|
15098
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15099
|
+
const { data, error } = await auth.client.from("need_nodes").update({ status: "dropped", drop_reason: why?.trim().slice(0, 400) || "not true of them" }).eq("project_id", project.id).eq("ref", ref.toUpperCase()).in("status", ["proposed", "confirmed"]).select("ref, label");
|
|
15100
|
+
if (error) throw new Error(pgMessage(error));
|
|
15101
|
+
if (!data?.length) throw new Error(`${ref} is not on the map any more.`);
|
|
15102
|
+
return `Dropped ${data[0].ref}: ${data[0].label}. It stays on the record, and NanoPM will not propose it again in the same place.`;
|
|
15103
|
+
}
|
|
15104
|
+
|
|
15105
|
+
// src/commands/opportunities.ts
|
|
15106
|
+
async function lensOf2(client, projectId) {
|
|
15107
|
+
const { data } = await client.from("memory").select("id, key, content").eq("project_id", projectId).eq("topic", GOALS_TOPIC).in("key", [OBJECTIVE_KEY, DEFINITION_KEY]).is("superseded_by", null);
|
|
15108
|
+
const rows2 = data ?? [];
|
|
15109
|
+
const aim = rows2.find((r) => r.key === OBJECTIVE_KEY);
|
|
15110
|
+
const m = aim ? /^Lever:\s*([a-z]+)\.\s*(.*)$/s.exec(aim.content.trim()) : null;
|
|
15111
|
+
const lever = m?.[1];
|
|
15112
|
+
if (!aim || !lever || !OUTCOME_LEVERS.includes(lever)) return null;
|
|
15113
|
+
const def = rows2.find((r) => r.key === DEFINITION_KEY);
|
|
15114
|
+
const parsed = def ? parseDefinition(def.content) : null;
|
|
15115
|
+
const sentence2 = m[2].trim();
|
|
15116
|
+
const definition = parsed && parsed.lever === lever ? parsed.sentence : null;
|
|
15117
|
+
return { id: aim.id, lever, sentence: sentence2, definition, key: lensKey(lever, sentence2, definition) };
|
|
15118
|
+
}
|
|
15119
|
+
async function loadOpportunities(client, projectId) {
|
|
15120
|
+
const [standing, replaced] = await Promise.all([
|
|
15121
|
+
client.from("opportunities").select(OPPORTUNITY_COLUMNS).eq("project_id", projectId).neq("status", "replaced"),
|
|
15122
|
+
client.from("opportunities").select(REPLACED_COLUMNS).eq("project_id", projectId).eq("status", "replaced")
|
|
15123
|
+
]);
|
|
15124
|
+
const error = standing.error ?? replaced.error;
|
|
15125
|
+
if (error) throw new Error(`could not read the opportunities: ${error.message}`);
|
|
15126
|
+
return [...standing.data ?? [], ...replaced.data ?? []];
|
|
15127
|
+
}
|
|
15128
|
+
async function workingOn(client, projectId) {
|
|
15129
|
+
const { data } = await client.from("jobs").select("params").eq("project_id", projectId).eq("skill", SOLUTIONS_SKILL).in("status", [...ACTIVE_STATUSES]);
|
|
15130
|
+
return leavesAtWork((data ?? []).map((j) => j.params));
|
|
15131
|
+
}
|
|
15132
|
+
async function opportunities(opts) {
|
|
15133
|
+
const auth = await authenticate();
|
|
15134
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15135
|
+
const [lens, all, personas2] = await Promise.all([lensOf2(auth.client, project.id), loadOpportunities(auth.client, project.id), people(auth.client, project.id)]);
|
|
15136
|
+
const { shown, drawnFor } = shownMap(all, lens?.key ?? null);
|
|
15137
|
+
const names = new Map(personas2.map((p) => [p.slug, p.name]));
|
|
15138
|
+
return renderOpportunities(shown, lens, { color: opts.color ?? useColor(), previous: drawnFor === "previous", names });
|
|
15139
|
+
}
|
|
15140
|
+
async function rankOpportunities(opts) {
|
|
15141
|
+
const auth = await authenticate();
|
|
15142
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15143
|
+
const list = (s) => s ? s.split(",").map((x) => x.trim()).filter(Boolean) : void 0;
|
|
15144
|
+
const sources2 = opts.use || opts.skip ? { ...opts.use ? { use: list(opts.use) } : {}, ...opts.skip ? { skip: list(opts.skip) } : {} } : void 0;
|
|
15145
|
+
const params = {
|
|
15146
|
+
depth: opts.deep ? "deep" : "short",
|
|
15147
|
+
...opts.note?.trim() ? { note: opts.note.trim().slice(0, OPPORTUNITIES_NOTE_MAX) } : {},
|
|
15148
|
+
...sources2 ? { sources: sources2 } : {}
|
|
15149
|
+
};
|
|
15150
|
+
const checked = opportunitiesAsk(params);
|
|
15151
|
+
if (!checked.ok) throw new Error(checked.why);
|
|
15152
|
+
const { data: active } = await auth.client.from("jobs").select("id").eq("project_id", project.id).eq("skill", "opportunities").in("status", [...ACTIVE_STATUSES]);
|
|
15153
|
+
const clash = opportunitiesRunConflict(active ?? []);
|
|
15154
|
+
if (clash) throw new Error(clash);
|
|
15155
|
+
const repo = await detectRepo(opts.cwd);
|
|
15156
|
+
if (repo) await rememberClone(auth.client, project.id, repo.root);
|
|
15157
|
+
return runSkill("opportunities", { project: project.id, params: { ...params, ...repo ? { repo_path: repo.root } : {} }, cancelSignal: opts.cancelSignal });
|
|
15158
|
+
}
|
|
15159
|
+
async function rpc2(name, args, opts) {
|
|
15160
|
+
const auth = await authenticate();
|
|
15161
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15162
|
+
const { error } = await auth.client.rpc(name, { p_project_id: project.id, ...args });
|
|
15163
|
+
if (error) throw new Error(pgMessage(error));
|
|
15164
|
+
}
|
|
15165
|
+
async function confirmOpportunity(ref, opts) {
|
|
15166
|
+
await rpc2("confirm_opportunity", { p_ref: ref }, opts);
|
|
15167
|
+
return `${ref.toUpperCase()} is yours. A later run ranks around it and never replaces it.`;
|
|
15168
|
+
}
|
|
15169
|
+
async function setAsideOpportunity(ref, why, opts) {
|
|
15170
|
+
await rpc2("set_aside_opportunity", { p_ref: ref, p_reason: why ?? "not now" }, opts);
|
|
15171
|
+
return `${ref.toUpperCase()} is set aside for this objective, with the narrower problems under it. It comes back if the objective changes.`;
|
|
15172
|
+
}
|
|
15173
|
+
async function moveOpportunity(ref, place, opts) {
|
|
15174
|
+
const rank2 = Number(place);
|
|
15175
|
+
if (!Number.isInteger(rank2) || rank2 < 1) throw new Error("say where: nanopm opportunities move <ref> <place>, a place from 1");
|
|
15176
|
+
await rpc2("move_opportunity", { p_ref: ref, p_rank: rank2 }, opts);
|
|
15177
|
+
return `${ref.toUpperCase()} is at #${rank2} among its siblings, and the place is yours. Moved above a higher priority, it takes that priority.`;
|
|
15178
|
+
}
|
|
15179
|
+
async function rewordOpportunity(ref, problem, opts) {
|
|
15180
|
+
const text2 = problem?.trim().replace(/\s+/g, " ") ?? "";
|
|
15181
|
+
if (!text2) throw new Error('say it: nanopm opportunities reword <ref> "the problem, in your words"');
|
|
15182
|
+
if (text2.length > PROBLEM_MAX) throw new Error(`one line, under ${PROBLEM_MAX} characters`);
|
|
15183
|
+
await rpc2("restate_opportunity", { p_ref: ref, p_problem: text2 }, opts);
|
|
15184
|
+
return `Kept in your words: ${text2}`;
|
|
15185
|
+
}
|
|
15186
|
+
var AWAKE_MS = 12e4;
|
|
15187
|
+
async function awakeDaemon(client, userId) {
|
|
15188
|
+
const since = new Date(Date.now() - AWAKE_MS).toISOString();
|
|
15189
|
+
const [{ data }, local] = await Promise.all([
|
|
15190
|
+
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 }),
|
|
15191
|
+
localDaemonId()
|
|
15192
|
+
]);
|
|
15193
|
+
return data?.find((d) => d.id === local) ?? data?.[0] ?? null;
|
|
15194
|
+
}
|
|
15195
|
+
async function brainstorm(ref, opts) {
|
|
15196
|
+
const auth = await authenticate();
|
|
15197
|
+
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15198
|
+
const [lens, all, busy] = await Promise.all([lensOf2(auth.client, project.id), loadOpportunities(auth.client, project.id), workingOn(auth.client, project.id)]);
|
|
15199
|
+
const deep = Boolean(opts.deep);
|
|
15200
|
+
const plan = brainstormPlan(shownMap(all, lens?.key ?? null).shown, ref, deep, new Set(busy.keys()));
|
|
15201
|
+
const daemon = await awakeDaemon(auth.client, auth.userId);
|
|
15202
|
+
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.");
|
|
15203
|
+
const repo = deep ? await detectRepo(opts.cwd) : null;
|
|
15204
|
+
if (repo) await rememberClone(auth.client, project.id, repo.root);
|
|
15205
|
+
for (const params of plan.jobs) {
|
|
15206
|
+
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 });
|
|
15207
|
+
if (error) throw new Error(`could not queue the brainstorm: ${error.message}`);
|
|
15016
15208
|
}
|
|
15017
|
-
|
|
15018
|
-
|
|
15209
|
+
const n = plan.leaves.length;
|
|
15210
|
+
const minutes = deep ? DEEP_MINUTES * n : FLASH_MINUTES;
|
|
15211
|
+
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`;
|
|
15212
|
+
const how = deep ? `${DEPTH_WORD.deep}${n > 1 ? ", one after the other" : ""}` : `${DEPTH_WORD.quick}, a first pass`;
|
|
15213
|
+
const out = [`${deep ? "Going deeper on" : "Brainstorming"} ${what}: ${how}, about ${minutes} minutes on ${daemon.hostname}.`];
|
|
15214
|
+
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))}`);
|
|
15215
|
+
if (plan.busy) out.push(`${plan.busy} left out: Nano is already finding solutions to ${plan.busy === 1 ? "it" : "them"}.`);
|
|
15216
|
+
out.push("The solutions land on `nanopm roadmap`.");
|
|
15019
15217
|
return out.join("\n");
|
|
15020
15218
|
}
|
|
15021
15219
|
|
|
15022
|
-
// src/format/
|
|
15023
|
-
var
|
|
15220
|
+
// src/format/roadmap.ts
|
|
15221
|
+
var SHAPE = {
|
|
15024
15222
|
proposed: { mark: "\u25CB", colour: ANSI.amber, label: "proposed" },
|
|
15025
15223
|
accepted: { mark: "\u25D0", colour: ANSI.green, label: "accepted" },
|
|
15026
15224
|
building: { mark: "\u25D4", colour: ANSI.blue, label: "building" },
|
|
15027
|
-
done: { mark: "\u25CF", colour: ANSI.green, label: "done" },
|
|
15028
15225
|
rejected: { mark: "\u2298", colour: ANSI.red, label: "rejected" },
|
|
15226
|
+
done: { mark: "\u25CF", colour: ANSI.green, label: "done" },
|
|
15029
15227
|
dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
|
|
15030
15228
|
};
|
|
15031
|
-
var
|
|
15032
|
-
|
|
15033
|
-
|
|
15034
|
-
|
|
15035
|
-
|
|
15229
|
+
var WORK_KINDS = /* @__PURE__ */ new Set(["branch", "git", "url", "job"]);
|
|
15230
|
+
function renderRoadmapItem(item, opts, problem, builtBy) {
|
|
15231
|
+
const paint = painter(opts.color);
|
|
15232
|
+
const shape2 = SHAPE[item.status] ?? { mark: "\xB7", colour: "", label: item.status };
|
|
15233
|
+
const out = [`${paint(shape2.colour, shape2.mark)} ${paint(ANSI.bold, item.ref)} ${item.title}`];
|
|
15234
|
+
if (problem) out.push(paint(ANSI.dim, ` answers ${problem.ref} ${clip(problem.statement, Math.max(30, columns() - 20))}`));
|
|
15235
|
+
out.push("");
|
|
15236
|
+
for (const line of wrap(item.reasoning, Math.max(40, columns() - 4))) out.push(` ${line}`);
|
|
15237
|
+
out.push("");
|
|
15238
|
+
const facts = [
|
|
15239
|
+
["status", shape2.label + (item.outcome ? ` \xB7 ${item.outcome}${item.outcome_note ? `: ${item.outcome_note}` : ""}` : "")],
|
|
15240
|
+
["rank", String(item.rank)],
|
|
15241
|
+
["who", item.actor === "founder" ? "you" : "NanoPM"],
|
|
15242
|
+
["bets on", item.assumption ?? "\u2014"],
|
|
15243
|
+
["we'll know", item.eval ?? "\u2014"]
|
|
15244
|
+
];
|
|
15245
|
+
for (const [k, v] of facts) out.push(paint(ANSI.dim, ` ${k.padEnd(9)} ${v}`));
|
|
15246
|
+
const sources2 = item.sources ?? [];
|
|
15247
|
+
const restsOn = sources2.filter((s) => !WORK_KINDS.has(s.kind));
|
|
15248
|
+
if (restsOn.length) {
|
|
15249
|
+
out.push(paint(ANSI.dim, ` ${"rests on".padEnd(9)} ${restsOn.map((s) => `${s.kind}:${s.ref}`).join(", ")}`));
|
|
15250
|
+
}
|
|
15251
|
+
const where = renderWhere(sources2);
|
|
15252
|
+
if (where) out.push(paint(ANSI.dim, ` ${"where".padEnd(9)} ${where}${builtBy ? ` \xB7 built by ${builtBy}` : ""}`));
|
|
15253
|
+
return out.join("\n");
|
|
15254
|
+
}
|
|
15255
|
+
function renderWhere(sources2) {
|
|
15256
|
+
const branch = sources2.find((s) => s.kind === "branch")?.ref;
|
|
15257
|
+
const commit = sources2.find((s) => s.kind === "git")?.ref;
|
|
15258
|
+
const url = sources2.find((s) => s.kind === "url")?.ref;
|
|
15259
|
+
return [branch, commit, url].filter(Boolean).join(" \xB7 ");
|
|
15260
|
+
}
|
|
15261
|
+
var VERB = {
|
|
15262
|
+
want: { text: "wanted", colour: ANSI.amber },
|
|
15263
|
+
confirm: { text: "confirmed", colour: ANSI.green },
|
|
15264
|
+
drop: { text: "dropped", colour: ANSI.red },
|
|
15265
|
+
merge: { text: "merged into", colour: ANSI.blue },
|
|
15266
|
+
separate: { text: "kept apart", colour: ANSI.blue },
|
|
15267
|
+
weight: { text: "weighed", colour: ANSI.blue },
|
|
15268
|
+
area: { text: "filed", colour: ANSI.blue },
|
|
15269
|
+
accept: { text: "accepted", colour: ANSI.green },
|
|
15270
|
+
reject: { text: "rejected", colour: ANSI.red },
|
|
15271
|
+
first: { text: "put first", colour: ANSI.blue },
|
|
15272
|
+
done: { text: "did", colour: ANSI.green },
|
|
15273
|
+
dismiss: { text: "dismissed", colour: ANSI.red }
|
|
15036
15274
|
};
|
|
15037
|
-
|
|
15038
|
-
var SHOWN = /* @__PURE__ */ new Set(["proposed", "accepted", "building", "done"]);
|
|
15039
|
-
function renderTree(problems2, solutions, strategy, opts) {
|
|
15275
|
+
function renderDecisions(rows2, opts) {
|
|
15040
15276
|
const paint = painter(opts.color);
|
|
15277
|
+
if (!rows2.length) return "No decisions yet.";
|
|
15041
15278
|
const out = [];
|
|
15042
|
-
const
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
}
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
if (opts.alongside && !open.length) return "";
|
|
15051
|
-
if (!open.length && !shown.length) {
|
|
15052
|
-
out.push("No problems yet. `nanopm opportunities` shows what to work on.");
|
|
15053
|
-
return out.join("\n");
|
|
15054
|
-
}
|
|
15055
|
-
const byProblem = /* @__PURE__ */ new Map();
|
|
15056
|
-
for (const s of shown) byProblem.set(s.problem_id, [...byProblem.get(s.problem_id) ?? [], s]);
|
|
15057
|
-
const refWidth = Math.max(...[...open.map((p) => p.ref), ...shown.map((s) => s.ref)].map((r) => r.length), 5);
|
|
15058
|
-
for (const [area, rows2] of groupByArea(open)) {
|
|
15059
|
-
out.push(paint(ANSI.bold, area));
|
|
15060
|
-
for (const p of rows2) {
|
|
15061
|
-
const shape2 = PROBLEM_SHAPE[p.status] ?? { mark: "\xB7", colour: "" };
|
|
15062
|
-
const kind = p.kind === "founder" ? paint(ANSI.dim, " (founder)") : "";
|
|
15063
|
-
const proof = p.proofs ? `${p.proofs} proof${p.proofs === 1 ? "" : "s"}` : "no proof";
|
|
15064
|
-
const tail2 = `${renderImportance(p, opts)} ${paint(ANSI.dim, proof)}`;
|
|
15065
|
-
const head = ` ${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, p.ref.padEnd(refWidth))} `;
|
|
15066
|
-
out.push(head + clip(p.statement, Math.max(30, width - visibleLength(head) - visibleLength(tail2) - 2)) + kind + " " + tail2);
|
|
15067
|
-
const under = byProblem.get(p.id) ?? [];
|
|
15068
|
-
if (!under.length) {
|
|
15069
|
-
out.push(paint(ANSI.dim, ` ${" ".repeat(refWidth + 2)} no solution yet`));
|
|
15070
|
-
continue;
|
|
15071
|
-
}
|
|
15072
|
-
for (const s of under) out.push(...solutionLines(s, refWidth, width, paint));
|
|
15279
|
+
for (const row of rows2) {
|
|
15280
|
+
const when = row.created_at.slice(0, 10);
|
|
15281
|
+
const verb = VERB[row.kind] ?? { text: row.kind, colour: ANSI.blue };
|
|
15282
|
+
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() : "";
|
|
15283
|
+
const by = row.by ? paint(ANSI.dim, ` \xB7 by ${row.by}`) : "";
|
|
15284
|
+
out.push(`${paint(ANSI.dim, when)} ${paint(verb.colour, verb.text)} ${about}${by}`.trimEnd());
|
|
15285
|
+
if (row.body) {
|
|
15286
|
+
for (const line of wrap(row.body, 72)) out.push(paint(ANSI.dim, ` ${line}`));
|
|
15073
15287
|
}
|
|
15074
|
-
out.push("");
|
|
15075
|
-
}
|
|
15076
|
-
const waiting = shown.filter((s) => s.status === "proposed").length;
|
|
15077
|
-
if (waiting) {
|
|
15078
|
-
out.push(`${waiting} waiting on you.`);
|
|
15079
|
-
out.push(paint(ANSI.dim, ' nanopm accept <ref> \xB7 nanopm reject <ref> "why" \xB7 nanopm roadmap <ref> \xB7 nanopm roadmap --board'));
|
|
15080
|
-
} else if (!opts.alongside) {
|
|
15081
|
-
out.push(paint(ANSI.dim, "Nothing waiting on you."));
|
|
15082
15288
|
}
|
|
15083
|
-
return out.join("\n")
|
|
15289
|
+
return out.join("\n");
|
|
15084
15290
|
}
|
|
15085
|
-
|
|
15086
|
-
|
|
15087
|
-
|
|
15088
|
-
const STATE = { accepted: "chosen", building: "building", done: "done" };
|
|
15089
|
-
const tail2 = s.actor === "founder" ? "yours to do" : s.outcome ? s.outcome : s.status === "proposed" ? s.assumption ? clip(s.assumption, 40) : null : STATE[s.status] ?? null;
|
|
15090
|
-
const badge = tail2 ? paint(ANSI.dim, ` ${tail2}`) : "";
|
|
15091
|
-
const head = `${pad}${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, s.ref.padEnd(refWidth))} `;
|
|
15092
|
-
const lines = [head + clip(s.title, Math.max(30, width - visibleLength(head) - visibleLength(badge))) + badge];
|
|
15093
|
-
const reason2 = `${pad} ${" ".repeat(refWidth)} `;
|
|
15094
|
-
lines.push(paint(ANSI.dim, reason2 + clip(firstSentence(s.reasoning), Math.max(30, width - reason2.length))));
|
|
15095
|
-
return lines;
|
|
15291
|
+
var HUE = ["\x1B[34m", "\x1B[35m", "\x1B[36m", "\x1B[32m", "\x1B[95m"];
|
|
15292
|
+
function themesOf(tree) {
|
|
15293
|
+
return new Map(tree.map((b) => [b.id, { name: themeName(b), colour: HUE[themeHue(tree, b) - 1] }]));
|
|
15096
15294
|
}
|
|
15097
|
-
function
|
|
15098
|
-
|
|
15099
|
-
for (const p of problems2) {
|
|
15100
|
-
const area = p.area ?? NO_AREA;
|
|
15101
|
-
groups.set(area, [...groups.get(area) ?? [], p]);
|
|
15102
|
-
}
|
|
15103
|
-
const named = [...groups.entries()].filter(([a]) => a !== NO_AREA);
|
|
15104
|
-
const rest = groups.get(NO_AREA);
|
|
15105
|
-
return rest ? [...named, [NO_AREA, rest]] : named;
|
|
15295
|
+
function sizeWord(size, paint) {
|
|
15296
|
+
return size === "big" ? `${paint(ANSI.bold, "\u25AA")} ${SIZE_WORD.big}` : size === "small" ? paint(ANSI.dim, SIZE_WORD.small) : "";
|
|
15106
15297
|
}
|
|
15107
|
-
|
|
15108
|
-
{ status: "proposed", label: "Proposed" },
|
|
15109
|
-
{ status: "accepted", label: "Chosen" },
|
|
15110
|
-
{ status: "building", label: "Building" },
|
|
15111
|
-
{ status: "done", label: "Done" }
|
|
15112
|
-
];
|
|
15113
|
-
function renderBoard(problems2, solutions, opts) {
|
|
15298
|
+
function renderSolutionsBoard(board, opts) {
|
|
15114
15299
|
const paint = painter(opts.color);
|
|
15115
|
-
const out = [];
|
|
15116
|
-
const byId = new Map(problems2.map((p) => [p.id, p]));
|
|
15117
15300
|
const width = Math.max(40, columns());
|
|
15118
|
-
const
|
|
15119
|
-
|
|
15120
|
-
|
|
15121
|
-
|
|
15122
|
-
|
|
15123
|
-
|
|
15124
|
-
|
|
15125
|
-
|
|
15126
|
-
|
|
15127
|
-
|
|
15128
|
-
|
|
15129
|
-
|
|
15301
|
+
const themes = themesOf(board.tree);
|
|
15302
|
+
const out = [`${paint(ANSI.bold, "Roadmap")}${board.objective ? ` ${paint(ANSI.dim, `${board.objective.lever} \u2014 ${board.objective.sentence}`)}` : ""}`];
|
|
15303
|
+
const chips = board.tree.flatMap((b) => {
|
|
15304
|
+
const n = board.cards.filter((c) => c.branch.id === b.id).length;
|
|
15305
|
+
const t = themes.get(b.id);
|
|
15306
|
+
return n ? [`${paint(t.colour, "\u25CF")} ${t.name} ${paint(ANSI.dim, String(n))}`] : [];
|
|
15307
|
+
});
|
|
15308
|
+
const legend = chips.reduce((lines, chip) => {
|
|
15309
|
+
const last = lines.at(-1);
|
|
15310
|
+
if (last !== void 0 && visibleLength(last) + 3 + visibleLength(chip) <= width) lines[lines.length - 1] = `${last} ${chip}`;
|
|
15311
|
+
else lines.push(chip);
|
|
15312
|
+
return lines;
|
|
15313
|
+
}, []);
|
|
15314
|
+
out.push("", ...legend);
|
|
15315
|
+
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."));
|
|
15316
|
+
const refWidth = Math.max(5, ...board.cards.map((c) => c.pick.ref.length));
|
|
15317
|
+
const gutter = " ".repeat(refWidth + 4);
|
|
15318
|
+
const titleWidth = Math.min(Math.max(...board.cards.map((c) => c.pick.title.length)), width - gutter.length - 8);
|
|
15319
|
+
for (const h of HORIZONS) {
|
|
15320
|
+
const column = board.cards.filter((c) => c.horizon === h);
|
|
15321
|
+
out.push("", `${paint(ANSI.bold, HORIZON_WORD[h])} ${paint(ANSI.dim, String(column.length))}`);
|
|
15322
|
+
if (!column.length) out.push(paint(ANSI.dim, ` Move a card here: nanopm roadmap move <ref> ${h}`));
|
|
15323
|
+
for (const c of column) {
|
|
15324
|
+
const t = themes.get(c.branch.id);
|
|
15325
|
+
out.push(` ${paint(ANSI.dim, c.pick.ref.padEnd(refWidth))} ${paint(t.colour, "\u25CF")} ${paint(ANSI.dim, t.name)}`);
|
|
15326
|
+
out.push(`${gutter}${clip(c.pick.title, titleWidth).padEnd(titleWidth)} ${sizeWord(c.pick.size, paint)}`.trimEnd());
|
|
15327
|
+
const depth = board.working.get(c.leaf.id);
|
|
15328
|
+
if (depth) out.push(`${gutter}${paint(ANSI.blue, "\u25CF")} ${paint(ANSI.dim, WORK_WORD[depth])}`);
|
|
15130
15329
|
}
|
|
15131
|
-
out.push("");
|
|
15132
15330
|
}
|
|
15133
|
-
|
|
15331
|
+
if (board.earlier) out.push("", paint(ANSI.bold, "On the problem list"), "", board.earlier);
|
|
15332
|
+
out.push(
|
|
15333
|
+
"",
|
|
15334
|
+
paint(ANSI.dim, 'nanopm roadmap <ref> \xB7 nanopm roadmap move <ref> now|next|later [position] \xB7 nanopm reject <ref> ["why"]'),
|
|
15335
|
+
paint(ANSI.dim, "nanopm opportunities brainstorm <ref> [--deep]: solutions to another problem, or deeper ones")
|
|
15336
|
+
);
|
|
15337
|
+
return out.join("\n");
|
|
15134
15338
|
}
|
|
15135
|
-
|
|
15136
|
-
// src/format/opportunities.ts
|
|
15137
|
-
function renderOpportunities(rows2, lens, opts) {
|
|
15339
|
+
function renderSolutionSheet(s, at, opts) {
|
|
15138
15340
|
const paint = painter(opts.color);
|
|
15139
15341
|
const width = Math.max(40, columns());
|
|
15342
|
+
const theme = at.branch ? themesOf(at.tree).get(at.branch.id) : void 0;
|
|
15140
15343
|
const out = [];
|
|
15141
|
-
if (
|
|
15142
|
-
|
|
15143
|
-
out.push(paint(ANSI.dim,
|
|
15144
|
-
|
|
15145
|
-
|
|
15146
|
-
|
|
15147
|
-
|
|
15344
|
+
if (theme) out.push(`${paint(theme.colour, "\u25CF")} ${paint(ANSI.dim, theme.name)}`);
|
|
15345
|
+
const size = sizeWord(s.size, paint);
|
|
15346
|
+
out.push(`${paint(ANSI.dim, s.ref)} ${paint(ANSI.bold, s.title)}${size ? ` ${size}` : ""}`);
|
|
15347
|
+
const pad = " ".repeat(12);
|
|
15348
|
+
const heading = (text2) => out.push("", ` ${paint(ANSI.bold, text2)}`);
|
|
15349
|
+
heading("Problem we are solving");
|
|
15350
|
+
if (at.leaf) {
|
|
15351
|
+
const solves = wrap(at.leaf.problem, width - 6 - at.leaf.ref.length - 2);
|
|
15352
|
+
solves[solves.length - 1] += ` ${paint(ANSI.dim, at.leaf.ref)}`;
|
|
15353
|
+
for (const line of solves) out.push(` ${line}`);
|
|
15354
|
+
if (at.branch && at.branch.id !== at.leaf.id) {
|
|
15355
|
+
for (const line of wrap(`Part of: ${at.branch.problem} ${at.branch.ref}`, width - 6)) out.push(` ${paint(ANSI.dim, line)}`);
|
|
15356
|
+
}
|
|
15357
|
+
} else {
|
|
15358
|
+
out.push(paint(ANSI.dim, " The problem it answered is no longer on the map."));
|
|
15148
15359
|
}
|
|
15149
|
-
const
|
|
15150
|
-
|
|
15151
|
-
|
|
15152
|
-
|
|
15153
|
-
|
|
15154
|
-
|
|
15155
|
-
|
|
15156
|
-
|
|
15157
|
-
|
|
15158
|
-
|
|
15159
|
-
|
|
15160
|
-
out.push(
|
|
15161
|
-
const
|
|
15162
|
-
|
|
15163
|
-
|
|
15164
|
-
|
|
15165
|
-
|
|
15166
|
-
|
|
15167
|
-
|
|
15168
|
-
|
|
15169
|
-
|
|
15170
|
-
|
|
15171
|
-
|
|
15172
|
-
|
|
15360
|
+
const described = paragraphs(s.description);
|
|
15361
|
+
if (described.length) {
|
|
15362
|
+
heading("Proposed solution");
|
|
15363
|
+
described.forEach((p, i) => {
|
|
15364
|
+
if (i) out.push("");
|
|
15365
|
+
for (const line of wrap(p, width - 6)) out.push(` ${line}`);
|
|
15366
|
+
});
|
|
15367
|
+
}
|
|
15368
|
+
const changelog = parseChangelog(s.changelog);
|
|
15369
|
+
if (changelog) {
|
|
15370
|
+
heading("Future changelog");
|
|
15371
|
+
for (const line of wrap(changelog.headline, width - 6)) out.push(` ${paint(ANSI.bold, line)}`);
|
|
15372
|
+
for (const point of changelog.points) wrap(point, width - 8).forEach((line, i) => out.push(` ${i ? " " : "\u2022"} ${line}`));
|
|
15373
|
+
}
|
|
15374
|
+
if (s.steps.length) {
|
|
15375
|
+
heading("Plan");
|
|
15376
|
+
s.steps.forEach((step, i) => {
|
|
15377
|
+
const who = STEP_WHO_WORD[step.who] ?? step.who;
|
|
15378
|
+
wrap(step.do, width - pad.length - 8).forEach((line, j) => out.push(`${j ? pad : ` \u2610 ${who.padEnd(6)}`}${line}`));
|
|
15379
|
+
if (step.checks) out.push(pad + paint(ANSI.amber, i === 0 ? "checks the problem first" : "checks the problem"));
|
|
15380
|
+
});
|
|
15381
|
+
}
|
|
15382
|
+
const card = at.leaf ? at.cards.find((c) => c.leaf.id === at.leaf.id) : void 0;
|
|
15383
|
+
const others = card ? card.others.filter((o) => o.id !== s.id) : [];
|
|
15384
|
+
out.push("");
|
|
15385
|
+
if (s.status === "rejected") out.push(paint(ANSI.dim, " Thrown out."));
|
|
15386
|
+
else if (card?.pick.id === s.id) {
|
|
15387
|
+
const column = at.cards.filter((c) => c.horizon === card.horizon);
|
|
15388
|
+
const yours = s.placed_by === "founder" ? ", where you put it" : "";
|
|
15389
|
+
out.push(paint(ANSI.dim, ` On the board: ${HORIZON_WORD[card.horizon]}, ${column.indexOf(card) + 1} of ${column.length}${yours}.`));
|
|
15390
|
+
} else out.push(paint(ANSI.dim, card ? ` Not on the board: ${card.pick.ref} is this problem's card.` : " Not on the board."));
|
|
15391
|
+
if (others.length) out.push(paint(ANSI.dim, ` Other solutions to this problem: ${others.map((o) => o.ref).join(", ")}.`));
|
|
15392
|
+
if (s.status === "proposed") {
|
|
15393
|
+
const move = card?.pick.id === s.id ? `nanopm roadmap move ${s.ref} now|next|later [position] \xB7 ` : "";
|
|
15394
|
+
out.push(paint(ANSI.dim, ` ${move}nanopm reject ${s.ref} ["why"]`));
|
|
15173
15395
|
}
|
|
15174
|
-
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]`.'));
|
|
15175
15396
|
return out.join("\n");
|
|
15176
15397
|
}
|
|
15177
|
-
|
|
15178
|
-
// src/format/
|
|
15179
|
-
var
|
|
15180
|
-
|
|
15398
|
+
|
|
15399
|
+
// src/format/problems.ts
|
|
15400
|
+
var SHAPE2 = {
|
|
15401
|
+
candidate: { mark: "\u25CB", colour: ANSI.dim, label: "candidate" },
|
|
15402
|
+
confirmed: { mark: "\u25C9", colour: ANSI.amber, label: "confirmed" },
|
|
15403
|
+
addressed: { mark: "\u25CF", colour: ANSI.green, label: "addressed" },
|
|
15404
|
+
dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
|
|
15405
|
+
};
|
|
15406
|
+
function renderImportance(problem, opts) {
|
|
15407
|
+
const paint = painter(opts.color);
|
|
15408
|
+
if (problem.importance_basis === "evidence") {
|
|
15409
|
+
return paint(ANSI.bold, String(Math.round(problem.importance)).padStart(3));
|
|
15410
|
+
}
|
|
15411
|
+
const value = problem.importance;
|
|
15412
|
+
const word = value >= 67 ? "high" : value >= 34 ? "med" : value > 0 ? "low" : "\u2014";
|
|
15413
|
+
return paint(ANSI.dim, `~${word}`.padStart(5));
|
|
15414
|
+
}
|
|
15415
|
+
function renderProblems(problems2, duplicates, opts) {
|
|
15181
15416
|
const paint = painter(opts.color);
|
|
15182
|
-
|
|
15417
|
+
if (!problems2.length) {
|
|
15418
|
+
return [
|
|
15419
|
+
"No problems on the list yet.",
|
|
15420
|
+
"",
|
|
15421
|
+
paint(ANSI.dim, " nanopm review reads your data, then works out what they say about your users"),
|
|
15422
|
+
paint(ANSI.dim, " nanopm opportunities what to work on, for your objective")
|
|
15423
|
+
].join("\n");
|
|
15424
|
+
}
|
|
15425
|
+
const refWidth = Math.max(...problems2.map((p) => p.ref.length));
|
|
15183
15426
|
const out = [];
|
|
15184
|
-
const
|
|
15185
|
-
|
|
15186
|
-
|
|
15187
|
-
|
|
15188
|
-
|
|
15427
|
+
const map = groupProblemMap(problems2.map((p) => ({ ...p, parentRef: p.parent_ref })));
|
|
15428
|
+
const ordered = [...map.branches.flatMap((b) => [b.parent, ...b.children]), ...map.unplaced];
|
|
15429
|
+
for (const problem of ordered) {
|
|
15430
|
+
const shape2 = SHAPE2[problem.status] ?? { mark: "\xB7", colour: ANSI.dim, label: problem.status };
|
|
15431
|
+
const score = renderImportance(problem, opts);
|
|
15432
|
+
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))}`;
|
|
15433
|
+
out.push(head);
|
|
15434
|
+
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"}`));
|
|
15435
|
+
const indent = ` ${" ".repeat(refWidth)} `;
|
|
15436
|
+
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";
|
|
15437
|
+
out.push(paint(ANSI.dim, indent + clip(tail, Math.max(40, columns() - indent.length))));
|
|
15189
15438
|
}
|
|
15190
|
-
const
|
|
15191
|
-
|
|
15192
|
-
const lead = n.level === 1 ? `${n.emoji ?? " "} ` : `${MARK[n.provenance]} `;
|
|
15193
|
-
const where = n.provenance === "inferred" ? paint(ANSI.dim, ` guess ${n.confidence ?? 0}%`) : n.sources.length ? paint(ANSI.dim, ` ${n.sources.length} source${n.sources.length === 1 ? "" : "s"}`) : "";
|
|
15194
|
-
const state = n.status === "dropped" ? paint(ANSI.dim, " dropped") : n.status === "confirmed" || n.author_kind === "user" ? "" : paint(ANSI.amber, " to review");
|
|
15195
|
-
const label = n.status === "dropped" ? paint(ANSI.dim, n.label) : n.level === 1 ? paint(ANSI.bold, n.label) : n.label;
|
|
15196
|
-
const body = wrap(`${label}${where}${state}${n.proposal ? paint(ANSI.amber, " \xB7 NanoPM suggests a new wording") : ""}`, width - indent.length - 12);
|
|
15197
|
-
out.push(`${indent}${lead}${paint(ANSI.dim, n.ref.padEnd(8))} ${body[0] ?? ""}`);
|
|
15198
|
-
for (const more of body.slice(1)) out.push(`${indent} ${more}`);
|
|
15199
|
-
for (const c of n.children) line(c, depth + 1);
|
|
15200
|
-
};
|
|
15201
|
-
for (const need of tree) {
|
|
15439
|
+
const unproven = problems2.filter((p) => p.proofs === 0).length;
|
|
15440
|
+
if (unproven) {
|
|
15202
15441
|
out.push("");
|
|
15203
|
-
|
|
15442
|
+
out.push(
|
|
15443
|
+
paint(
|
|
15444
|
+
ANSI.dim,
|
|
15445
|
+
`${unproven} of ${problems2.length} rest on judgement, not proof. A tilde marks those.`
|
|
15446
|
+
)
|
|
15447
|
+
);
|
|
15204
15448
|
}
|
|
15205
|
-
|
|
15206
|
-
|
|
15449
|
+
if (duplicates.length) {
|
|
15450
|
+
out.push("");
|
|
15451
|
+
for (const pair of duplicates.slice(0, 3)) {
|
|
15452
|
+
const why = pair.flagged ? `NanoPM is not sure ${pair.a} and ${pair.b} are different problems` : `${pair.a} and ${pair.b} read alike`;
|
|
15453
|
+
out.push(paint(ANSI.amber, `? ${why}`));
|
|
15454
|
+
}
|
|
15455
|
+
out.push(paint(ANSI.dim, ` nanopm decisions why \xB7 nanopm problems merge ${duplicates[0].b} ${duplicates[0].a}`));
|
|
15456
|
+
}
|
|
15457
|
+
if (opts.manualMerges !== void 0) {
|
|
15458
|
+
out.push("");
|
|
15459
|
+
out.push(
|
|
15460
|
+
paint(
|
|
15461
|
+
ANSI.dim,
|
|
15462
|
+
`${opts.manualMerges} merge${opts.manualMerges === 1 ? "" : "s"} you have had to make by hand.`
|
|
15463
|
+
)
|
|
15464
|
+
);
|
|
15465
|
+
}
|
|
15466
|
+
out.push("");
|
|
15467
|
+
out.push(paint(ANSI.dim, " nanopm problems <ref> \xB7 nanopm problems merge <from> <into> \xB7 nanopm problems weight <ref> <0-100>"));
|
|
15207
15468
|
return out.join("\n");
|
|
15208
15469
|
}
|
|
15209
|
-
|
|
15210
|
-
|
|
15211
|
-
|
|
15212
|
-
const
|
|
15213
|
-
|
|
15214
|
-
|
|
15215
|
-
|
|
15216
|
-
|
|
15217
|
-
|
|
15218
|
-
|
|
15219
|
-
|
|
15220
|
-
if (
|
|
15221
|
-
|
|
15222
|
-
|
|
15223
|
-
|
|
15224
|
-
|
|
15225
|
-
}
|
|
15226
|
-
|
|
15227
|
-
|
|
15228
|
-
|
|
15229
|
-
|
|
15230
|
-
|
|
15231
|
-
|
|
15232
|
-
|
|
15233
|
-
|
|
15234
|
-
|
|
15235
|
-
|
|
15236
|
-
|
|
15237
|
-
const
|
|
15238
|
-
|
|
15239
|
-
|
|
15240
|
-
|
|
15241
|
-
|
|
15242
|
-
|
|
15243
|
-
|
|
15244
|
-
|
|
15245
|
-
|
|
15246
|
-
|
|
15247
|
-
|
|
15248
|
-
|
|
15249
|
-
|
|
15250
|
-
|
|
15251
|
-
|
|
15252
|
-
|
|
15253
|
-
|
|
15254
|
-
|
|
15255
|
-
|
|
15256
|
-
|
|
15257
|
-
|
|
15258
|
-
|
|
15259
|
-
|
|
15260
|
-
|
|
15261
|
-
|
|
15262
|
-
|
|
15263
|
-
if (deeper) return deeper;
|
|
15264
|
-
}
|
|
15265
|
-
return null;
|
|
15266
|
-
};
|
|
15267
|
-
return walk(tree) ?? [ref];
|
|
15268
|
-
};
|
|
15269
|
-
const clash = needsRunConflict((active ?? []).map((j) => runScopeOf(j.params)), { persona: persona.slug, scope }, { primary: list.find((p) => p.primary)?.slug ?? list[0]?.slug ?? null, lineage, name: (s) => list.find((p) => p.slug === s)?.name ?? s });
|
|
15270
|
-
if (clash) throw new Error(clash);
|
|
15271
|
-
const repo = await detectRepo(opts.cwd);
|
|
15272
|
-
if (repo) await rememberClone(auth.client, project.id, repo.root);
|
|
15273
|
-
const params = {
|
|
15274
|
-
persona: persona.slug,
|
|
15275
|
-
...scope === "map" ? {} : { scope },
|
|
15276
|
-
depth: opts.short ? "short" : "deep",
|
|
15277
|
-
...opts.note?.trim() ? { note: opts.note.trim().slice(0, 600) } : {},
|
|
15278
|
-
...repo ? { repo_path: repo.root } : {}
|
|
15279
|
-
};
|
|
15280
|
-
return runSkill("needs", { project: project.id, params, cancelSignal: opts.cancelSignal });
|
|
15281
|
-
}
|
|
15282
|
-
async function confirmNeed(ref, opts) {
|
|
15283
|
-
const auth = await authenticate();
|
|
15284
|
-
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15285
|
-
const { data, error } = await auth.client.from("need_nodes").update({ status: "confirmed" }).eq("project_id", project.id).eq("ref", ref.toUpperCase()).eq("status", "proposed").select("ref, label");
|
|
15286
|
-
if (error) throw new Error(pgMessage(error));
|
|
15287
|
-
if (!data?.length) throw new Error(`${ref} is not a node waiting on your word. \`nanopm needs\` shows which are.`);
|
|
15288
|
-
return `Confirmed ${data[0].ref}: ${data[0].label}. A run proposes beside it from now on, never over it.`;
|
|
15289
|
-
}
|
|
15290
|
-
async function dropNeed(ref, why, opts) {
|
|
15291
|
-
const auth = await authenticate();
|
|
15292
|
-
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15293
|
-
const { data, error } = await auth.client.from("need_nodes").update({ status: "dropped", drop_reason: why?.trim().slice(0, 400) || "not true of them" }).eq("project_id", project.id).eq("ref", ref.toUpperCase()).in("status", ["proposed", "confirmed"]).select("ref, label");
|
|
15294
|
-
if (error) throw new Error(pgMessage(error));
|
|
15295
|
-
if (!data?.length) throw new Error(`${ref} is not on the map any more.`);
|
|
15296
|
-
return `Dropped ${data[0].ref}: ${data[0].label}. It stays on the record, and NanoPM will not propose it again in the same place.`;
|
|
15297
|
-
}
|
|
15298
|
-
|
|
15299
|
-
// src/commands/opportunities.ts
|
|
15300
|
-
async function lensOf2(client, projectId) {
|
|
15301
|
-
const { data } = await client.from("memory").select("id, key, content").eq("project_id", projectId).eq("topic", GOALS_TOPIC).in("key", [OBJECTIVE_KEY, DEFINITION_KEY]).is("superseded_by", null);
|
|
15302
|
-
const rows2 = data ?? [];
|
|
15303
|
-
const aim = rows2.find((r) => r.key === OBJECTIVE_KEY);
|
|
15304
|
-
const m = aim ? /^Lever:\s*([a-z]+)\.\s*(.*)$/s.exec(aim.content.trim()) : null;
|
|
15305
|
-
const lever = m?.[1];
|
|
15306
|
-
if (!aim || !lever || !OUTCOME_LEVERS.includes(lever)) return null;
|
|
15307
|
-
const def = rows2.find((r) => r.key === DEFINITION_KEY);
|
|
15308
|
-
const parsed = def ? parseDefinition(def.content) : null;
|
|
15309
|
-
const sentence2 = m[2].trim();
|
|
15310
|
-
const definition = parsed && parsed.lever === lever ? parsed.sentence : null;
|
|
15311
|
-
return { id: aim.id, lever, sentence: sentence2, definition, key: lensKey(lever, sentence2, definition) };
|
|
15312
|
-
}
|
|
15313
|
-
async function loadOpportunities(client, projectId) {
|
|
15314
|
-
const [standing, replaced] = await Promise.all([
|
|
15315
|
-
client.from("opportunities").select(OPPORTUNITY_COLUMNS).eq("project_id", projectId).neq("status", "replaced"),
|
|
15316
|
-
client.from("opportunities").select(REPLACED_COLUMNS).eq("project_id", projectId).eq("status", "replaced")
|
|
15317
|
-
]);
|
|
15318
|
-
const error = standing.error ?? replaced.error;
|
|
15319
|
-
if (error) throw new Error(`could not read the opportunities: ${error.message}`);
|
|
15320
|
-
return [...standing.data ?? [], ...replaced.data ?? []];
|
|
15321
|
-
}
|
|
15322
|
-
async function workingOn(client, projectId) {
|
|
15323
|
-
const { data } = await client.from("jobs").select("params").eq("project_id", projectId).eq("skill", SOLUTIONS_SKILL).in("status", [...ACTIVE_STATUSES]);
|
|
15324
|
-
return leavesAtWork((data ?? []).map((j) => j.params));
|
|
15325
|
-
}
|
|
15326
|
-
async function opportunities(opts) {
|
|
15327
|
-
const auth = await authenticate();
|
|
15328
|
-
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15329
|
-
const [lens, all, personas2] = await Promise.all([lensOf2(auth.client, project.id), loadOpportunities(auth.client, project.id), people(auth.client, project.id)]);
|
|
15330
|
-
const { shown, drawnFor } = shownMap(all, lens?.key ?? null);
|
|
15331
|
-
const names = new Map(personas2.map((p) => [p.slug, p.name]));
|
|
15332
|
-
return renderOpportunities(shown, lens, { color: opts.color ?? useColor(), previous: drawnFor === "previous", names });
|
|
15333
|
-
}
|
|
15334
|
-
async function rankOpportunities(opts) {
|
|
15335
|
-
const auth = await authenticate();
|
|
15336
|
-
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15337
|
-
const list = (s) => s ? s.split(",").map((x) => x.trim()).filter(Boolean) : void 0;
|
|
15338
|
-
const sources2 = opts.use || opts.skip ? { ...opts.use ? { use: list(opts.use) } : {}, ...opts.skip ? { skip: list(opts.skip) } : {} } : void 0;
|
|
15339
|
-
const params = {
|
|
15340
|
-
depth: opts.deep ? "deep" : "short",
|
|
15341
|
-
...opts.note?.trim() ? { note: opts.note.trim().slice(0, OPPORTUNITIES_NOTE_MAX) } : {},
|
|
15342
|
-
...sources2 ? { sources: sources2 } : {}
|
|
15343
|
-
};
|
|
15344
|
-
const checked = opportunitiesAsk(params);
|
|
15345
|
-
if (!checked.ok) throw new Error(checked.why);
|
|
15346
|
-
const { data: active } = await auth.client.from("jobs").select("id").eq("project_id", project.id).eq("skill", "opportunities").in("status", [...ACTIVE_STATUSES]);
|
|
15347
|
-
const clash = opportunitiesRunConflict(active ?? []);
|
|
15348
|
-
if (clash) throw new Error(clash);
|
|
15349
|
-
const repo = await detectRepo(opts.cwd);
|
|
15350
|
-
if (repo) await rememberClone(auth.client, project.id, repo.root);
|
|
15351
|
-
return runSkill("opportunities", { project: project.id, params: { ...params, ...repo ? { repo_path: repo.root } : {} }, cancelSignal: opts.cancelSignal });
|
|
15352
|
-
}
|
|
15353
|
-
async function rpc2(name, args, opts) {
|
|
15354
|
-
const auth = await authenticate();
|
|
15355
|
-
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15356
|
-
const { error } = await auth.client.rpc(name, { p_project_id: project.id, ...args });
|
|
15357
|
-
if (error) throw new Error(pgMessage(error));
|
|
15358
|
-
}
|
|
15359
|
-
async function confirmOpportunity(ref, opts) {
|
|
15360
|
-
await rpc2("confirm_opportunity", { p_ref: ref }, opts);
|
|
15361
|
-
return `${ref.toUpperCase()} is yours. A later run ranks around it and never replaces it.`;
|
|
15362
|
-
}
|
|
15363
|
-
async function setAsideOpportunity(ref, why, opts) {
|
|
15364
|
-
await rpc2("set_aside_opportunity", { p_ref: ref, p_reason: why ?? "not now" }, opts);
|
|
15365
|
-
return `${ref.toUpperCase()} is set aside for this objective, with the narrower problems under it. It comes back if the objective changes.`;
|
|
15470
|
+
function renderProblem(problem, evidence2, addressedBy, opts) {
|
|
15471
|
+
const paint = painter(opts.color);
|
|
15472
|
+
const shape2 = SHAPE2[problem.status] ?? { mark: "\xB7", colour: ANSI.dim, label: problem.status };
|
|
15473
|
+
const width = columns();
|
|
15474
|
+
const out = [];
|
|
15475
|
+
out.push(`${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, problem.ref)} ${paint(ANSI.bold, shape2.label)}`);
|
|
15476
|
+
out.push("");
|
|
15477
|
+
for (const line of wrap(problem.statement, width)) out.push(line);
|
|
15478
|
+
out.push("");
|
|
15479
|
+
const facts = [];
|
|
15480
|
+
const m = problem.mapping ?? null;
|
|
15481
|
+
if (m) {
|
|
15482
|
+
const story = problemStory(m, { proofs: problem.proofs, kinds: problem.proof_kinds ?? [], provenance: problem.provenance ?? "inferred", authorKind: problem.author_kind ?? "nanopm", createdAt: problem.created_at ?? "" });
|
|
15483
|
+
out.push(...wrap(story.story, width));
|
|
15484
|
+
out.push("");
|
|
15485
|
+
out.push(...wrap(story.doubt, width).map((l) => paint(ANSI.dim, l)));
|
|
15486
|
+
out.push(...wrap(story.assumes ? `${story.test} ${story.assumes}` : story.test, width).map((l) => paint(ANSI.dim, l)));
|
|
15487
|
+
out.push("");
|
|
15488
|
+
out.push(paint(ANSI.dim, "how I got here"));
|
|
15489
|
+
facts.push(["kind", `${m.level === 1 ? "a need" : "a difficulty"} \xB7 ${m.knowledge} \xB7 ${m.origin === "expert_inference" ? "expert inference" : "source analysis"}`]);
|
|
15490
|
+
if (m.parent_reason) facts.push(["grouping", m.parent_reason]);
|
|
15491
|
+
}
|
|
15492
|
+
if (problem.segment) facts.push(["who", problem.segment]);
|
|
15493
|
+
facts.push([
|
|
15494
|
+
"weight",
|
|
15495
|
+
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}` : ""}`
|
|
15496
|
+
]);
|
|
15497
|
+
if (problem.parent_ref) facts.push(["part of", problem.parent_ref]);
|
|
15498
|
+
for (const [label, value] of facts) {
|
|
15499
|
+
out.push(`${paint(ANSI.dim, label.padEnd(9))}${clip(value, width - 9)}`);
|
|
15500
|
+
}
|
|
15501
|
+
if (evidence2.length) {
|
|
15502
|
+
out.push("");
|
|
15503
|
+
out.push(paint(ANSI.dim, "sources"));
|
|
15504
|
+
for (const item of evidence2) {
|
|
15505
|
+
const when = (item.occurred_at ?? item.created_at).slice(0, 10);
|
|
15506
|
+
const where = item.metric_key ?? item.source_ref ?? item.kind;
|
|
15507
|
+
for (const line of wrap(item.body, width - 2)) out.push(` ${line}`);
|
|
15508
|
+
out.push(paint(ANSI.dim, ` \u2014 ${item.relation ?? "supports"} \xB7 ${item.kind} \xB7 ${where} \xB7 ${when}`));
|
|
15509
|
+
}
|
|
15510
|
+
} else {
|
|
15511
|
+
out.push("");
|
|
15512
|
+
out.push(paint(ANSI.dim, "No proof yet. This ranking is a judgement, and the next review may settle it."));
|
|
15513
|
+
}
|
|
15514
|
+
if (addressedBy.length) {
|
|
15515
|
+
out.push("");
|
|
15516
|
+
out.push(paint(ANSI.dim, "addressed by"));
|
|
15517
|
+
for (const item of addressedBy) {
|
|
15518
|
+
out.push(` ${paint(ANSI.dim, item.ref)} ${clip(item.title, width - 12)} ${paint(ANSI.dim, `(${item.status})`)}`);
|
|
15519
|
+
}
|
|
15520
|
+
}
|
|
15521
|
+
out.push("");
|
|
15522
|
+
out.push(paint(ANSI.dim, " nanopm problems \xB7 nanopm roadmap"));
|
|
15523
|
+
return out.join("\n");
|
|
15366
15524
|
}
|
|
15367
|
-
|
|
15368
|
-
|
|
15369
|
-
|
|
15370
|
-
|
|
15371
|
-
|
|
15525
|
+
|
|
15526
|
+
// src/format/tree.ts
|
|
15527
|
+
var SOLUTION_SHAPE = {
|
|
15528
|
+
proposed: { mark: "\u25CB", colour: ANSI.amber, label: "proposed" },
|
|
15529
|
+
accepted: { mark: "\u25D0", colour: ANSI.green, label: "accepted" },
|
|
15530
|
+
building: { mark: "\u25D4", colour: ANSI.blue, label: "building" },
|
|
15531
|
+
done: { mark: "\u25CF", colour: ANSI.green, label: "done" },
|
|
15532
|
+
rejected: { mark: "\u2298", colour: ANSI.red, label: "rejected" },
|
|
15533
|
+
dropped: { mark: "\u2298", colour: ANSI.dim, label: "dropped" }
|
|
15534
|
+
};
|
|
15535
|
+
var PROBLEM_SHAPE = {
|
|
15536
|
+
candidate: { mark: "\u25CB", colour: ANSI.dim },
|
|
15537
|
+
confirmed: { mark: "\u25C9", colour: ANSI.amber },
|
|
15538
|
+
addressed: { mark: "\u25CF", colour: ANSI.green },
|
|
15539
|
+
dropped: { mark: "\u2298", colour: ANSI.dim }
|
|
15540
|
+
};
|
|
15541
|
+
var NO_AREA = "no area yet";
|
|
15542
|
+
var SHOWN = /* @__PURE__ */ new Set(["proposed", "accepted", "building", "done"]);
|
|
15543
|
+
function renderTree(problems2, solutions, strategy, opts) {
|
|
15544
|
+
const paint = painter(opts.color);
|
|
15545
|
+
const out = [];
|
|
15546
|
+
const width = Math.max(40, columns());
|
|
15547
|
+
if (strategy) {
|
|
15548
|
+
out.push(paint(ANSI.bold, "Strategy"));
|
|
15549
|
+
for (const line of wrap(strategy, width - 4)) out.push(` ${line}`);
|
|
15550
|
+
out.push("");
|
|
15551
|
+
}
|
|
15552
|
+
const shown = solutions.filter((s) => SHOWN.has(s.status));
|
|
15553
|
+
const open = problems2.filter((p) => p.status !== "dropped" && (!opts.alongside || shown.some((s) => s.problem_id === p.id)));
|
|
15554
|
+
if (opts.alongside && !open.length) return "";
|
|
15555
|
+
if (!open.length && !shown.length) {
|
|
15556
|
+
out.push("No problems yet. `nanopm opportunities` shows what to work on.");
|
|
15557
|
+
return out.join("\n");
|
|
15558
|
+
}
|
|
15559
|
+
const byProblem = /* @__PURE__ */ new Map();
|
|
15560
|
+
for (const s of shown) byProblem.set(s.problem_id, [...byProblem.get(s.problem_id) ?? [], s]);
|
|
15561
|
+
const refWidth = Math.max(...[...open.map((p) => p.ref), ...shown.map((s) => s.ref)].map((r) => r.length), 5);
|
|
15562
|
+
for (const [area, rows2] of groupByArea(open)) {
|
|
15563
|
+
out.push(paint(ANSI.bold, area));
|
|
15564
|
+
for (const p of rows2) {
|
|
15565
|
+
const shape2 = PROBLEM_SHAPE[p.status] ?? { mark: "\xB7", colour: "" };
|
|
15566
|
+
const kind = p.kind === "founder" ? paint(ANSI.dim, " (founder)") : "";
|
|
15567
|
+
const proof = p.proofs ? `${p.proofs} proof${p.proofs === 1 ? "" : "s"}` : "no proof";
|
|
15568
|
+
const tail = `${renderImportance(p, opts)} ${paint(ANSI.dim, proof)}`;
|
|
15569
|
+
const head = ` ${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, p.ref.padEnd(refWidth))} `;
|
|
15570
|
+
out.push(head + clip(p.statement, Math.max(30, width - visibleLength(head) - visibleLength(tail) - 2)) + kind + " " + tail);
|
|
15571
|
+
const under = byProblem.get(p.id) ?? [];
|
|
15572
|
+
if (!under.length) {
|
|
15573
|
+
out.push(paint(ANSI.dim, ` ${" ".repeat(refWidth + 2)} no solution yet`));
|
|
15574
|
+
continue;
|
|
15575
|
+
}
|
|
15576
|
+
for (const s of under) out.push(...solutionLines(s, refWidth, width, paint));
|
|
15577
|
+
}
|
|
15578
|
+
out.push("");
|
|
15579
|
+
}
|
|
15580
|
+
const waiting = shown.filter((s) => s.status === "proposed").length;
|
|
15581
|
+
if (waiting) {
|
|
15582
|
+
out.push(`${waiting} waiting on you.`);
|
|
15583
|
+
out.push(paint(ANSI.dim, ' nanopm accept <ref> \xB7 nanopm reject <ref> "why" \xB7 nanopm roadmap <ref> \xB7 nanopm roadmap --board'));
|
|
15584
|
+
} else if (!opts.alongside) {
|
|
15585
|
+
out.push(paint(ANSI.dim, "Nothing waiting on you."));
|
|
15586
|
+
}
|
|
15587
|
+
return out.join("\n").trimEnd();
|
|
15372
15588
|
}
|
|
15373
|
-
|
|
15374
|
-
const
|
|
15375
|
-
|
|
15376
|
-
|
|
15377
|
-
|
|
15378
|
-
|
|
15589
|
+
function solutionLines(s, refWidth, width, paint, indent = 6) {
|
|
15590
|
+
const shape2 = SOLUTION_SHAPE[s.status] ?? { mark: "\xB7", colour: "", label: s.status };
|
|
15591
|
+
const pad = " ".repeat(indent);
|
|
15592
|
+
const STATE = { accepted: "chosen", building: "building", done: "done" };
|
|
15593
|
+
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;
|
|
15594
|
+
const badge = tail ? paint(ANSI.dim, ` ${tail}`) : "";
|
|
15595
|
+
const head = `${pad}${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, s.ref.padEnd(refWidth))} `;
|
|
15596
|
+
const lines = [head + clip(s.title, Math.max(30, width - visibleLength(head) - visibleLength(badge))) + badge];
|
|
15597
|
+
const reason2 = `${pad} ${" ".repeat(refWidth)} `;
|
|
15598
|
+
lines.push(paint(ANSI.dim, reason2 + clip(firstSentence(s.reasoning), Math.max(30, width - reason2.length))));
|
|
15599
|
+
return lines;
|
|
15379
15600
|
}
|
|
15380
|
-
|
|
15381
|
-
|
|
15382
|
-
const
|
|
15383
|
-
|
|
15384
|
-
|
|
15385
|
-
|
|
15386
|
-
]);
|
|
15387
|
-
|
|
15601
|
+
function groupByArea(problems2) {
|
|
15602
|
+
const groups = /* @__PURE__ */ new Map();
|
|
15603
|
+
for (const p of problems2) {
|
|
15604
|
+
const area = p.area ?? NO_AREA;
|
|
15605
|
+
groups.set(area, [...groups.get(area) ?? [], p]);
|
|
15606
|
+
}
|
|
15607
|
+
const named = [...groups.entries()].filter(([a]) => a !== NO_AREA);
|
|
15608
|
+
const rest = groups.get(NO_AREA);
|
|
15609
|
+
return rest ? [...named, [NO_AREA, rest]] : named;
|
|
15388
15610
|
}
|
|
15389
|
-
|
|
15390
|
-
|
|
15391
|
-
|
|
15392
|
-
|
|
15393
|
-
|
|
15394
|
-
|
|
15395
|
-
|
|
15396
|
-
|
|
15397
|
-
const
|
|
15398
|
-
|
|
15399
|
-
|
|
15400
|
-
|
|
15401
|
-
|
|
15611
|
+
var BOARD_COLUMNS = [
|
|
15612
|
+
{ status: "proposed", label: "Proposed" },
|
|
15613
|
+
{ status: "accepted", label: "Chosen" },
|
|
15614
|
+
{ status: "building", label: "Building" },
|
|
15615
|
+
{ status: "done", label: "Done" }
|
|
15616
|
+
];
|
|
15617
|
+
function renderBoard(problems2, solutions, opts) {
|
|
15618
|
+
const paint = painter(opts.color);
|
|
15619
|
+
const out = [];
|
|
15620
|
+
const byId = new Map(problems2.map((p) => [p.id, p]));
|
|
15621
|
+
const width = Math.max(40, columns());
|
|
15622
|
+
const refWidth = Math.max(...solutions.map((s) => s.ref.length), 5);
|
|
15623
|
+
for (const col of BOARD_COLUMNS) {
|
|
15624
|
+
const rows2 = solutions.filter((s) => s.status === col.status);
|
|
15625
|
+
out.push(`${paint(ANSI.bold, col.label)} ${paint(ANSI.dim, String(rows2.length))}`);
|
|
15626
|
+
if (!rows2.length) out.push(paint(ANSI.dim, " \u2014"));
|
|
15627
|
+
for (const s of rows2) {
|
|
15628
|
+
const shape2 = SOLUTION_SHAPE[s.status];
|
|
15629
|
+
const problem = byId.get(s.problem_id);
|
|
15630
|
+
const head = ` ${paint(shape2.colour, shape2.mark)} ${paint(ANSI.dim, s.ref.padEnd(refWidth))} `;
|
|
15631
|
+
out.push(head + clip(s.title, Math.max(30, width - visibleLength(head))));
|
|
15632
|
+
const under = [problem ? `${problem.ref} ${clip(problem.statement, 50)}` : "its problem was dropped", s.assumption ? clip(s.assumption, 40) : null].filter(Boolean).join(" \xB7 ");
|
|
15633
|
+
out.push(paint(ANSI.dim, ` ${" ".repeat(refWidth + 2)} ${under}`));
|
|
15634
|
+
}
|
|
15635
|
+
out.push("");
|
|
15402
15636
|
}
|
|
15403
|
-
|
|
15404
|
-
const minutes = deep ? DEEP_MINUTES * n : FLASH_MINUTES;
|
|
15405
|
-
const what = plan.problem.children.length ? `the ${n} narrower problem${n === 1 ? "" : "s"} under ${plan.problem.ref}` : `${plan.problem.ref} \u201C${clip(plan.problem.problem, 60)}\u201D`;
|
|
15406
|
-
const how = deep ? `${DEPTH_WORD.deep}${n > 1 ? ", one after the other" : ""}` : `${DEPTH_WORD.quick}, a first pass`;
|
|
15407
|
-
const out = [`${deep ? "Going deeper on" : "Brainstorming"} ${what}: ${how}, about ${minutes} minutes on ${daemon.hostname}.`];
|
|
15408
|
-
if (plan.problem.children.length) for (const l of plan.leaves) out.push(` ${l.ref.padEnd(8)} ${clip(l.problem, Math.max(30, columns() - 11))}`);
|
|
15409
|
-
if (plan.busy) out.push(`${plan.busy} left out: Nano is already finding solutions to ${plan.busy === 1 ? "it" : "them"}.`);
|
|
15410
|
-
out.push("The solutions land on `nanopm roadmap`.");
|
|
15411
|
-
return out.join("\n");
|
|
15637
|
+
return out.join("\n").trimEnd();
|
|
15412
15638
|
}
|
|
15413
15639
|
|
|
15414
15640
|
// src/commands/roadmap.ts
|
|
@@ -15568,8 +15794,8 @@ async function decide(kind, ref, body, opts) {
|
|
|
15568
15794
|
const row = Array.isArray(data) ? data[0] : void 0;
|
|
15569
15795
|
if (!row) throw new Error(`nothing changed for ${ref}`);
|
|
15570
15796
|
if (onMap) {
|
|
15571
|
-
const
|
|
15572
|
-
const after = !card ? "" :
|
|
15797
|
+
const next = card?.others[0];
|
|
15798
|
+
const after = !card ? "" : next ? ` ${next.ref} \u201C${next.title}\u201D takes its card in ${HORIZON_WORD[card.horizon]}.` : ` Its card leaves the board: nothing else answers ${card.leaf.ref} yet. \`nanopm opportunities brainstorm ${card.leaf.ref}\` finds more.`;
|
|
15573
15799
|
return `Thrown out: ${row.ref}${body?.trim() ? ` \u2014 ${body.trim()}` : ""}.${after}`;
|
|
15574
15800
|
}
|
|
15575
15801
|
const verb = kind === "accept" ? "Accepted" : "Rejected";
|
|
@@ -15586,123 +15812,65 @@ function normalizeRef(input, items) {
|
|
|
15586
15812
|
return given;
|
|
15587
15813
|
}
|
|
15588
15814
|
|
|
15589
|
-
// src/commands/
|
|
15590
|
-
|
|
15815
|
+
// src/commands/build.ts
|
|
15816
|
+
var execFileAsync9 = promisify9(execFile10);
|
|
15817
|
+
async function build(ref, opts) {
|
|
15591
15818
|
const { renderer, out } = rendererFor(opts.out);
|
|
15592
15819
|
const auth = await authenticate();
|
|
15593
15820
|
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15594
|
-
const autonomy2 = await readAutonomy(auth.client, project.id);
|
|
15595
|
-
if (autonomy2 === "propose") {
|
|
15596
|
-
throw new Error(
|
|
15597
|
-
`${project.name} is on "propose", so NanoPM may look but not touch.
|
|
15598
|
-
nanopm autonomy execute_after_validation to let it change files, asking you first`
|
|
15599
|
-
);
|
|
15600
|
-
}
|
|
15601
|
-
const { data: items, error } = await auth.client.from("solutions").select("id, ref, title, rank, status").eq("project_id", project.id).is("superseded_by", null).eq("status", "accepted").eq("actor", "nanopm").order("rank");
|
|
15602
|
-
if (error) throw new Error(`could not read the solutions: ${error.message}`);
|
|
15603
|
-
const accepted = items ?? [];
|
|
15604
|
-
if (!accepted.length) {
|
|
15605
|
-
throw new Error("nothing is accepted yet. Run `nanopm roadmap`, then `nanopm accept <ref>`.");
|
|
15606
|
-
}
|
|
15607
|
-
let item = accepted[0];
|
|
15608
|
-
if (opts.ref) {
|
|
15609
|
-
const wanted = normalizeRef(opts.ref, accepted);
|
|
15610
|
-
const found = accepted.find((i) => i.ref === wanted);
|
|
15611
|
-
if (!found) {
|
|
15612
|
-
throw new Error(`${opts.ref} is not an accepted solution NanoPM can build. Accepted: ${accepted.map((i) => i.ref).join(", ")}.`);
|
|
15613
|
-
}
|
|
15614
|
-
item = found;
|
|
15615
|
-
}
|
|
15616
15821
|
const repo = await detectRepo(opts.cwd);
|
|
15617
|
-
if (!repo) throw new Error("run this inside the repository, so NanoPM
|
|
15822
|
+
if (!repo) throw new Error("run this inside the repository, so NanoPM builds from the clone you are looking at");
|
|
15823
|
+
const { data: solutions } = await auth.client.from("solutions").select("id, ref, title, status").eq("project_id", project.id).is("superseded_by", null);
|
|
15824
|
+
const wanted = normalizeRef(ref, solutions ?? []);
|
|
15825
|
+
const solution = (solutions ?? []).find((s) => s.ref === wanted);
|
|
15826
|
+
if (!solution) throw new Error(`No solution ${wanted} on the Roadmap.`);
|
|
15827
|
+
const stream = async (jobId2) => {
|
|
15828
|
+
out(`${solution.ref} ${solution.title}`);
|
|
15829
|
+
const answerer = opts.answersFile ? await answersFromFile(opts.answersFile) : answersFromTerminal(out);
|
|
15830
|
+
return streamJob(auth.client, jobId2, { pollMs: 700, out, renderer, cancelSignal: opts.cancelSignal, answerer, verbose: verboseWanted() });
|
|
15831
|
+
};
|
|
15832
|
+
const live = await buildOf(auth.client, project.id, [...ACTIVE_STATUSES]);
|
|
15833
|
+
if (live) {
|
|
15834
|
+
if (live.solutionId !== solution.id) throw new Error(`${project.name} already has a build on its way: one at a time. \`nanopm build\` on that solution comes back to it.`);
|
|
15835
|
+
if (opts.throwAway) throw new Error(`${solution.ref}'s build is still on its way: stop it first (\`nanopm stop\`).`);
|
|
15836
|
+
return stream(live.id);
|
|
15837
|
+
}
|
|
15838
|
+
const stopped = opts.continue || opts.throwAway ? await buildOf(auth.client, project.id, ["failed", "cancelled"], solution.id) : null;
|
|
15839
|
+
if ((opts.continue || opts.throwAway) && !stopped) throw new Error(`${solution.ref} has no stopped build to ${opts.continue ? "continue" : "throw away"}.`);
|
|
15840
|
+
if (opts.throwAway) return throwAway(auth.client, stopped.origin, repo.root, out);
|
|
15841
|
+
const [daemon, local] = await Promise.all([awakeDaemon(auth.client, auth.userId), localDaemonId()]);
|
|
15842
|
+
if (!local || daemon?.id !== local) throw new Error("This machine\u2019s daemon is asleep, so nothing was queued. `nanopm daemon install` starts it; then build again.");
|
|
15618
15843
|
await rememberClone(auth.client, project.id, repo.root);
|
|
15619
|
-
const
|
|
15620
|
-
|
|
15621
|
-
|
|
15622
|
-
|
|
15623
|
-
|
|
15624
|
-
|
|
15625
|
-
|
|
15626
|
-
|
|
15627
|
-
|
|
15628
|
-
|
|
15629
|
-
|
|
15630
|
-
|
|
15631
|
-
|
|
15632
|
-
|
|
15633
|
-
const { data } = await
|
|
15634
|
-
|
|
15635
|
-
}
|
|
15636
|
-
|
|
15637
|
-
|
|
15638
|
-
|
|
15639
|
-
|
|
15640
|
-
|
|
15641
|
-
|
|
15642
|
-
|
|
15643
|
-
|
|
15644
|
-
const current = await readAutonomy(auth.client, project.id);
|
|
15645
|
-
if (!level) {
|
|
15646
|
-
const lines = [`${project.name} is on "${current}".`, ""];
|
|
15647
|
-
for (const l of AUTONOMY_LEVELS) {
|
|
15648
|
-
lines.push(` ${l === current ? "\u25CF" : "\u25CB"} ${l.padEnd(26)} ${EXPLAINED[l]}`);
|
|
15649
|
-
}
|
|
15650
|
-
lines.push("", " nanopm autonomy <level>");
|
|
15651
|
-
return lines.join("\n");
|
|
15652
|
-
}
|
|
15653
|
-
const wanted = level.trim();
|
|
15654
|
-
if (!AUTONOMY_LEVELS.includes(wanted)) {
|
|
15655
|
-
throw new Error(`"${level}" is not a level. Pick one of: ${AUTONOMY_LEVELS.join(", ")}.`);
|
|
15656
|
-
}
|
|
15657
|
-
if (wanted === "execute_then_report") {
|
|
15658
|
-
throw new Error(
|
|
15659
|
-
"execute_then_report is not available yet. NanoPM asks before it changes anything until the review loop has been used in anger."
|
|
15660
|
-
);
|
|
15661
|
-
}
|
|
15662
|
-
if (wanted === current) return `${project.name} is already on "${current}".`;
|
|
15663
|
-
const { error } = await auth.client.from("project_preferences").update({ autonomy: wanted }).eq("project_id", project.id);
|
|
15664
|
-
if (error) {
|
|
15665
|
-
throw new Error(`could not change the dial: ${error.message}`);
|
|
15666
|
-
}
|
|
15667
|
-
return [`${project.name} is now on "${wanted}".`, EXPLAINED[wanted]].join("\n");
|
|
15668
|
-
}
|
|
15669
|
-
async function verifyCommand(command, opts) {
|
|
15670
|
-
const auth = await authenticate();
|
|
15671
|
-
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
15672
|
-
const { data } = await auth.client.from("project_preferences").select("verify_command").eq("project_id", project.id).maybeSingle();
|
|
15673
|
-
const current = data?.verify_command ?? null;
|
|
15674
|
-
if (!command && !opts.clear) {
|
|
15675
|
-
if (!current) {
|
|
15676
|
-
return [
|
|
15677
|
-
`${project.name} has no check, so nothing verifies what NanoPM writes.`,
|
|
15678
|
-
"",
|
|
15679
|
-
' nanopm verify "npm run lint && npm test"',
|
|
15680
|
-
"",
|
|
15681
|
-
"The daemon runs it in the repository root after a change and before the solution is",
|
|
15682
|
-
"marked done. A failure goes back to NanoPM once, to fix."
|
|
15683
|
-
].join("\n");
|
|
15684
|
-
}
|
|
15685
|
-
return [`${project.name} verifies with:`, "", ` ${current}`, "", ' nanopm verify "<command>" \xB7 nanopm verify --clear'].join("\n");
|
|
15686
|
-
}
|
|
15687
|
-
const next2 = opts.clear ? null : command.trim();
|
|
15688
|
-
if (next2 !== null && !next2) throw new Error('a check needs a command: nanopm verify "npm test"');
|
|
15689
|
-
const { error } = await auth.client.from("project_preferences").update({ verify_command: next2 }).eq("project_id", project.id);
|
|
15690
|
-
if (error) throw new Error(`could not save the check: ${error.message}`);
|
|
15691
|
-
if (next2 === null) return `${project.name} no longer runs a check. Nothing will verify what NanoPM writes.`;
|
|
15692
|
-
return [
|
|
15693
|
-
`${project.name} will verify with:`,
|
|
15694
|
-
"",
|
|
15695
|
-
` ${next2}`,
|
|
15696
|
-
"",
|
|
15697
|
-
"It runs in the repository root. If your change touches what the command itself runs \u2014",
|
|
15698
|
-
"package.json, a lockfile, the script it calls \u2014 the check is skipped and you are told,",
|
|
15699
|
-
"because an agent that can rewrite its own examiner is not being examined."
|
|
15700
|
-
].join("\n");
|
|
15844
|
+
const { jobId } = await queueBuild(auth.client, {
|
|
15845
|
+
projectId: project.id,
|
|
15846
|
+
userId: auth.userId,
|
|
15847
|
+
daemonId: local,
|
|
15848
|
+
ref: solution.ref,
|
|
15849
|
+
repoPath: repo.root,
|
|
15850
|
+
anyway: opts.anyway,
|
|
15851
|
+
...stopped ? { resumeOf: { id: stopped.id, was: stopped.was, origin: stopped.origin } } : {}
|
|
15852
|
+
});
|
|
15853
|
+
return stream(jobId);
|
|
15854
|
+
}
|
|
15855
|
+
async function buildOf(client, projectId, statuses, solutionId) {
|
|
15856
|
+
let query = client.from("jobs").select("id, params").eq("project_id", projectId).eq("skill", BUILD_SKILL).in("status", statuses);
|
|
15857
|
+
if (solutionId) query = query.eq("params->>solution_id", solutionId);
|
|
15858
|
+
const { data } = await query.order("created_at", { ascending: false }).limit(1).maybeSingle();
|
|
15859
|
+
const params = data?.params ?? {};
|
|
15860
|
+
return data ? { id: data.id, solutionId: params.solution_id ?? "", was: wasOf(data.params), origin: params.origin ?? data.id } : null;
|
|
15861
|
+
}
|
|
15862
|
+
async function throwAway(client, jobId, clone, out) {
|
|
15863
|
+
const { place } = await loadBuildHistory(client, [jobId], { home: homedir6(), jobId });
|
|
15864
|
+
if (!place) throw new Error("that build left nothing on this machine.");
|
|
15865
|
+
if (existsSync7(place.dir)) await execFileAsync9("git", ["-C", clone, "worktree", "remove", "--force", place.dir]);
|
|
15866
|
+
await execFileAsync9("git", ["-C", clone, "branch", "-D", place.branch]).catch(() => void 0);
|
|
15867
|
+
out(`Thrown away: the copy of the code and the local branch ${place.branch}.`);
|
|
15868
|
+
return 0;
|
|
15701
15869
|
}
|
|
15702
15870
|
|
|
15703
15871
|
// src/detect-sources.ts
|
|
15704
|
-
import { readFile as
|
|
15705
|
-
import { join as
|
|
15872
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
15873
|
+
import { join as join17 } from "path";
|
|
15706
15874
|
var ENV_FILES = [".env", ".env.local", ".env.development.local", ".env.development", ".env.example"];
|
|
15707
15875
|
var POSTGRES_KEYS = [
|
|
15708
15876
|
"DATABASE_URL",
|
|
@@ -15752,7 +15920,7 @@ async function envFiles(repoRoot) {
|
|
|
15752
15920
|
const out = [];
|
|
15753
15921
|
for (const file of ENV_FILES) {
|
|
15754
15922
|
try {
|
|
15755
|
-
out.push({ file, env: parseEnv(await
|
|
15923
|
+
out.push({ file, env: parseEnv(await readFile15(join17(repoRoot, file), "utf8")) });
|
|
15756
15924
|
} catch {
|
|
15757
15925
|
continue;
|
|
15758
15926
|
}
|
|
@@ -15812,7 +15980,7 @@ function describeUrl(url) {
|
|
|
15812
15980
|
// src/format/closing.ts
|
|
15813
15981
|
function closingLines(project, exit, env = process.env, mode = "onboard") {
|
|
15814
15982
|
if (exit !== 0) return [];
|
|
15815
|
-
const
|
|
15983
|
+
const build2 = mode === "start" ? `nanopm accept <ref> takes an option; when one needs code, in its repo: nanopm onboard --project ${project.id ?? "<id>"} attaches it, then nanopm build <ref> builds it` : "nanopm spec <ref> --write writes its specs, nanopm build <ref> builds it";
|
|
15816
15984
|
const chain = chainedAfter(mode).map((skill) => skillSentence(skill));
|
|
15817
15985
|
return [
|
|
15818
15986
|
"",
|
|
@@ -15820,7 +15988,7 @@ function closingLines(project, exit, env = process.env, mode = "onboard") {
|
|
|
15820
15988
|
...chain.length ? [`NanoPM is now ${joinAnd(chain)}; the Roadmap fills in as it goes. Watch it at ${webUrl(env)}/p/${project.slug}/journal`] : [],
|
|
15821
15989
|
// The map is no longer chained after onboarding, so the first three clauses are what
|
|
15822
15990
|
// builds it: what users said, where they leave traces, then the map that reads both.
|
|
15823
|
-
`Next: nanopm add <file> adds what users said \xB7 nanopm connect adds where they leave traces \xB7 nanopm opportunities run ranks what to work on \xB7 ${
|
|
15991
|
+
`Next: nanopm add <file> adds what users said \xB7 nanopm connect adds where they leave traces \xB7 nanopm opportunities run ranks what to work on \xB7 ${build2} \xB7 nanopm schedule weekly monday 7 sets the review`,
|
|
15824
15992
|
"With a partner: nanopm invite <email> prints a link; they open it, or in their clone: nanopm login, then nanopm join <code>"
|
|
15825
15993
|
];
|
|
15826
15994
|
}
|
|
@@ -16521,7 +16689,7 @@ async function seedWatches(watches2, read6) {
|
|
|
16521
16689
|
}
|
|
16522
16690
|
|
|
16523
16691
|
// src/commands/watch.ts
|
|
16524
|
-
import { readFile as
|
|
16692
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
16525
16693
|
function cursorFor(source, watchId) {
|
|
16526
16694
|
const all = source.cursor ?? {};
|
|
16527
16695
|
return all[watchId] ?? {};
|
|
@@ -16582,7 +16750,7 @@ async function addWatch(labelOrId, id, opts) {
|
|
|
16582
16750
|
const paint = painter(color);
|
|
16583
16751
|
if (!labelOrId || !id) throw new Error('say which feed and what to call it: nanopm watch add <source> <id> --query "\u2026"');
|
|
16584
16752
|
if (opts.query && opts.queryFile) throw new Error("--query or --query-file, not both");
|
|
16585
|
-
const query = opts.queryFile ? await
|
|
16753
|
+
const query = opts.queryFile ? await readFile16(opts.queryFile, "utf8") : opts.query ?? "";
|
|
16586
16754
|
const watch = watchInput(id, query, opts);
|
|
16587
16755
|
const auth = await authenticate();
|
|
16588
16756
|
const project = await resolveProject(auth.client, { projectId: opts.projectId, cwd: opts.cwd });
|
|
@@ -16769,9 +16937,9 @@ async function review(opts) {
|
|
|
16769
16937
|
}
|
|
16770
16938
|
const usable = connections.filter((c) => c.status === "ok");
|
|
16771
16939
|
if (!usable.length) {
|
|
16772
|
-
const
|
|
16940
|
+
const failed2 = connections.map((c) => c.label).join(", ");
|
|
16773
16941
|
throw new Error(
|
|
16774
|
-
`no connection is working (${
|
|
16942
|
+
`no connection is working (${failed2}), so a review would have nothing to read.
|
|
16775
16943
|
nanopm connect --list see what is wrong`
|
|
16776
16944
|
);
|
|
16777
16945
|
}
|
|
@@ -17406,12 +17574,12 @@ function renderDirection(rows2) {
|
|
|
17406
17574
|
const out = [];
|
|
17407
17575
|
for (const line of DIRECTION_ROWS) {
|
|
17408
17576
|
const row = rows2.find((r) => r.key === directionKey(line));
|
|
17409
|
-
const
|
|
17577
|
+
const next = rows2.find((r) => r.key === directionKey(line, true));
|
|
17410
17578
|
out.push(`${DIRECTION_ROW_TITLE[line]}${mark(row)}`);
|
|
17411
17579
|
out.push(` ${row?.content ?? "\u2014"}`);
|
|
17412
|
-
if (
|
|
17413
|
-
const because = reasonOf2(
|
|
17414
|
-
out.push(` proposed: ${
|
|
17580
|
+
if (next) {
|
|
17581
|
+
const because = reasonOf2(next.sources);
|
|
17582
|
+
out.push(` proposed: ${next.content}${because ? ` \xB7 ${because}` : ""}`);
|
|
17415
17583
|
}
|
|
17416
17584
|
}
|
|
17417
17585
|
out.push("Objective");
|
|
@@ -17451,13 +17619,13 @@ function render2(rows2) {
|
|
|
17451
17619
|
const out = [];
|
|
17452
17620
|
for (const line of PITCH_LINE_KEYS) {
|
|
17453
17621
|
const row = rows2.find((r) => r.key === pitchKey(line));
|
|
17454
|
-
const
|
|
17622
|
+
const next = rows2.find((r) => r.key === pitchKey(line, true));
|
|
17455
17623
|
const mark2 = row && row.provenance !== "stated" ? ` \xB7 to review${row.confidence != null ? ` (${row.confidence}% sure)` : ""}` : "";
|
|
17456
17624
|
out.push(`${PITCH_LINE_TITLE[line]}${mark2}`);
|
|
17457
17625
|
out.push(` ${row?.content ?? "\u2014"}`);
|
|
17458
|
-
if (
|
|
17459
|
-
const because = reasonOf3(
|
|
17460
|
-
out.push(` proposed: ${
|
|
17626
|
+
if (next) {
|
|
17627
|
+
const because = reasonOf3(next.sources);
|
|
17628
|
+
out.push(` proposed: ${next.content}${because ? ` \xB7 ${because}` : ""}`);
|
|
17461
17629
|
}
|
|
17462
17630
|
}
|
|
17463
17631
|
return out.join("\n");
|
|
@@ -17515,7 +17683,7 @@ async function invite(email, opts) {
|
|
|
17515
17683
|
const [, ...rest] = inviteLines({ name: project.name, code, email: email.trim().toLowerCase(), web: webUrl() });
|
|
17516
17684
|
return [`Invite to ${paint(ANSI.bold, project.name)}: ${paint(ANSI.bold, code)}`, ...rest.map((l) => l ? paint(ANSI.dim, l) : l)].join("\n");
|
|
17517
17685
|
}
|
|
17518
|
-
async function
|
|
17686
|
+
async function join18(code, opts) {
|
|
17519
17687
|
const normalized2 = code.trim().toUpperCase();
|
|
17520
17688
|
if (!INVITE_CODE.test(normalized2)) throw new Error(`that does not look like an invite code: ten letters and digits, like ${"K7PQ2XM9RT"}`);
|
|
17521
17689
|
const repo = await detectRepo(opts.cwd);
|
|
@@ -17800,7 +17968,7 @@ async function showSchedule(opts) {
|
|
|
17800
17968
|
}
|
|
17801
17969
|
|
|
17802
17970
|
// src/commands/status.ts
|
|
17803
|
-
import { existsSync as
|
|
17971
|
+
import { existsSync as existsSync8 } from "fs";
|
|
17804
17972
|
async function status(opts) {
|
|
17805
17973
|
const auth = await authenticate();
|
|
17806
17974
|
const color = opts.color ?? useColor();
|
|
@@ -17853,7 +18021,7 @@ async function projectStatus(client, project) {
|
|
|
17853
18021
|
openProblems: (problems2.data ?? []).length,
|
|
17854
18022
|
unprovenProblems: (problems2.data ?? []).filter((p) => p.importance_basis === "judgment").length,
|
|
17855
18023
|
search: describeSearch(search, await localWebSearch()),
|
|
17856
|
-
onThisMachine: Boolean(project.repoPath &&
|
|
18024
|
+
onThisMachine: Boolean(project.repoPath && existsSync8(project.repoPath)),
|
|
17857
18025
|
measures: measures({
|
|
17858
18026
|
solutionStatuses: (roadmap2.data ?? []).map((r) => r.status),
|
|
17859
18027
|
decisions: decisions2.data ?? [],
|
|
@@ -18216,7 +18384,7 @@ function buildProgram() {
|
|
|
18216
18384
|
console.log(await invite(email, { cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
|
|
18217
18385
|
});
|
|
18218
18386
|
program.command("join <code>").description("Join a project you were invited to, from inside your clone of its repository").option("--no-color", "plain output").action(async (code, opts) => {
|
|
18219
|
-
console.log(await
|
|
18387
|
+
console.log(await join18(code, { cwd: process.cwd(), color: colorOf(opts) }));
|
|
18220
18388
|
});
|
|
18221
18389
|
program.command("members").description("Who is on this project, and whether their daemon is awake").option("--project <id>", "project id (default: the project of the current repo)").option("--no-color", "plain output").action(async (opts) => {
|
|
18222
18390
|
console.log(await members({ cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
|
|
@@ -18238,16 +18406,11 @@ function buildProgram() {
|
|
|
18238
18406
|
program.command("status").description("What is happening with this product: next review, what is waiting on you, whether the daemon is awake").option("--project <id>", "project id (default: the project of the current repo)").option("--no-color", "plain output").action(async (opts) => {
|
|
18239
18407
|
console.log(await status({ cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
|
|
18240
18408
|
});
|
|
18241
|
-
program.command("
|
|
18409
|
+
program.command("build <ref>").description("Build a solution from its agent spec, in a copy of this repository, to a draft pull request").option("--project <id>", "project id (default: the project of the current repo)").option("--anyway", "build although its questions for you are not answered").option("--continue", "take up a stopped build where it was").option("--throw-away", "delete a stopped build's copy of the code and its local branch").option("--answers <file>", "answer its questions from a file instead of the terminal").action(async (ref, opts) => {
|
|
18242
18410
|
const cancel = new AbortController();
|
|
18243
18411
|
process.once("SIGINT", () => cancel.abort());
|
|
18244
|
-
|
|
18245
|
-
|
|
18246
|
-
projectId: opts.project,
|
|
18247
|
-
ref: ref ?? opts.ref,
|
|
18248
|
-
answersFile: opts.answers,
|
|
18249
|
-
cancelSignal: cancel.signal
|
|
18250
|
-
});
|
|
18412
|
+
if (opts.continue && opts.throwAway) throw new Error("--continue or --throw-away, not both.");
|
|
18413
|
+
process.exitCode = await build(ref, { cwd: process.cwd(), projectId: opts.project, anyway: opts.anyway, continue: opts.continue, throwAway: opts.throwAway, answersFile: opts.answers, cancelSignal: cancel.signal });
|
|
18251
18414
|
});
|
|
18252
18415
|
program.command("autonomy [level]").description("How far NanoPM may go on its own: read it, or set it").option("--project <id>", "project id (default: the project of the current repo)").action(async (level, opts) => {
|
|
18253
18416
|
console.log(await autonomy(level, { cwd: process.cwd(), projectId: opts.project }));
|