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