@nanonets/graft 0.20.0 → 0.21.1
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/app/history.d.ts +36 -1
- package/dist/app/history.d.ts.map +1 -1
- package/dist/app/history.js +72 -25
- package/dist/app/history.js.map +1 -1
- package/dist/app/sources.d.ts +13 -0
- package/dist/app/sources.d.ts.map +1 -1
- package/dist/app/sources.js +129 -1
- package/dist/app/sources.js.map +1 -1
- package/dist/brain/claude-md.d.ts +4 -2
- package/dist/brain/claude-md.d.ts.map +1 -1
- package/dist/brain/claude-md.js +4 -3
- package/dist/brain/claude-md.js.map +1 -1
- package/dist/brain/context-files.d.ts +79 -0
- package/dist/brain/context-files.d.ts.map +1 -0
- package/dist/brain/context-files.js +245 -0
- package/dist/brain/context-files.js.map +1 -0
- package/dist/brain/link.d.ts +17 -0
- package/dist/brain/link.d.ts.map +1 -1
- package/dist/brain/link.js +21 -0
- package/dist/brain/link.js.map +1 -1
- package/dist/brain/pull.d.ts +48 -0
- package/dist/brain/pull.d.ts.map +1 -0
- package/dist/brain/pull.js +257 -0
- package/dist/brain/pull.js.map +1 -0
- package/dist/brain/push.d.ts +62 -7
- package/dist/brain/push.d.ts.map +1 -1
- package/dist/brain/push.js +97 -62
- package/dist/brain/push.js.map +1 -1
- package/dist/brain/signup.d.ts +46 -2
- package/dist/brain/signup.d.ts.map +1 -1
- package/dist/brain/signup.js +101 -11
- package/dist/brain/signup.js.map +1 -1
- package/dist/brain/upload.d.ts +61 -0
- package/dist/brain/upload.d.ts.map +1 -0
- package/dist/brain/upload.js +210 -0
- package/dist/brain/upload.js.map +1 -0
- package/dist/brain/watch.d.ts +76 -3
- package/dist/brain/watch.d.ts.map +1 -1
- package/dist/brain/watch.js +264 -39
- package/dist/brain/watch.js.map +1 -1
- package/dist/claude/build-progress.d.ts +29 -0
- package/dist/claude/build-progress.d.ts.map +1 -0
- package/dist/claude/build-progress.js +76 -0
- package/dist/claude/build-progress.js.map +1 -0
- package/dist/cli-picker.d.ts +17 -2
- package/dist/cli-picker.d.ts.map +1 -1
- package/dist/cli-picker.js +103 -22
- package/dist/cli-picker.js.map +1 -1
- package/dist/cli.js +385 -187
- package/dist/cli.js.map +1 -1
- package/dist/telemetry/contract.d.ts +16 -3
- package/dist/telemetry/contract.d.ts.map +1 -1
- package/dist/telemetry/contract.js +23 -5
- package/dist/telemetry/contract.js.map +1 -1
- package/dist/util/spinner.d.ts +15 -0
- package/dist/util/spinner.d.ts.map +1 -0
- package/dist/util/spinner.js +52 -0
- package/dist/util/spinner.js.map +1 -0
- package/dist/util/state.d.ts +8 -0
- package/dist/util/state.d.ts.map +1 -1
- package/dist/util/state.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -19,31 +19,33 @@ import { hostIds } from "./hosts/registry.js";
|
|
|
19
19
|
import { parseBrainArg, connectBrain, pullBrain, brainStatus } from "./brain/connect.js";
|
|
20
20
|
import { rulesForPointers } from "./brain/attach.js";
|
|
21
21
|
import { clearLink } from "./brain/link.js";
|
|
22
|
-
import { buildEarlyDigest, buildLocalDigest, fetchExpectedRepo, pushDigest, pushEarlyDigest, repoSlugFromGit, sameRepo, } from "./brain/push.js";
|
|
23
|
-
import { readLink, writeLink } from "./brain/link.js";
|
|
22
|
+
import { buildEarlyDigest, buildLocalDigest, fetchExpectedRepo, pushContext, pushDigest, pushEarlyDigest, repoSlugFromGit, sameRepo, uploadCaps, } from "./brain/push.js";
|
|
23
|
+
import { apiBaseUrl, clearPendingSignup, readLink, readPendingSignup, writeLink, writePendingSignup } from "./brain/link.js";
|
|
24
24
|
import { withLegacyNames } from "./legacy-args.js";
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
25
|
+
import { currentStage, DOING_LABEL, rulesSoFar, watchBuild } from "./brain/watch.js";
|
|
26
|
+
import { AGENT_WAIT_MS, brainUrl, newSignupState, openBrowser, PENDING_SIGNUP_TTL_MS, reviewUrl, signupUrl, startHandoff, waitForSignup, } from "./brain/signup.js";
|
|
27
|
+
import { runTrailPull, suggestionsLine } from "./brain/pull.js";
|
|
28
|
+
import { startSpinner } from "./util/spinner.js";
|
|
28
29
|
import { contextDirFor } from "./context/node-file.js";
|
|
29
30
|
import { loadGraphCached } from "./graph/load.js";
|
|
30
31
|
import { ensureFreshChildren, ensureFreshGraph, refreshNote } from "./graph/refresh.js";
|
|
31
32
|
import { isWorkspaceBuildRoot, readWorkspace } from "./graph/workspace.js";
|
|
32
|
-
import { nearestGraftRoot } from "./graph/root.js";
|
|
33
|
+
import { hasGraftIndex, nearestGraftRoot } from "./graph/root.js";
|
|
33
34
|
import { unsupportedExtensions, supportedExtensions } from "./graph/source-files.js";
|
|
34
35
|
import { discoverWorkspaceChildren } from "./graph/scopes.js";
|
|
35
36
|
import { runWorkspaceAsk, runWorkspaceBuild, runWorkspaceCallers, runWorkspaceCheck, runWorkspaceGrep, runWorkspaceMap, } from "./graph/workspace-cli.js";
|
|
36
37
|
import { formatInitEpilogue } from "./cli-epilogue.js";
|
|
37
38
|
import { planInit, selectedWrites } from "./hosts/plan.js";
|
|
38
39
|
import { planRetract, runRetract, changed } from "./hosts/retract.js";
|
|
39
|
-
import { formatNonInteractiveHelp, formatPlan, runPicker } from "./cli-picker.js";
|
|
40
|
+
import { compactWrites, formatNonInteractiveHelp, formatPlan, runPicker } from "./cli-picker.js";
|
|
41
|
+
import { buildGraphWithProgress } from "./claude/build-progress.js";
|
|
40
42
|
import { homedir } from "node:os";
|
|
41
43
|
import { formatUpgradeReport, formatVersionReport, getNpmViewVersion, readCurrentVersion, runUpgrade } from "./cli-meta.js";
|
|
42
44
|
import { patchBuildConfig } from "./util/state.js";
|
|
43
45
|
import { normalizePathPrefix } from "./util/paths.js";
|
|
44
46
|
import { latestSession, formatSessionStats, sessionInputRate } from "./claude/session-metrics.js";
|
|
45
47
|
import { setInputRate } from "./context/savings.js";
|
|
46
|
-
import { formatUpdateNudge, maybeRefreshInBackground, readUpdateCache, refreshUpdateCache, wiredHostIds, writeStamp } from "./upkeep.js";
|
|
48
|
+
import { formatUpdateNudge, maybeRefreshInBackground, readStamp, readUpdateCache, refreshUpdateCache, wiredHostIds, writeStamp } from "./upkeep.js";
|
|
47
49
|
import { errorCode, filesBucket, durationBucket, formatDebug, formatStatus, firstRunNotice, isTrackedCommand, langsValue, offReason, maybeFlushInBackground, patchState, runFlush, track, trackFirstRunIfNew, } from "./telemetry/index.js";
|
|
48
50
|
const program = new Command();
|
|
49
51
|
const currentVersion = readCurrentVersion(import.meta.url);
|
|
@@ -820,7 +822,20 @@ program
|
|
|
820
822
|
.option("-y, --yes", "skip the picker and wire every detected agent (the pre-0.8 default)")
|
|
821
823
|
.option("--no-global", "skip writes outside this repo (the ~/.codex/ config + hooks)")
|
|
822
824
|
.option("--trail <handoff>", "attach a Trail: <brainId>:<token> (or a bare id with GRAFT_BRAIN_TOKEN set)")
|
|
823
|
-
.
|
|
825
|
+
.option("--verbose", "print every file written, the graph build's own output and the closing banner")
|
|
826
|
+
.action(async (dir, opts) => {
|
|
827
|
+
await runInitCommand(dir, opts);
|
|
828
|
+
});
|
|
829
|
+
/** `a, b and c`. */
|
|
830
|
+
function joinAnd(items) {
|
|
831
|
+
if (items.length <= 1)
|
|
832
|
+
return items.join("");
|
|
833
|
+
return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
|
|
834
|
+
}
|
|
835
|
+
/** Repo-relative with forward slashes, for printing. */
|
|
836
|
+
function shown(repo, path) {
|
|
837
|
+
return relative(repo, path).split("\\").join("/");
|
|
838
|
+
}
|
|
824
839
|
/**
|
|
825
840
|
* `graft init`. Also run by `graft trail push` on a repo graft has not been set
|
|
826
841
|
* up in — the Trail pages start people at push, and a trail whose rules no
|
|
@@ -831,7 +846,7 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
831
846
|
if (opts.listAgents) {
|
|
832
847
|
for (const id of [...hostIds(), "claude"])
|
|
833
848
|
console.log(id);
|
|
834
|
-
return;
|
|
849
|
+
return null;
|
|
835
850
|
}
|
|
836
851
|
// Parsed before anything is written: a mistyped handoff should cost the user
|
|
837
852
|
// an error, not a half-wired repo they have to `graft uninstall` out of.
|
|
@@ -841,7 +856,7 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
841
856
|
if ("error" in parsed) {
|
|
842
857
|
console.error(`✗ --trail: ${parsed.error}`);
|
|
843
858
|
process.exitCode = 1;
|
|
844
|
-
return;
|
|
859
|
+
return null;
|
|
845
860
|
}
|
|
846
861
|
brainLink = parsed;
|
|
847
862
|
}
|
|
@@ -881,17 +896,19 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
881
896
|
const reason = offReason();
|
|
882
897
|
const picked = await runPicker(plan, repo, home, {
|
|
883
898
|
offerTelemetry: reason === null || reason === "disabled",
|
|
899
|
+
// From push, the line above the picker already asks the question.
|
|
900
|
+
...(how.push ? { title: null } : {}),
|
|
884
901
|
});
|
|
885
902
|
if (picked === null) {
|
|
886
903
|
console.error("· cancelled — nothing written");
|
|
887
|
-
return;
|
|
904
|
+
return null;
|
|
888
905
|
}
|
|
889
906
|
ids = picked.hosts;
|
|
890
907
|
consent = picked.telemetry;
|
|
891
908
|
}
|
|
892
909
|
else {
|
|
893
910
|
console.error(formatNonInteractiveHelp(detectedIds));
|
|
894
|
-
return;
|
|
911
|
+
return null;
|
|
895
912
|
}
|
|
896
913
|
// The picker's answer, recorded before anything is wired: a user who
|
|
897
914
|
// unchecked the row must not have this run's init_completed sent.
|
|
@@ -912,34 +929,121 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
912
929
|
console.error(formatPlan(plan, ids, repo, home));
|
|
913
930
|
for (const child of children)
|
|
914
931
|
console.error(`\n— ${child}/ (workspace child)\n` + formatPlan(planInit(join(repo, child), { home }), ids, join(repo, child), home));
|
|
915
|
-
return;
|
|
932
|
+
return null;
|
|
916
933
|
}
|
|
917
934
|
if (ids.length === 0) {
|
|
918
935
|
console.error("· no agents selected — nothing written");
|
|
919
|
-
return;
|
|
936
|
+
return null;
|
|
920
937
|
}
|
|
921
938
|
const wantClaude = ids.includes("claude");
|
|
922
939
|
const cliPath = fileURLToPath(import.meta.url);
|
|
923
940
|
if (children.length)
|
|
924
941
|
console.error(`· workspace: wiring ${repo} and ${children.length} child repo(s) — ${children.join(", ")}`);
|
|
942
|
+
// Today's output, file by file, behind --verbose. Everything else gets one
|
|
943
|
+
// line per step: the graph, then one per agent.
|
|
944
|
+
const verbose = opts.verbose === true;
|
|
945
|
+
const reports = [];
|
|
925
946
|
for (const target of targets) {
|
|
926
|
-
if (target !== repo)
|
|
947
|
+
if (verbose && target !== repo)
|
|
927
948
|
console.error(`\n— ${relative(repo, target)}/`);
|
|
928
|
-
wireTarget(target, ids, { home, cliPath, plan, opts, wantClaude });
|
|
949
|
+
reports.push(wireTarget(target, ids, { home, cliPath, plan, opts, wantClaude, quiet: !verbose }));
|
|
950
|
+
}
|
|
951
|
+
// The graph, built once after the wiring: a workspace parent's build is
|
|
952
|
+
// the workspace build, so it covers every child.
|
|
953
|
+
let graphLine = null;
|
|
954
|
+
let graphNodes = 0;
|
|
955
|
+
if (!verbose) {
|
|
956
|
+
const res = await buildGraphWithProgress(repo, { build: opts.build, cliPath });
|
|
957
|
+
for (const m of res.messages)
|
|
958
|
+
console.error(m);
|
|
959
|
+
const existing = res.built ? null : loadGraphCached(contextDirFor(repo, children.length ? undefined : program.opts().dir));
|
|
960
|
+
const fmt = (x) => x.toLocaleString("en-US");
|
|
961
|
+
if (res.graph) {
|
|
962
|
+
graphNodes = res.graph.nodes;
|
|
963
|
+
graphLine = `✓ graph built · ${fmt(res.graph.nodes)} nodes, ${fmt(res.graph.edges)} edges${res.graph.files ? ` from ${fmt(res.graph.files)} file${res.graph.files === 1 ? "" : "s"}` : ""}`;
|
|
964
|
+
}
|
|
965
|
+
else if (res.built) {
|
|
966
|
+
graphLine = "✓ graph built";
|
|
967
|
+
}
|
|
968
|
+
else if (res.failed) {
|
|
969
|
+
graphLine = "⚠ the graph build failed — run graft build to see why";
|
|
970
|
+
}
|
|
971
|
+
else if (existing) {
|
|
972
|
+
graphNodes = existing.meta.nodeCount;
|
|
973
|
+
graphLine = `✓ graph ready · ${fmt(existing.meta.nodeCount)} nodes, ${fmt(existing.meta.edgeCount)} edges`;
|
|
974
|
+
}
|
|
975
|
+
else if (hasGraftIndex(repo)) {
|
|
976
|
+
graphLine = "✓ graph ready";
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
graphLine = "· skipped the graph build — run graft build";
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (!verbose) {
|
|
983
|
+
// Warnings and removals in full, whatever else is folded.
|
|
984
|
+
const removed = [...new Set(reports.flatMap((r) => r.retracted))];
|
|
985
|
+
for (const id of removed)
|
|
986
|
+
console.error(`- removed ${id} — agent not selected`);
|
|
987
|
+
for (const w of reports.flatMap((r) => r.warnings))
|
|
988
|
+
console.error(w);
|
|
989
|
+
const writesFor = (id) => (plan.find((p) => p.id === id)?.writes ?? []).filter((w) => opts.global !== false || w.scope !== "global");
|
|
990
|
+
if (how.push) {
|
|
991
|
+
const built = graphLine?.startsWith("✓ graph built") ? `graph built, ${graphNodes.toLocaleString("en-US")} nodes` : graphLine?.startsWith("✓") ? "graph ready" : null;
|
|
992
|
+
console.error(`✓ wired ${joinAnd(ids)}${built ? ` · ${built}` : ""}`);
|
|
993
|
+
if (graphLine && !graphLine.startsWith("✓"))
|
|
994
|
+
console.error(graphLine);
|
|
995
|
+
}
|
|
996
|
+
else {
|
|
997
|
+
if (graphLine)
|
|
998
|
+
console.error(graphLine);
|
|
999
|
+
const width = Math.max(...ids.map((id) => id.length));
|
|
1000
|
+
for (const id of ids) {
|
|
1001
|
+
const summary = compactWrites(writesFor(id), repo, home);
|
|
1002
|
+
console.error(`✓ ${id.padEnd(width)} ${summary}`.trimEnd());
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
929
1005
|
}
|
|
930
1006
|
// The brain comes last, after the graph exists: its rules are anchored to
|
|
931
1007
|
// symbols, and `graft trail status` can only report how many of them resolve
|
|
932
1008
|
// once there is a graph to resolve them against.
|
|
933
1009
|
if (brainLink) {
|
|
934
1010
|
const res = await connectBrain(repo, brainLink, { home, ids });
|
|
935
|
-
if (
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1011
|
+
if (verbose) {
|
|
1012
|
+
if (res.warning)
|
|
1013
|
+
console.error(`⚠ brain: ${res.warning}`);
|
|
1014
|
+
else
|
|
1015
|
+
console.error(`✓ brain: pulled ${res.ruleCount} rule(s) from ${brainLink.brainId}`);
|
|
1016
|
+
for (const w of res.writes)
|
|
1017
|
+
console.error(`✓ brain rules: ${w.path} (${w.action})`);
|
|
1018
|
+
if (res.ruleCount > 0 && res.writes.length === 0)
|
|
1019
|
+
console.error("· no instruction file to write rules into — graft ask still carries them");
|
|
1020
|
+
}
|
|
1021
|
+
else if (res.warning) {
|
|
1022
|
+
console.error(`⚠ trail: ${res.warning}`);
|
|
1023
|
+
}
|
|
1024
|
+
else if (res.ruleCount === 0) {
|
|
1025
|
+
console.error("✓ trail connected · no rules yet — graft trail push reads this repo into it");
|
|
1026
|
+
}
|
|
1027
|
+
else {
|
|
1028
|
+
const where = res.writes.map((w) => shown(repo, w.path)).join(", ");
|
|
1029
|
+
const n = `${res.ruleCount.toLocaleString("en-US")} rule${res.ruleCount === 1 ? "" : "s"}`;
|
|
1030
|
+
console.error(`✓ trail connected · ${n}${where ? ` in ${where}` : ""}`);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (!verbose && how.epilogue !== false) {
|
|
1034
|
+
// What there is to commit: the top-level entries the picked agents wrote.
|
|
1035
|
+
const tops = [];
|
|
1036
|
+
for (const w of selectedWrites(plan, ids)) {
|
|
1037
|
+
if (w.scope !== "repo")
|
|
1038
|
+
continue;
|
|
1039
|
+
const parts = shown(repo, w.path).split("/");
|
|
1040
|
+
const top = parts.length > 1 ? `${parts[0]}/` : parts[0];
|
|
1041
|
+
if (!tops.includes(top))
|
|
1042
|
+
tops.push(top);
|
|
1043
|
+
}
|
|
1044
|
+
console.error("· restart your agents so a new session picks up graft");
|
|
1045
|
+
if (tops.length)
|
|
1046
|
+
console.error(`· commit ${tops.slice(0, 4).join(" ")}${tops.length > 4 ? ` +${tops.length - 4} more` : ""} to share it — graft/ stays local and git-ignored`);
|
|
943
1047
|
}
|
|
944
1048
|
// One epilogue for the whole run. A workspace parent holds no nodes of its
|
|
945
1049
|
// own, so the totals come from the children — the graph the user actually got.
|
|
@@ -947,7 +1051,7 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
947
1051
|
const graphs = (children.length ? children.map((c) => join(repo, c)) : [repo])
|
|
948
1052
|
.map((d) => loadGraphCached(contextDirFor(d, children.length ? undefined : globalDir)))
|
|
949
1053
|
.filter((g) => g !== null);
|
|
950
|
-
if (how.epilogue !== false)
|
|
1054
|
+
if (verbose && how.epilogue !== false)
|
|
951
1055
|
console.error("\n" +
|
|
952
1056
|
formatInitEpilogue({
|
|
953
1057
|
graphBuilt: graphs.length > 0,
|
|
@@ -956,10 +1060,21 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
956
1060
|
}));
|
|
957
1061
|
// Sorted so `claude,cursor` and `cursor,claude` aggregate as one value.
|
|
958
1062
|
track("init_completed", { agents: [...ids].sort().join(","), consent: consent === undefined ? "unasked" : String(consent) }, { repo });
|
|
1063
|
+
return { ids };
|
|
959
1064
|
}
|
|
960
1065
|
/** One repo's worth of `init` writes — the parent, then each workspace child. */
|
|
961
1066
|
function wireTarget(repo, ids, ctx) {
|
|
962
|
-
const { home, cliPath, plan, wantClaude
|
|
1067
|
+
const { home, cliPath, plan, wantClaude } = ctx;
|
|
1068
|
+
const quiet = ctx.quiet === true;
|
|
1069
|
+
const opts = quiet ? { ...ctx.opts, build: false } : ctx.opts;
|
|
1070
|
+
const report = { retracted: [], warnings: [] };
|
|
1071
|
+
// Every per-file line goes through here; in quiet mode only ⚠ lines survive.
|
|
1072
|
+
const say = (line) => {
|
|
1073
|
+
if (!quiet)
|
|
1074
|
+
console.error(line);
|
|
1075
|
+
else if (line.startsWith("⚠"))
|
|
1076
|
+
report.warnings.push(line);
|
|
1077
|
+
};
|
|
963
1078
|
const wantStatusline = statuslineWanted({ statusline: opts.statusline });
|
|
964
1079
|
// Converge, don't just add. init writes the selected hosts; without this it
|
|
965
1080
|
// never touches the rest, so a repo wired by an older version (or by the same
|
|
@@ -969,27 +1084,28 @@ function wireTarget(repo, ids, ctx) {
|
|
|
969
1084
|
// be rewritten, and the graph cache is kept (init is one step from using it).
|
|
970
1085
|
const retracted = changed(runRetract(repo, { home, apply: true, global: opts.global, cache: false, exclude: ids })).filter((r) => r.action !== "skipped-unparseable");
|
|
971
1086
|
for (const r of retracted)
|
|
972
|
-
|
|
1087
|
+
say(`- removed ${r.path} (${r.what}) — agent not selected`);
|
|
1088
|
+
report.retracted = [...new Set(retracted.map((r) => r.hostId))];
|
|
973
1089
|
if (wantClaude) {
|
|
974
1090
|
// `global`/`home` are threaded through alongside `statusline`: the claude layer
|
|
975
1091
|
// writes under `~/.claude` now (hosts/claude-global.ts), so --no-global has to
|
|
976
1092
|
// reach it or the flag would silently mean "no out-of-repo writes, except three".
|
|
977
1093
|
const res = runInit(repo, { build: opts.build, cliPath, statusline: wantStatusline, global: opts.global, home });
|
|
978
|
-
|
|
1094
|
+
say(`✓ wrote ${res.settingsPath}`);
|
|
979
1095
|
for (const s of res.shims)
|
|
980
|
-
|
|
981
|
-
|
|
1096
|
+
say(`✓ wrote ${s}`);
|
|
1097
|
+
say(`✓ wrote ${res.skill}`);
|
|
982
1098
|
if (res.mcp.action === "skipped-unparseable")
|
|
983
|
-
|
|
1099
|
+
say(`⚠ .mcp.json: ${res.mcp.path} left unchanged (not valid JSON) — add the graft server manually`);
|
|
984
1100
|
else if (res.mcp.action === "unchanged")
|
|
985
|
-
|
|
1101
|
+
say(`· mcp claude: ${res.mcp.path} (already registered)`);
|
|
986
1102
|
else
|
|
987
|
-
|
|
988
|
-
|
|
1103
|
+
say(`✓ mcp claude: ${res.mcp.path} (${res.mcp.action}) — restart Claude Code to load the graft MCP server`);
|
|
1104
|
+
say(res.built ? "✓ built the graph (graft build)" : "· skipped graph build");
|
|
989
1105
|
if (!wantStatusline)
|
|
990
|
-
|
|
1106
|
+
say("· skipped Claude Code statusLine (--no-statusline)");
|
|
991
1107
|
for (const w of res.warnings)
|
|
992
|
-
|
|
1108
|
+
say(`⚠ ${w}`);
|
|
993
1109
|
}
|
|
994
1110
|
// `ids` is already resolved, so hosts init is always driven by an explicit
|
|
995
1111
|
// list — never by its own detection fallback.
|
|
@@ -1003,14 +1119,14 @@ function wireTarget(repo, ids, ctx) {
|
|
|
1003
1119
|
global: opts.global,
|
|
1004
1120
|
});
|
|
1005
1121
|
for (const w of r.written)
|
|
1006
|
-
|
|
1122
|
+
say(`✓ ${w.id}: ${w.path} (${w.action})`);
|
|
1007
1123
|
for (const m of r.mcp)
|
|
1008
|
-
|
|
1124
|
+
say(`✓ mcp ${m.id}: ${m.path} (${m.action})`);
|
|
1009
1125
|
for (const h of r.hooks)
|
|
1010
|
-
|
|
1126
|
+
say(`✓ hook ${h.id}: ${h.path} (${h.action})`);
|
|
1011
1127
|
// Only worth saying when there was actually something out-of-repo to skip.
|
|
1012
1128
|
if (opts.global === false && selectedWrites(plan, ids).some((w) => w.scope === "global"))
|
|
1013
|
-
|
|
1129
|
+
say("· skipped out-of-repo writes (--no-global)");
|
|
1014
1130
|
}
|
|
1015
1131
|
// Record WHICH graft wrote this repo's agent files, and under which flags.
|
|
1016
1132
|
// Every entry point compares this against the running binary and re-writes
|
|
@@ -1026,10 +1142,12 @@ function wireTarget(repo, ids, ctx) {
|
|
|
1026
1142
|
// Every host's wiring points at graft/, so the graph is built whatever was
|
|
1027
1143
|
// selected — not only when Claude Code is in the list (runInit does its own).
|
|
1028
1144
|
if (!wantClaude) {
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1145
|
+
if (!quiet)
|
|
1146
|
+
say(buildGraphIfMissing(repo, { build: opts.build, cliPath })
|
|
1147
|
+
? "✓ built the graph (graft build)"
|
|
1148
|
+
: "· skipped graph build");
|
|
1032
1149
|
}
|
|
1150
|
+
return report;
|
|
1033
1151
|
}
|
|
1034
1152
|
/** Group a retraction report by host, so the output reads as "what leaves each agent". */
|
|
1035
1153
|
function formatRetractions(rs, apply) {
|
|
@@ -1093,14 +1211,19 @@ const brain = program
|
|
|
1093
1211
|
.command("trail")
|
|
1094
1212
|
.description("The Trail attached to this repo: the rules mined from its own history");
|
|
1095
1213
|
/**
|
|
1096
|
-
* Get this repo a
|
|
1214
|
+
* Get this repo a trail from the terminal, by sending the user through signup
|
|
1097
1215
|
* in their browser and catching the handoff on loopback.
|
|
1098
1216
|
*
|
|
1099
1217
|
* Returns the link, already saved, or null when the user should be left alone —
|
|
1100
1218
|
* every failure prints its own reason first, because the caller only needs to
|
|
1101
|
-
* know whether to carry on.
|
|
1219
|
+
* know whether to carry on. Nothing about the repository has been read or sent
|
|
1220
|
+
* by then, and every message says so.
|
|
1102
1221
|
*/
|
|
1103
1222
|
async function signUpForBrain(repo, slug) {
|
|
1223
|
+
// No terminal means an agent is running this, and a loopback listener cannot
|
|
1224
|
+
// outlive the command it is waiting in: see signUpWithoutTerminal.
|
|
1225
|
+
if (!process.stderr.isTTY)
|
|
1226
|
+
return signUpWithoutTerminal(repo, slug);
|
|
1104
1227
|
const handoff = await startHandoff();
|
|
1105
1228
|
const url = signupUrl({ repo: slug, port: handoff.port, state: handoff.state });
|
|
1106
1229
|
const startedAt = Date.now();
|
|
@@ -1108,37 +1231,96 @@ async function signUpForBrain(repo, slug) {
|
|
|
1108
1231
|
// the outcome is the one thing a terminal handoff can lose: a user who reads
|
|
1109
1232
|
// the URL and walks away kills the process, and only an event already on disk
|
|
1110
1233
|
// survives that. This is the denominator; `brain_signup_settled` is not.
|
|
1111
|
-
track("brain_signup_opened", {}, { repo });
|
|
1234
|
+
track("brain_signup_opened", { mode: "terminal" }, { repo });
|
|
1112
1235
|
// Printed before the browser opens, and printed whether or not it opens: on a
|
|
1113
1236
|
// remote shell nothing can open, and on a desktop the window sometimes lands
|
|
1114
1237
|
// behind the terminal. The URL is the thing that always works.
|
|
1115
|
-
console.error(`· ${slug} has no
|
|
1238
|
+
console.error(`· ${slug} has no trail yet — opening your browser to make one:`);
|
|
1116
1239
|
console.error(` ${url}`);
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1240
|
+
openBrowser(url);
|
|
1241
|
+
const spinner = startSpinner("waiting for you to finish signing up · Ctrl-C to stop");
|
|
1242
|
+
// Ctrl-C while waiting is a person changing their mind, not a crash: say that
|
|
1243
|
+
// nothing was sent, and leave with the usual interrupted exit code.
|
|
1244
|
+
let onSigint;
|
|
1245
|
+
const stopped = new Promise((resolve) => {
|
|
1246
|
+
onSigint = () => resolve("stopped");
|
|
1247
|
+
process.once("SIGINT", onSigint);
|
|
1248
|
+
});
|
|
1249
|
+
const got = await Promise.race([handoff.wait(), stopped]);
|
|
1250
|
+
if (onSigint)
|
|
1251
|
+
process.off("SIGINT", onSigint);
|
|
1252
|
+
spinner.stop();
|
|
1253
|
+
if (got === "stopped") {
|
|
1125
1254
|
handoff.close();
|
|
1255
|
+
console.error("· stopped — nothing was sent; run graft trail push again when you're ready");
|
|
1256
|
+
track("brain_signup_settled", { outcome: "stopped", mode: "terminal", duration_bucket: durationBucket(Date.now() - startedAt) }, { repo });
|
|
1257
|
+
process.exitCode = 130;
|
|
1126
1258
|
return null;
|
|
1127
1259
|
}
|
|
1128
|
-
openBrowser(url);
|
|
1129
|
-
console.error("· waiting for you to finish signing up…");
|
|
1130
|
-
const got = await handoff.wait();
|
|
1131
1260
|
if ("error" in got) {
|
|
1132
1261
|
console.error(`✗ ${got.error}`);
|
|
1133
1262
|
// The category, never the sentence: `got.error` names the repo and the link.
|
|
1134
|
-
track("brain_signup_settled", { outcome: got.reason, duration_bucket: durationBucket(Date.now() - startedAt) }, { repo });
|
|
1263
|
+
track("brain_signup_settled", { outcome: got.reason, mode: "terminal", duration_bucket: durationBucket(Date.now() - startedAt) }, { repo });
|
|
1135
1264
|
return null;
|
|
1136
1265
|
}
|
|
1137
1266
|
writeLink(repo, got.link);
|
|
1138
|
-
console.error(`✓
|
|
1139
|
-
track("brain_signup_settled", { outcome: "linked", duration_bucket: durationBucket(Date.now() - startedAt) }, { repo });
|
|
1267
|
+
console.error(`✓ trail connected · ${slug} — your browser shows it building`);
|
|
1268
|
+
track("brain_signup_settled", { outcome: "linked", mode: "terminal", duration_bucket: durationBucket(Date.now() - startedAt) }, { repo });
|
|
1140
1269
|
return got.link;
|
|
1141
1270
|
}
|
|
1271
|
+
/**
|
|
1272
|
+
* The same sign-up when an agent runs the push, in two runs of one command.
|
|
1273
|
+
*
|
|
1274
|
+
* An agent shows the person nothing until a command exits, so the first run
|
|
1275
|
+
* opens the sign-up page, saves the state in its link and exits at once, with
|
|
1276
|
+
* a line telling the agent what to do next. The run after that finds the saved
|
|
1277
|
+
* state and asks Trail for the trail it made, for up to AGENT_WAIT_MS, then
|
|
1278
|
+
* carries on with the push. Still not signed up by then, it says so and the
|
|
1279
|
+
* agent runs it again. Nothing about the repository is read or sent until the
|
|
1280
|
+
* trail is linked.
|
|
1281
|
+
*/
|
|
1282
|
+
async function signUpWithoutTerminal(repo, slug) {
|
|
1283
|
+
const startedAt = Date.now();
|
|
1284
|
+
const settled = (outcome) => track("brain_signup_settled", { outcome, mode: "agent", duration_bucket: durationBucket(Date.now() - startedAt) }, { repo });
|
|
1285
|
+
const pending = readPendingSignup(repo, slug, PENDING_SIGNUP_TTL_MS);
|
|
1286
|
+
if (!pending) {
|
|
1287
|
+
const state = newSignupState();
|
|
1288
|
+
writePendingSignup(repo, { state, repo: slug, createdAt: Date.now() });
|
|
1289
|
+
const url = signupUrl({ repo: slug, state });
|
|
1290
|
+
track("brain_signup_opened", { mode: "agent" }, { repo });
|
|
1291
|
+
openBrowser(url);
|
|
1292
|
+
console.error(`· ${slug} has no trail yet — opened Trail's sign-up page in the browser:`);
|
|
1293
|
+
console.error(` ${url}`);
|
|
1294
|
+
console.error("· nothing has been read or sent yet.");
|
|
1295
|
+
console.error("· next: ask the user to sign up in the tab that just opened (or at the link above), then run `graft trail push` again straight away — it waits for the sign-up to finish and carries on with the push. Don't wait for them to reply first.");
|
|
1296
|
+
settled("agent_link_opened");
|
|
1297
|
+
process.exitCode = 1;
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
const url = signupUrl({ repo: slug, state: pending.state });
|
|
1301
|
+
console.error(`· waiting up to ${Math.round(AGENT_WAIT_MS / 1000)} s for the sign-up for ${slug} to finish:`);
|
|
1302
|
+
console.error(` ${url}`);
|
|
1303
|
+
const got = await waitForSignup(pending.state, apiBaseUrl());
|
|
1304
|
+
if ("link" in got) {
|
|
1305
|
+
clearPendingSignup(repo);
|
|
1306
|
+
writeLink(repo, got.link);
|
|
1307
|
+
console.error(`✓ trail connected · ${slug}`);
|
|
1308
|
+
settled("linked");
|
|
1309
|
+
return got.link;
|
|
1310
|
+
}
|
|
1311
|
+
if ("pending" in got) {
|
|
1312
|
+
console.error("· not signed up yet — nothing has been read or sent.");
|
|
1313
|
+
console.error("· next: run `graft trail push` again to keep waiting. The link above stays valid for about 15 minutes.");
|
|
1314
|
+
settled("still_waiting");
|
|
1315
|
+
process.exitCode = 1;
|
|
1316
|
+
return null;
|
|
1317
|
+
}
|
|
1318
|
+
clearPendingSignup(repo);
|
|
1319
|
+
console.error(`✗ ${got.error}`);
|
|
1320
|
+
settled(got.reason);
|
|
1321
|
+
process.exitCode = 1;
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1142
1324
|
brain
|
|
1143
1325
|
.command("connect")
|
|
1144
1326
|
.description("Attach a brain to this repo and pull its rules")
|
|
@@ -1171,94 +1353,106 @@ brain
|
|
|
1171
1353
|
});
|
|
1172
1354
|
brain
|
|
1173
1355
|
.command("pull")
|
|
1174
|
-
.description("
|
|
1356
|
+
.description("Refresh the trail's rules and write every change you accepted in Trail into this repo's context files")
|
|
1175
1357
|
.argument("[dir]", "target repo directory", ".")
|
|
1176
|
-
.
|
|
1358
|
+
.option("--dry-run", "show what would change without writing anything or telling Trail")
|
|
1359
|
+
.action(async (dir, opts) => {
|
|
1360
|
+
await runTrailPullCommand(dir, opts);
|
|
1361
|
+
});
|
|
1362
|
+
/** `graft trail pull`, and `graft claude-md pull` which is now the same thing. */
|
|
1363
|
+
async function runTrailPullCommand(dir, opts) {
|
|
1177
1364
|
const repo = resolve(dir);
|
|
1178
|
-
const
|
|
1179
|
-
if (!
|
|
1180
|
-
console.error("
|
|
1181
|
-
return;
|
|
1182
|
-
}
|
|
1183
|
-
if (res.warning) {
|
|
1184
|
-
console.error(`⚠ ${res.warning}`);
|
|
1365
|
+
const link = readLink(repo);
|
|
1366
|
+
if (!link) {
|
|
1367
|
+
console.error("✗ this repo has no trail yet — run graft trail push first");
|
|
1185
1368
|
process.exitCode = 1;
|
|
1186
1369
|
return;
|
|
1187
1370
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1371
|
+
const stamp = readStamp(repo);
|
|
1372
|
+
const wired = [...new Set([...(stamp?.hosts ?? []), ...wiredHostIds(repo)])];
|
|
1373
|
+
const code = await runTrailPull(repo, link, { home: homedir(), wired, dryRun: opts.dryRun });
|
|
1374
|
+
if (code !== 0)
|
|
1375
|
+
process.exitCode = code;
|
|
1376
|
+
}
|
|
1192
1377
|
brain
|
|
1193
1378
|
.command("push")
|
|
1194
|
-
.description("Read THIS repo on your machine and build its
|
|
1379
|
+
.description("Read THIS repo on your machine and build its trail — no GitHub App, works on private repos")
|
|
1195
1380
|
.argument("[dir]", "target repo directory", ".")
|
|
1196
1381
|
.option("--no-approve", "leave the mined rules as drafts for review")
|
|
1197
1382
|
.option("--no-watch", "return as soon as the push is sent, without following the build")
|
|
1383
|
+
.option("--verbose", "print each step's counts and every build stage as it finishes")
|
|
1198
1384
|
.action(async (dir, opts) => {
|
|
1199
1385
|
const repo = resolve(dir);
|
|
1386
|
+
const tty = Boolean(process.stderr.isTTY);
|
|
1387
|
+
const verbose = opts.verbose === true;
|
|
1200
1388
|
// Resolved before the link, because an unlinked repo now signs up for a
|
|
1201
|
-
//
|
|
1389
|
+
// trail and Trail creates that trail FOR a named repository. Without a slug
|
|
1202
1390
|
// there is nothing to name it after — and the digest builder would fail on
|
|
1203
1391
|
// the same missing remote a moment later regardless.
|
|
1204
1392
|
const here = repoSlugFromGit(repo);
|
|
1205
1393
|
let link = readLink(repo);
|
|
1394
|
+
// Set when this run opened the browser, which then sits on Trail's build
|
|
1395
|
+
// page: the closing lines point at that tab instead of printing a new link.
|
|
1396
|
+
let browserOpen = false;
|
|
1206
1397
|
if (!link) {
|
|
1207
1398
|
if (!here) {
|
|
1208
|
-
console.error("✗ this directory has no GitHub
|
|
1399
|
+
console.error("✗ this directory has no GitHub origin remote — graft can only push a GitHub repository today");
|
|
1209
1400
|
process.exitCode = 1;
|
|
1210
1401
|
return;
|
|
1211
1402
|
}
|
|
1212
1403
|
const signedUp = await signUpForBrain(repo, `${here.owner}/${here.name}`);
|
|
1213
1404
|
if (!signedUp) {
|
|
1214
|
-
process.exitCode
|
|
1405
|
+
if (!process.exitCode)
|
|
1406
|
+
process.exitCode = 1;
|
|
1215
1407
|
return;
|
|
1216
1408
|
}
|
|
1217
1409
|
link = signedUp;
|
|
1410
|
+
browserOpen = true;
|
|
1218
1411
|
}
|
|
1219
|
-
// What the website said this
|
|
1220
|
-
// standing in the wrong checkout costs a message rather than a
|
|
1412
|
+
// What the website said this trail is for. Checked BEFORE any reading, so
|
|
1413
|
+
// standing in the wrong checkout costs a message rather than a trail full
|
|
1221
1414
|
// of another repository's rules — a mistake that is silent afterwards,
|
|
1222
1415
|
// because the rules look perfectly plausible, just not about your code.
|
|
1223
1416
|
const expected = await fetchExpectedRepo(link);
|
|
1224
1417
|
if (expected && here && !sameRepo(expected.slug, `${here.owner}/${here.name}`)) {
|
|
1225
|
-
console.error(`✗ this
|
|
1226
|
-
console.error(` cd into ${expected.slug} and run this again, or attach a different
|
|
1418
|
+
console.error(`✗ this trail is for ${expected.slug}, but you are in ${here.owner}/${here.name}`);
|
|
1419
|
+
console.error(` cd into ${expected.slug} and run this again, or attach a different trail here.`);
|
|
1420
|
+
process.exitCode = 1;
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
const ctx = pushContext(repo);
|
|
1424
|
+
if (!ctx) {
|
|
1425
|
+
console.error("✗ this directory has no GitHub origin remote — graft can only push a GitHub repository today");
|
|
1227
1426
|
process.exitCode = 1;
|
|
1228
1427
|
return;
|
|
1229
1428
|
}
|
|
1230
|
-
const
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
//
|
|
1234
|
-
//
|
|
1235
|
-
// the
|
|
1236
|
-
// at once, reusing the threads the early one already fetched.
|
|
1429
|
+
const firstPush = !expected || expected.ruleCount === 0;
|
|
1430
|
+
// The instruction files and the newest pull requests go first, when the
|
|
1431
|
+
// trail takes them: its suggestions start from those while the rest of the
|
|
1432
|
+
// history is still being read here. In the background, from here on — the
|
|
1433
|
+
// picker, the graph build and the full read all happen while it goes, and
|
|
1434
|
+
// the full read reuses every GitHub request it made.
|
|
1237
1435
|
let early = Promise.resolve();
|
|
1238
|
-
let knownThreads = [];
|
|
1239
1436
|
if (expected?.earlyUpload) {
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
// A repo graft was never set up in gets `graft init` first
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1252
|
-
// so a non-interactive push only says what to run.
|
|
1437
|
+
early = (async () => {
|
|
1438
|
+
const first = await buildEarlyDigest(repo, { context: ctx });
|
|
1439
|
+
if (!first)
|
|
1440
|
+
return;
|
|
1441
|
+
const ok = await pushEarlyDigest(link, first.digest, fetch, { gzip: expected.gzipUpload });
|
|
1442
|
+
if (ok && verbose)
|
|
1443
|
+
console.error("· sent the instruction files and the newest pull requests first, so suggestions start now");
|
|
1444
|
+
})().catch(() => undefined);
|
|
1445
|
+
}
|
|
1446
|
+
// A repo graft was never set up in gets `graft init` first. Same picker as
|
|
1447
|
+
// init: it is how someone agrees to what gets written, so a
|
|
1448
|
+
// non-interactive push only says what to run.
|
|
1253
1449
|
if (wiredHostIds(repo).length === 0) {
|
|
1254
|
-
if (process.stdin.isTTY &&
|
|
1255
|
-
|
|
1256
|
-
await
|
|
1257
|
-
console.error("· graft is not set up in this repo yet — setting it up first, the same as `graft init`");
|
|
1258
|
-
await runInitCommand(repo, { build: true, mcp: true, hooks: true, statusline: true, global: true }, { epilogue: false });
|
|
1450
|
+
if (process.stdin.isTTY && tty) {
|
|
1451
|
+
console.error("· graft isn't set up here yet — pick the agents your team uses:");
|
|
1452
|
+
await runInitCommand(repo, { build: true, mcp: true, hooks: true, statusline: true, global: true, verbose }, { epilogue: false, push: !verbose });
|
|
1259
1453
|
}
|
|
1260
1454
|
else {
|
|
1261
|
-
console.error("· graft
|
|
1455
|
+
console.error("· graft isn't set up here — run graft init so your agents read these rules");
|
|
1262
1456
|
}
|
|
1263
1457
|
}
|
|
1264
1458
|
// The graph is what symbol anchors are resolved against, so a rule mined
|
|
@@ -1266,126 +1460,130 @@ brain
|
|
|
1266
1460
|
// its rules simply govern the repo rather than a symbol in it.
|
|
1267
1461
|
const graph = loadGraphCached(contextDirFor(repo, program.opts().dir));
|
|
1268
1462
|
if (!graph)
|
|
1269
|
-
console.error("· no graph yet — run
|
|
1270
|
-
const
|
|
1271
|
-
await
|
|
1463
|
+
console.error("· no graph yet — run graft build so rules can be anchored to symbols");
|
|
1464
|
+
const reading = startSpinner(`reading ${ctx.owner}/${ctx.name} · commits, pull-request discussion and the docs in the tree`);
|
|
1465
|
+
const built = await buildLocalDigest(repo, graph, { autoApprove: opts.approve !== false, context: ctx });
|
|
1272
1466
|
if ("error" in built) {
|
|
1467
|
+
reading.stop();
|
|
1273
1468
|
console.error(`✗ ${built.error}`);
|
|
1274
1469
|
process.exitCode = 1;
|
|
1275
1470
|
return;
|
|
1276
1471
|
}
|
|
1277
1472
|
const d = built.digest;
|
|
1473
|
+
reading.update("sending it to Trail");
|
|
1474
|
+
await early;
|
|
1475
|
+
const sent = await pushDigest(link, d, fetch, uploadCaps(expected));
|
|
1476
|
+
reading.stop();
|
|
1278
1477
|
if (built.warning)
|
|
1279
1478
|
console.error(`⚠ ${built.warning}`);
|
|
1280
|
-
console.error(`· ${d.commits.length} commits, ${d.threads.length} discussions, ${d.symbols.length} symbols, ${d.sources.length} stated sources`);
|
|
1281
|
-
const sent = await pushDigest(link, d);
|
|
1282
1479
|
if ("error" in sent) {
|
|
1283
1480
|
console.error(`✗ ${sent.error}`);
|
|
1284
1481
|
process.exitCode = 1;
|
|
1285
1482
|
return;
|
|
1286
1483
|
}
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1484
|
+
const fmt = (x) => x.toLocaleString("en-US");
|
|
1485
|
+
const trailLabel = expected?.brainName ? `“${expected.brainName}”` : `${d.owner}/${d.name}`;
|
|
1486
|
+
console.error(`✓ sent ${fmt(d.commits.length)} commits and ${fmt(d.threads.length)} discussions to ${trailLabel} · no file contents left this machine`);
|
|
1487
|
+
if (verbose)
|
|
1488
|
+
console.error(` ${fmt(d.symbols.length)} symbols, ${fmt(d.sources.length)} stated sources`);
|
|
1489
|
+
const buildPage = brainUrl(link.brainId);
|
|
1289
1490
|
// Held rather than handed back. Everything above this line succeeded even
|
|
1290
1491
|
// in the runs that end badly: the push lands, and then the miner fails —
|
|
1291
|
-
// which is where roughly a third of production's repo
|
|
1492
|
+
// which is where roughly a third of production's repo trails die. Returning
|
|
1292
1493
|
// the prompt here is what made that invisible from this side, on CI and
|
|
1293
1494
|
// over SSH permanently so. `--no-watch` is for a caller that genuinely
|
|
1294
|
-
// wants fire-and-forget
|
|
1495
|
+
// wants fire-and-forget.
|
|
1295
1496
|
if (opts.watch === false) {
|
|
1296
|
-
console.error("·
|
|
1297
|
-
console.error(
|
|
1497
|
+
console.error("· building now — the rules reach this repo on their own; watch it at");
|
|
1498
|
+
console.error(` ${buildPage}`);
|
|
1298
1499
|
return;
|
|
1299
1500
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1501
|
+
const live = tty && !verbose;
|
|
1502
|
+
if (!live)
|
|
1503
|
+
console.error("· building the trail");
|
|
1504
|
+
const spinner = live ? startSpinner("building the trail · reaching the repository · Ctrl-C detaches") : null;
|
|
1505
|
+
let last = null;
|
|
1506
|
+
// Ctrl-C detaches: the work is server-side, and killing a watcher must
|
|
1507
|
+
// never look like killing the build.
|
|
1508
|
+
const onSigint = () => {
|
|
1509
|
+
spinner?.stop();
|
|
1510
|
+
console.error("· detached — the build keeps going; watch it at");
|
|
1511
|
+
console.error(` ${buildPage}`);
|
|
1512
|
+
process.exit(130);
|
|
1513
|
+
};
|
|
1514
|
+
process.once("SIGINT", onSigint);
|
|
1515
|
+
const outcome = await watchBuild(link, {
|
|
1516
|
+
events: expected?.events === true,
|
|
1517
|
+
stageLines: !live,
|
|
1518
|
+
write: (line) => (spinner ? spinner.print(line) : console.error(line)),
|
|
1519
|
+
onState: (view, repoState) => {
|
|
1520
|
+
last = repoState;
|
|
1521
|
+
spinner?.update(`building the trail · ${DOING_LABEL[currentStage(view)]} · Ctrl-C detaches`);
|
|
1522
|
+
},
|
|
1523
|
+
});
|
|
1524
|
+
process.off("SIGINT", onSigint);
|
|
1525
|
+
spinner?.stop();
|
|
1526
|
+
const ruleCount = rulesSoFar(last);
|
|
1527
|
+
const rules = `${fmt(ruleCount)} rule${ruleCount === 1 ? "" : "s"}`;
|
|
1528
|
+
const wired = [...new Set([...(readStamp(repo)?.hosts ?? []), ...wiredHostIds(repo)])];
|
|
1529
|
+
const closing = () => {
|
|
1530
|
+
const line = suggestionsLine(last?.suggestions, wired, !firstPush);
|
|
1531
|
+
if (!line)
|
|
1532
|
+
return;
|
|
1533
|
+
if (browserOpen) {
|
|
1534
|
+
console.error(`${line} — they are in the Trail tab in your browser`);
|
|
1535
|
+
console.error(` ${reviewUrl(link.brainId)}`);
|
|
1536
|
+
}
|
|
1537
|
+
else {
|
|
1538
|
+
console.error(line);
|
|
1539
|
+
console.error(` review: ${reviewUrl(link.brainId)}`);
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1302
1542
|
if (outcome === "completed") {
|
|
1303
|
-
console.error(
|
|
1543
|
+
console.error(`✓ trail built · ${rules}, reaching this repo on their own`);
|
|
1544
|
+
closing();
|
|
1304
1545
|
return;
|
|
1305
1546
|
}
|
|
1306
1547
|
// The ordinary ending for anything but a small repository. The history is
|
|
1307
1548
|
// mined in slices, and the prompt comes back on the first of them: mining is
|
|
1308
1549
|
// the part that fails and it has now succeeded, and the rest is filing,
|
|
1309
|
-
// which is minutes of reliable work nobody gains by watching.
|
|
1310
|
-
// already in the brain are already being served.
|
|
1550
|
+
// which is minutes of reliable work nobody gains by watching.
|
|
1311
1551
|
if (outcome === "building") {
|
|
1312
|
-
console.error(
|
|
1313
|
-
|
|
1552
|
+
console.error(firstPush
|
|
1553
|
+
? `✓ trail has its first ${rules} · they reach this repo on their own`
|
|
1554
|
+
: `✓ trail updated · ${rules}, reaching this repo on their own`);
|
|
1555
|
+
closing();
|
|
1314
1556
|
return;
|
|
1315
1557
|
}
|
|
1316
1558
|
if (outcome === "failed") {
|
|
1317
1559
|
// A non-zero exit, unlike every other ending here: this is the one case
|
|
1318
|
-
// where the work did not produce a
|
|
1319
|
-
// should hear about it the way it hears about any other failure.
|
|
1320
|
-
|
|
1560
|
+
// where the work did not produce a trail, and a CI step that ran the push
|
|
1561
|
+
// should hear about it the way it hears about any other failure. The
|
|
1562
|
+
// watcher has already printed where it stopped and why.
|
|
1321
1563
|
process.exitCode = 1;
|
|
1322
1564
|
return;
|
|
1323
1565
|
}
|
|
1324
1566
|
if (outcome === "unreachable") {
|
|
1325
|
-
console.error("·
|
|
1326
|
-
return;
|
|
1567
|
+
console.error("· lost touch with Trail — the build keeps going; watch it at the link below");
|
|
1327
1568
|
}
|
|
1328
|
-
|
|
1569
|
+
else {
|
|
1570
|
+
console.error("· still building after 15 minutes — not failed, just long; watch it at the link below");
|
|
1571
|
+
}
|
|
1572
|
+
console.error(` ${buildPage}`);
|
|
1329
1573
|
});
|
|
1574
|
+
// `graft claude-md pull` is `graft trail pull` now: the CLAUDE.md changes are
|
|
1575
|
+
// the first half of what that writes. Kept so the old name keeps working.
|
|
1330
1576
|
const claudeMd = program
|
|
1331
1577
|
.command("claude-md")
|
|
1332
|
-
.description("The CLAUDE.md changes this repo's
|
|
1578
|
+
.description("The CLAUDE.md changes this repo's trail suggested (now part of graft trail pull)");
|
|
1333
1579
|
claudeMd
|
|
1334
1580
|
.command("pull")
|
|
1335
|
-
.description("
|
|
1581
|
+
.description("Same as graft trail pull: write every change you accepted in Trail into this repo")
|
|
1336
1582
|
.argument("[dir]", "target repo directory", ".")
|
|
1337
|
-
.option("--dry-run", "show what would change without writing
|
|
1583
|
+
.option("--dry-run", "show what would change without writing anything or telling Trail")
|
|
1338
1584
|
.action(async (dir, opts) => {
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
if (!link) {
|
|
1342
|
-
console.error("✗ this repo has no brain attached — run `graft trail push` first");
|
|
1343
|
-
process.exitCode = 1;
|
|
1344
|
-
return;
|
|
1345
|
-
}
|
|
1346
|
-
const pulled = await fetchAcceptedChanges(link);
|
|
1347
|
-
if ("error" in pulled) {
|
|
1348
|
-
console.error(`✗ ${pulled.error}`);
|
|
1349
|
-
process.exitCode = 1;
|
|
1350
|
-
return;
|
|
1351
|
-
}
|
|
1352
|
-
if (pulled.changes.length === 0) {
|
|
1353
|
-
console.error("· nothing accepted yet — accept suggestions on the brain's CLAUDE.md page, then pull again");
|
|
1354
|
-
return;
|
|
1355
|
-
}
|
|
1356
|
-
// The file Trail read, or a new CLAUDE.md when the repo had none.
|
|
1357
|
-
const rel = pulled.path || "CLAUDE.md";
|
|
1358
|
-
const file = join(repo, rel);
|
|
1359
|
-
const { existsSync, readFileSync, writeFileSync } = await import("node:fs");
|
|
1360
|
-
const before = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
1361
|
-
const result = applyChanges(before, pulled.changes);
|
|
1362
|
-
for (const c of result.written)
|
|
1363
|
-
console.error(` + ${c.kind === "add" ? "new section" : "edit"} · ${c.heading}`);
|
|
1364
|
-
for (const c of result.present)
|
|
1365
|
-
console.error(` = already in the file · ${c.heading}`);
|
|
1366
|
-
for (const s of result.skipped)
|
|
1367
|
-
console.error(` ! skipped · ${s.change.heading} — ${s.why}`);
|
|
1368
|
-
if (opts.dryRun) {
|
|
1369
|
-
console.error(`· dry run: ${result.written.length} change(s) would be written to ${rel}; nothing was written`);
|
|
1370
|
-
return;
|
|
1371
|
-
}
|
|
1372
|
-
if (result.written.length > 0)
|
|
1373
|
-
writeFileSync(file, result.text);
|
|
1374
|
-
// Written now or already there: either way the page should stop listing it.
|
|
1375
|
-
const done = [...result.written, ...result.present].map((c) => c.id);
|
|
1376
|
-
const told = await markApplied(link, done);
|
|
1377
|
-
if (result.written.length > 0) {
|
|
1378
|
-
console.error(`✓ wrote ${result.written.length} change(s) to ${rel} — review and commit it like any other edit`);
|
|
1379
|
-
}
|
|
1380
|
-
else if (result.present.length > 0) {
|
|
1381
|
-
console.error(`✓ ${rel} already has every accepted change`);
|
|
1382
|
-
}
|
|
1383
|
-
if (!told)
|
|
1384
|
-
console.error("⚠ could not tell Trail which changes were written; the page may still list them");
|
|
1385
|
-
if (result.skipped.length > 0) {
|
|
1386
|
-
console.error(`· ${result.skipped.length} change(s) no longer match the local file — edit them on the CLAUDE.md page, or apply them by hand`);
|
|
1387
|
-
process.exitCode = 1;
|
|
1388
|
-
}
|
|
1585
|
+
console.error("· graft claude-md pull is now part of graft trail pull — running that");
|
|
1586
|
+
await runTrailPullCommand(dir, opts);
|
|
1389
1587
|
});
|
|
1390
1588
|
brain
|
|
1391
1589
|
.command("status")
|