@nanonets/graft 0.19.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/README.md +9 -4
- package/dist/app/history.d.ts +36 -1
- package/dist/app/history.d.ts.map +1 -1
- package/dist/app/history.js +75 -22
- 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/ask/ask.js +1 -1
- package/dist/brain/claude-md.d.ts +61 -0
- package/dist/brain/claude-md.d.ts.map +1 -0
- package/dist/brain/claude-md.js +171 -0
- package/dist/brain/claude-md.js.map +1 -0
- package/dist/brain/connect.d.ts +3 -3
- package/dist/brain/connect.js +7 -7
- 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 +18 -1
- package/dist/brain/link.d.ts.map +1 -1
- package/dist/brain/link.js +22 -1
- 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 +86 -3
- package/dist/brain/push.d.ts.map +1 -1
- package/dist/brain/push.js +149 -36
- 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 +103 -13
- 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 +77 -4
- 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/claude/init.d.ts.map +1 -1
- package/dist/claude/init.js +3 -1
- package/dist/claude/init.js.map +1 -1
- 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 +462 -123
- package/dist/cli.js.map +1 -1
- package/dist/legacy-args.d.ts +12 -0
- package/dist/legacy-args.d.ts.map +1 -0
- package/dist/legacy-args.js +31 -0
- package/dist/legacy-args.js.map +1 -0
- package/dist/telemetry/contract.d.ts +17 -4
- package/dist/telemetry/contract.d.ts.map +1 -1
- package/dist/telemetry/contract.js +25 -7
- package/dist/telemetry/contract.js.map +1 -1
- package/dist/upkeep.d.ts +1 -1
- package/dist/upkeep.js +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,29 +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 { buildLocalDigest, fetchExpectedRepo, pushDigest, repoSlugFromGit, sameRepo } from "./brain/push.js";
|
|
23
|
-
import { readLink, writeLink } from "./brain/link.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
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
|
+
import { withLegacyNames } from "./legacy-args.js";
|
|
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";
|
|
26
30
|
import { contextDirFor } from "./context/node-file.js";
|
|
27
31
|
import { loadGraphCached } from "./graph/load.js";
|
|
28
32
|
import { ensureFreshChildren, ensureFreshGraph, refreshNote } from "./graph/refresh.js";
|
|
29
33
|
import { isWorkspaceBuildRoot, readWorkspace } from "./graph/workspace.js";
|
|
30
|
-
import { nearestGraftRoot } from "./graph/root.js";
|
|
34
|
+
import { hasGraftIndex, nearestGraftRoot } from "./graph/root.js";
|
|
31
35
|
import { unsupportedExtensions, supportedExtensions } from "./graph/source-files.js";
|
|
32
36
|
import { discoverWorkspaceChildren } from "./graph/scopes.js";
|
|
33
37
|
import { runWorkspaceAsk, runWorkspaceBuild, runWorkspaceCallers, runWorkspaceCheck, runWorkspaceGrep, runWorkspaceMap, } from "./graph/workspace-cli.js";
|
|
34
38
|
import { formatInitEpilogue } from "./cli-epilogue.js";
|
|
35
39
|
import { planInit, selectedWrites } from "./hosts/plan.js";
|
|
36
40
|
import { planRetract, runRetract, changed } from "./hosts/retract.js";
|
|
37
|
-
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";
|
|
38
43
|
import { homedir } from "node:os";
|
|
39
44
|
import { formatUpgradeReport, formatVersionReport, getNpmViewVersion, readCurrentVersion, runUpgrade } from "./cli-meta.js";
|
|
40
45
|
import { patchBuildConfig } from "./util/state.js";
|
|
41
46
|
import { normalizePathPrefix } from "./util/paths.js";
|
|
42
47
|
import { latestSession, formatSessionStats, sessionInputRate } from "./claude/session-metrics.js";
|
|
43
48
|
import { setInputRate } from "./context/savings.js";
|
|
44
|
-
import { formatUpdateNudge, maybeRefreshInBackground, readUpdateCache, refreshUpdateCache, writeStamp } from "./upkeep.js";
|
|
49
|
+
import { formatUpdateNudge, maybeRefreshInBackground, readStamp, readUpdateCache, refreshUpdateCache, wiredHostIds, writeStamp } from "./upkeep.js";
|
|
45
50
|
import { errorCode, filesBucket, durationBucket, formatDebug, formatStatus, firstRunNotice, isTrackedCommand, langsValue, offReason, maybeFlushInBackground, patchState, runFlush, track, trackFirstRunIfNew, } from "./telemetry/index.js";
|
|
46
51
|
const program = new Command();
|
|
47
52
|
const currentVersion = readCurrentVersion(import.meta.url);
|
|
@@ -817,22 +822,42 @@ program
|
|
|
817
822
|
.option("--dry-run", "print every file init would touch, then exit without writing")
|
|
818
823
|
.option("-y, --yes", "skip the picker and wire every detected agent (the pre-0.8 default)")
|
|
819
824
|
.option("--no-global", "skip writes outside this repo (the ~/.codex/ config + hooks)")
|
|
820
|
-
.option("--
|
|
825
|
+
.option("--trail <handoff>", "attach a Trail: <brainId>:<token> (or a bare id with GRAFT_BRAIN_TOKEN set)")
|
|
826
|
+
.option("--verbose", "print every file written, the graph build's own output and the closing banner")
|
|
821
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
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* `graft init`. Also run by `graft trail push` on a repo graft has not been set
|
|
842
|
+
* up in — the Trail pages start people at push, and a trail whose rules no
|
|
843
|
+
* coding agent reads is only half set up. `epilogue: false` leaves out the
|
|
844
|
+
* closing banner when push is going to keep talking.
|
|
845
|
+
*/
|
|
846
|
+
async function runInitCommand(dir, opts, how = {}) {
|
|
822
847
|
if (opts.listAgents) {
|
|
823
848
|
for (const id of [...hostIds(), "claude"])
|
|
824
849
|
console.log(id);
|
|
825
|
-
return;
|
|
850
|
+
return null;
|
|
826
851
|
}
|
|
827
852
|
// Parsed before anything is written: a mistyped handoff should cost the user
|
|
828
853
|
// an error, not a half-wired repo they have to `graft uninstall` out of.
|
|
829
854
|
let brainLink;
|
|
830
|
-
if (opts.
|
|
831
|
-
const parsed = parseBrainArg(opts.
|
|
855
|
+
if (opts.trail) {
|
|
856
|
+
const parsed = parseBrainArg(opts.trail);
|
|
832
857
|
if ("error" in parsed) {
|
|
833
|
-
console.error(`✗ --
|
|
858
|
+
console.error(`✗ --trail: ${parsed.error}`);
|
|
834
859
|
process.exitCode = 1;
|
|
835
|
-
return;
|
|
860
|
+
return null;
|
|
836
861
|
}
|
|
837
862
|
brainLink = parsed;
|
|
838
863
|
}
|
|
@@ -872,17 +897,19 @@ program
|
|
|
872
897
|
const reason = offReason();
|
|
873
898
|
const picked = await runPicker(plan, repo, home, {
|
|
874
899
|
offerTelemetry: reason === null || reason === "disabled",
|
|
900
|
+
// From push, the line above the picker already asks the question.
|
|
901
|
+
...(how.push ? { title: null } : {}),
|
|
875
902
|
});
|
|
876
903
|
if (picked === null) {
|
|
877
904
|
console.error("· cancelled — nothing written");
|
|
878
|
-
return;
|
|
905
|
+
return null;
|
|
879
906
|
}
|
|
880
907
|
ids = picked.hosts;
|
|
881
908
|
consent = picked.telemetry;
|
|
882
909
|
}
|
|
883
910
|
else {
|
|
884
911
|
console.error(formatNonInteractiveHelp(detectedIds));
|
|
885
|
-
return;
|
|
912
|
+
return null;
|
|
886
913
|
}
|
|
887
914
|
// The picker's answer, recorded before anything is wired: a user who
|
|
888
915
|
// unchecked the row must not have this run's init_completed sent.
|
|
@@ -903,34 +930,121 @@ program
|
|
|
903
930
|
console.error(formatPlan(plan, ids, repo, home));
|
|
904
931
|
for (const child of children)
|
|
905
932
|
console.error(`\n— ${child}/ (workspace child)\n` + formatPlan(planInit(join(repo, child), { home }), ids, join(repo, child), home));
|
|
906
|
-
return;
|
|
933
|
+
return null;
|
|
907
934
|
}
|
|
908
935
|
if (ids.length === 0) {
|
|
909
936
|
console.error("· no agents selected — nothing written");
|
|
910
|
-
return;
|
|
937
|
+
return null;
|
|
911
938
|
}
|
|
912
939
|
const wantClaude = ids.includes("claude");
|
|
913
940
|
const cliPath = fileURLToPath(import.meta.url);
|
|
914
941
|
if (children.length)
|
|
915
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 = [];
|
|
916
947
|
for (const target of targets) {
|
|
917
|
-
if (target !== repo)
|
|
948
|
+
if (verbose && target !== repo)
|
|
918
949
|
console.error(`\n— ${relative(repo, target)}/`);
|
|
919
|
-
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
|
+
}
|
|
920
1006
|
}
|
|
921
1007
|
// The brain comes last, after the graph exists: its rules are anchored to
|
|
922
|
-
// symbols, and `graft
|
|
1008
|
+
// symbols, and `graft trail status` can only report how many of them resolve
|
|
923
1009
|
// once there is a graph to resolve them against.
|
|
924
1010
|
if (brainLink) {
|
|
925
1011
|
const res = await connectBrain(repo, brainLink, { home, ids });
|
|
926
|
-
if (
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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`);
|
|
934
1048
|
}
|
|
935
1049
|
// One epilogue for the whole run. A workspace parent holds no nodes of its
|
|
936
1050
|
// own, so the totals come from the children — the graph the user actually got.
|
|
@@ -938,18 +1052,30 @@ program
|
|
|
938
1052
|
const graphs = (children.length ? children.map((c) => join(repo, c)) : [repo])
|
|
939
1053
|
.map((d) => loadGraphCached(contextDirFor(d, children.length ? undefined : globalDir)))
|
|
940
1054
|
.filter((g) => g !== null);
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1055
|
+
if (verbose && how.epilogue !== false)
|
|
1056
|
+
console.error("\n" +
|
|
1057
|
+
formatInitEpilogue({
|
|
1058
|
+
graphBuilt: graphs.length > 0,
|
|
1059
|
+
nodes: graphs.reduce((n, g) => n + g.meta.nodeCount, 0),
|
|
1060
|
+
edges: graphs.reduce((n, g) => n + g.meta.edgeCount, 0),
|
|
1061
|
+
}));
|
|
947
1062
|
// Sorted so `claude,cursor` and `cursor,claude` aggregate as one value.
|
|
948
1063
|
track("init_completed", { agents: [...ids].sort().join(","), consent: consent === undefined ? "unasked" : String(consent) }, { repo });
|
|
949
|
-
}
|
|
1064
|
+
return { ids };
|
|
1065
|
+
}
|
|
950
1066
|
/** One repo's worth of `init` writes — the parent, then each workspace child. */
|
|
951
1067
|
function wireTarget(repo, ids, ctx) {
|
|
952
|
-
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
|
+
};
|
|
953
1079
|
const wantStatusline = statuslineWanted({ statusline: opts.statusline });
|
|
954
1080
|
// Converge, don't just add. init writes the selected hosts; without this it
|
|
955
1081
|
// never touches the rest, so a repo wired by an older version (or by the same
|
|
@@ -959,27 +1085,28 @@ function wireTarget(repo, ids, ctx) {
|
|
|
959
1085
|
// be rewritten, and the graph cache is kept (init is one step from using it).
|
|
960
1086
|
const retracted = changed(runRetract(repo, { home, apply: true, global: opts.global, cache: false, exclude: ids })).filter((r) => r.action !== "skipped-unparseable");
|
|
961
1087
|
for (const r of retracted)
|
|
962
|
-
|
|
1088
|
+
say(`- removed ${r.path} (${r.what}) — agent not selected`);
|
|
1089
|
+
report.retracted = [...new Set(retracted.map((r) => r.hostId))];
|
|
963
1090
|
if (wantClaude) {
|
|
964
1091
|
// `global`/`home` are threaded through alongside `statusline`: the claude layer
|
|
965
1092
|
// writes under `~/.claude` now (hosts/claude-global.ts), so --no-global has to
|
|
966
1093
|
// reach it or the flag would silently mean "no out-of-repo writes, except three".
|
|
967
1094
|
const res = runInit(repo, { build: opts.build, cliPath, statusline: wantStatusline, global: opts.global, home });
|
|
968
|
-
|
|
1095
|
+
say(`✓ wrote ${res.settingsPath}`);
|
|
969
1096
|
for (const s of res.shims)
|
|
970
|
-
|
|
971
|
-
|
|
1097
|
+
say(`✓ wrote ${s}`);
|
|
1098
|
+
say(`✓ wrote ${res.skill}`);
|
|
972
1099
|
if (res.mcp.action === "skipped-unparseable")
|
|
973
|
-
|
|
1100
|
+
say(`⚠ .mcp.json: ${res.mcp.path} left unchanged (not valid JSON) — add the graft server manually`);
|
|
974
1101
|
else if (res.mcp.action === "unchanged")
|
|
975
|
-
|
|
1102
|
+
say(`· mcp claude: ${res.mcp.path} (already registered)`);
|
|
976
1103
|
else
|
|
977
|
-
|
|
978
|
-
|
|
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");
|
|
979
1106
|
if (!wantStatusline)
|
|
980
|
-
|
|
1107
|
+
say("· skipped Claude Code statusLine (--no-statusline)");
|
|
981
1108
|
for (const w of res.warnings)
|
|
982
|
-
|
|
1109
|
+
say(`⚠ ${w}`);
|
|
983
1110
|
}
|
|
984
1111
|
// `ids` is already resolved, so hosts init is always driven by an explicit
|
|
985
1112
|
// list — never by its own detection fallback.
|
|
@@ -993,14 +1120,14 @@ function wireTarget(repo, ids, ctx) {
|
|
|
993
1120
|
global: opts.global,
|
|
994
1121
|
});
|
|
995
1122
|
for (const w of r.written)
|
|
996
|
-
|
|
1123
|
+
say(`✓ ${w.id}: ${w.path} (${w.action})`);
|
|
997
1124
|
for (const m of r.mcp)
|
|
998
|
-
|
|
1125
|
+
say(`✓ mcp ${m.id}: ${m.path} (${m.action})`);
|
|
999
1126
|
for (const h of r.hooks)
|
|
1000
|
-
|
|
1127
|
+
say(`✓ hook ${h.id}: ${h.path} (${h.action})`);
|
|
1001
1128
|
// Only worth saying when there was actually something out-of-repo to skip.
|
|
1002
1129
|
if (opts.global === false && selectedWrites(plan, ids).some((w) => w.scope === "global"))
|
|
1003
|
-
|
|
1130
|
+
say("· skipped out-of-repo writes (--no-global)");
|
|
1004
1131
|
}
|
|
1005
1132
|
// Record WHICH graft wrote this repo's agent files, and under which flags.
|
|
1006
1133
|
// Every entry point compares this against the running binary and re-writes
|
|
@@ -1016,10 +1143,12 @@ function wireTarget(repo, ids, ctx) {
|
|
|
1016
1143
|
// Every host's wiring points at graft/, so the graph is built whatever was
|
|
1017
1144
|
// selected — not only when Claude Code is in the list (runInit does its own).
|
|
1018
1145
|
if (!wantClaude) {
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1146
|
+
if (!quiet)
|
|
1147
|
+
say(buildGraphIfMissing(repo, { build: opts.build, cliPath })
|
|
1148
|
+
? "✓ built the graph (graft build)"
|
|
1149
|
+
: "· skipped graph build");
|
|
1022
1150
|
}
|
|
1151
|
+
return report;
|
|
1023
1152
|
}
|
|
1024
1153
|
/** Group a retraction report by host, so the output reads as "what leaves each agent". */
|
|
1025
1154
|
function formatRetractions(rs, apply) {
|
|
@@ -1078,18 +1207,24 @@ program
|
|
|
1078
1207
|
? `\n⚠ ${bad.length} file(s) could not be parsed and were left as-is — see above.`
|
|
1079
1208
|
: "\n✓ graft fully removed. `graft init` re-wires from scratch.");
|
|
1080
1209
|
});
|
|
1210
|
+
// `graft trail …` still works: see legacy-args.ts.
|
|
1081
1211
|
const brain = program
|
|
1082
|
-
.command("
|
|
1083
|
-
.description("The Trail
|
|
1212
|
+
.command("trail")
|
|
1213
|
+
.description("The Trail attached to this repo: the rules mined from its own history");
|
|
1084
1214
|
/**
|
|
1085
|
-
* Get this repo a
|
|
1215
|
+
* Get this repo a trail from the terminal, by sending the user through signup
|
|
1086
1216
|
* in their browser and catching the handoff on loopback.
|
|
1087
1217
|
*
|
|
1088
1218
|
* Returns the link, already saved, or null when the user should be left alone —
|
|
1089
1219
|
* every failure prints its own reason first, because the caller only needs to
|
|
1090
|
-
* 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.
|
|
1091
1222
|
*/
|
|
1092
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);
|
|
1093
1228
|
const handoff = await startHandoff();
|
|
1094
1229
|
const url = signupUrl({ repo: slug, port: handoff.port, state: handoff.state });
|
|
1095
1230
|
const startedAt = Date.now();
|
|
@@ -1097,37 +1232,96 @@ async function signUpForBrain(repo, slug) {
|
|
|
1097
1232
|
// the outcome is the one thing a terminal handoff can lose: a user who reads
|
|
1098
1233
|
// the URL and walks away kills the process, and only an event already on disk
|
|
1099
1234
|
// survives that. This is the denominator; `brain_signup_settled` is not.
|
|
1100
|
-
track("brain_signup_opened", {}, { repo });
|
|
1235
|
+
track("brain_signup_opened", { mode: "terminal" }, { repo });
|
|
1101
1236
|
// Printed before the browser opens, and printed whether or not it opens: on a
|
|
1102
1237
|
// remote shell nothing can open, and on a desktop the window sometimes lands
|
|
1103
1238
|
// behind the terminal. The URL is the thing that always works.
|
|
1104
|
-
console.error(`· ${slug} has no
|
|
1239
|
+
console.error(`· ${slug} has no trail yet — opening your browser to make one:`);
|
|
1105
1240
|
console.error(` ${url}`);
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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") {
|
|
1114
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;
|
|
1115
1259
|
return null;
|
|
1116
1260
|
}
|
|
1117
|
-
openBrowser(url);
|
|
1118
|
-
console.error("· waiting for you to finish signing up…");
|
|
1119
|
-
const got = await handoff.wait();
|
|
1120
1261
|
if ("error" in got) {
|
|
1121
1262
|
console.error(`✗ ${got.error}`);
|
|
1122
1263
|
// The category, never the sentence: `got.error` names the repo and the link.
|
|
1123
|
-
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 });
|
|
1124
1265
|
return null;
|
|
1125
1266
|
}
|
|
1126
1267
|
writeLink(repo, got.link);
|
|
1127
|
-
console.error(`✓
|
|
1128
|
-
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 });
|
|
1129
1270
|
return got.link;
|
|
1130
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
|
+
}
|
|
1131
1325
|
brain
|
|
1132
1326
|
.command("connect")
|
|
1133
1327
|
.description("Attach a brain to this repo and pull its rules")
|
|
@@ -1151,7 +1345,7 @@ brain
|
|
|
1151
1345
|
// read the repo yet. Saying "0 rules" without saying why reads as a
|
|
1152
1346
|
// failure, and the next step is the whole point.
|
|
1153
1347
|
console.error("✓ attached this repo to the brain — it has no rules yet");
|
|
1154
|
-
console.error("· run `graft
|
|
1348
|
+
console.error("· run `graft trail push` to read this repository into it");
|
|
1155
1349
|
return;
|
|
1156
1350
|
}
|
|
1157
1351
|
console.error(`✓ pulled ${res.ruleCount} rule(s) from ${parsed.brainId}`);
|
|
@@ -1160,129 +1354,274 @@ brain
|
|
|
1160
1354
|
});
|
|
1161
1355
|
brain
|
|
1162
1356
|
.command("pull")
|
|
1163
|
-
.description("
|
|
1357
|
+
.description("Refresh the trail's rules and write every change you accepted in Trail into this repo's context files")
|
|
1164
1358
|
.argument("[dir]", "target repo directory", ".")
|
|
1165
|
-
.
|
|
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) {
|
|
1166
1365
|
const repo = resolve(dir);
|
|
1167
|
-
const
|
|
1168
|
-
if (!
|
|
1169
|
-
console.error("
|
|
1170
|
-
return;
|
|
1171
|
-
}
|
|
1172
|
-
if (res.warning) {
|
|
1173
|
-
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");
|
|
1174
1369
|
process.exitCode = 1;
|
|
1175
1370
|
return;
|
|
1176
1371
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
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
|
+
}
|
|
1181
1415
|
brain
|
|
1182
1416
|
.command("push")
|
|
1183
|
-
.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")
|
|
1184
1418
|
.argument("[dir]", "target repo directory", ".")
|
|
1185
1419
|
.option("--no-approve", "leave the mined rules as drafts for review")
|
|
1186
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")
|
|
1187
1422
|
.action(async (dir, opts) => {
|
|
1188
1423
|
const repo = resolve(dir);
|
|
1424
|
+
const tty = Boolean(process.stderr.isTTY);
|
|
1425
|
+
const verbose = opts.verbose === true;
|
|
1189
1426
|
// Resolved before the link, because an unlinked repo now signs up for a
|
|
1190
|
-
//
|
|
1427
|
+
// trail and Trail creates that trail FOR a named repository. Without a slug
|
|
1191
1428
|
// there is nothing to name it after — and the digest builder would fail on
|
|
1192
1429
|
// the same missing remote a moment later regardless.
|
|
1193
1430
|
const here = repoSlugFromGit(repo);
|
|
1194
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;
|
|
1195
1435
|
if (!link) {
|
|
1196
1436
|
if (!here) {
|
|
1197
|
-
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");
|
|
1198
1438
|
process.exitCode = 1;
|
|
1199
1439
|
return;
|
|
1200
1440
|
}
|
|
1201
1441
|
const signedUp = await signUpForBrain(repo, `${here.owner}/${here.name}`);
|
|
1202
1442
|
if (!signedUp) {
|
|
1203
|
-
process.exitCode
|
|
1443
|
+
if (!process.exitCode)
|
|
1444
|
+
process.exitCode = 1;
|
|
1204
1445
|
return;
|
|
1205
1446
|
}
|
|
1206
1447
|
link = signedUp;
|
|
1448
|
+
browserOpen = true;
|
|
1207
1449
|
}
|
|
1208
|
-
// What the website said this
|
|
1209
|
-
// 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
|
|
1210
1452
|
// of another repository's rules — a mistake that is silent afterwards,
|
|
1211
1453
|
// because the rules look perfectly plausible, just not about your code.
|
|
1212
1454
|
const expected = await fetchExpectedRepo(link);
|
|
1213
1455
|
if (expected && here && !sameRepo(expected.slug, `${here.owner}/${here.name}`)) {
|
|
1214
|
-
console.error(`✗ this
|
|
1215
|
-
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.`);
|
|
1458
|
+
process.exitCode = 1;
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
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");
|
|
1216
1464
|
process.exitCode = 1;
|
|
1217
1465
|
return;
|
|
1218
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.
|
|
1473
|
+
let early = Promise.resolve();
|
|
1474
|
+
if (expected?.earlyUpload) {
|
|
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.
|
|
1487
|
+
if (wiredHostIds(repo).length === 0) {
|
|
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 });
|
|
1491
|
+
}
|
|
1492
|
+
else {
|
|
1493
|
+
console.error("· graft isn't set up here — run graft init so your agents read these rules");
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1219
1496
|
// The graph is what symbol anchors are resolved against, so a rule mined
|
|
1220
1497
|
// here can later go stale on its own. Without one the ingest still works;
|
|
1221
1498
|
// its rules simply govern the repo rather than a symbol in it.
|
|
1222
1499
|
const graph = loadGraphCached(contextDirFor(repo, program.opts().dir));
|
|
1223
1500
|
if (!graph)
|
|
1224
|
-
console.error("· no graph yet — run
|
|
1225
|
-
const
|
|
1226
|
-
|
|
1227
|
-
console.error(" No file contents leave this machine.");
|
|
1228
|
-
const built = await buildLocalDigest(repo, graph, { autoApprove: opts.approve !== false });
|
|
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 });
|
|
1229
1504
|
if ("error" in built) {
|
|
1505
|
+
reading.stop();
|
|
1230
1506
|
console.error(`✗ ${built.error}`);
|
|
1231
1507
|
process.exitCode = 1;
|
|
1232
1508
|
return;
|
|
1233
1509
|
}
|
|
1234
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();
|
|
1235
1515
|
if (built.warning)
|
|
1236
1516
|
console.error(`⚠ ${built.warning}`);
|
|
1237
|
-
console.error(`· ${d.commits.length} commits, ${d.threads.length} discussions, ${d.symbols.length} symbols, ${d.sources.length} stated sources`);
|
|
1238
|
-
const sent = await pushDigest(link, d);
|
|
1239
1517
|
if ("error" in sent) {
|
|
1240
1518
|
console.error(`✗ ${sent.error}`);
|
|
1241
1519
|
process.exitCode = 1;
|
|
1242
1520
|
return;
|
|
1243
1521
|
}
|
|
1244
|
-
const
|
|
1245
|
-
|
|
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);
|
|
1246
1528
|
// Held rather than handed back. Everything above this line succeeded even
|
|
1247
1529
|
// in the runs that end badly: the push lands, and then the miner fails —
|
|
1248
|
-
// which is where roughly a third of production's repo
|
|
1530
|
+
// which is where roughly a third of production's repo trails die. Returning
|
|
1249
1531
|
// the prompt here is what made that invisible from this side, on CI and
|
|
1250
1532
|
// over SSH permanently so. `--no-watch` is for a caller that genuinely
|
|
1251
|
-
// wants fire-and-forget
|
|
1533
|
+
// wants fire-and-forget.
|
|
1252
1534
|
if (opts.watch === false) {
|
|
1253
|
-
console.error("·
|
|
1254
|
-
console.error(
|
|
1535
|
+
console.error("· building now — the rules reach this repo on their own; watch it at");
|
|
1536
|
+
console.error(` ${buildPage}`);
|
|
1255
1537
|
return;
|
|
1256
1538
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
+
};
|
|
1259
1580
|
if (outcome === "completed") {
|
|
1260
|
-
console.error(
|
|
1581
|
+
console.error(`✓ trail built · ${rules}, reaching this repo on their own`);
|
|
1582
|
+
closing();
|
|
1261
1583
|
return;
|
|
1262
1584
|
}
|
|
1263
1585
|
// The ordinary ending for anything but a small repository. The history is
|
|
1264
1586
|
// mined in slices, and the prompt comes back on the first of them: mining is
|
|
1265
1587
|
// the part that fails and it has now succeeded, and the rest is filing,
|
|
1266
|
-
// which is minutes of reliable work nobody gains by watching.
|
|
1267
|
-
// already in the brain are already being served.
|
|
1588
|
+
// which is minutes of reliable work nobody gains by watching.
|
|
1268
1589
|
if (outcome === "building") {
|
|
1269
|
-
console.error(
|
|
1270
|
-
|
|
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();
|
|
1271
1594
|
return;
|
|
1272
1595
|
}
|
|
1273
1596
|
if (outcome === "failed") {
|
|
1274
1597
|
// A non-zero exit, unlike every other ending here: this is the one case
|
|
1275
|
-
// where the work did not produce a
|
|
1276
|
-
// should hear about it the way it hears about any other failure.
|
|
1277
|
-
|
|
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.
|
|
1278
1601
|
process.exitCode = 1;
|
|
1279
1602
|
return;
|
|
1280
1603
|
}
|
|
1281
1604
|
if (outcome === "unreachable") {
|
|
1282
|
-
console.error("·
|
|
1283
|
-
return;
|
|
1605
|
+
console.error("· lost touch with Trail — the build keeps going; watch it at the link below");
|
|
1284
1606
|
}
|
|
1285
|
-
|
|
1607
|
+
else {
|
|
1608
|
+
console.error("· still building after 15 minutes — not failed, just long; watch it at the link below");
|
|
1609
|
+
}
|
|
1610
|
+
console.error(` ${buildPage}`);
|
|
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.
|
|
1614
|
+
const claudeMd = program
|
|
1615
|
+
.command("claude-md")
|
|
1616
|
+
.description("The CLAUDE.md changes this repo's trail suggested (now part of graft trail pull)");
|
|
1617
|
+
claudeMd
|
|
1618
|
+
.command("pull")
|
|
1619
|
+
.description("Same as graft trail pull: write every change you accepted in Trail into this repo")
|
|
1620
|
+
.argument("[dir]", "target repo directory", ".")
|
|
1621
|
+
.option("--dry-run", "show what would change without writing anything or telling Trail")
|
|
1622
|
+
.action(async (dir, opts) => {
|
|
1623
|
+
console.error("· graft claude-md pull is now part of graft trail pull — running that");
|
|
1624
|
+
await runTrailPullCommand(dir, opts);
|
|
1286
1625
|
});
|
|
1287
1626
|
brain
|
|
1288
1627
|
.command("status")
|
|
@@ -1303,7 +1642,7 @@ brain
|
|
|
1303
1642
|
return;
|
|
1304
1643
|
}
|
|
1305
1644
|
if (!link) {
|
|
1306
|
-
console.error("· no brain attached — run `graft
|
|
1645
|
+
console.error("· no brain attached — run `graft trail connect <brainId>:<token>`");
|
|
1307
1646
|
return;
|
|
1308
1647
|
}
|
|
1309
1648
|
console.error(`brain ${link.brainId}`);
|
|
@@ -1329,7 +1668,7 @@ brain
|
|
|
1329
1668
|
clearLink(resolve(dir));
|
|
1330
1669
|
console.error(`✓ detached the brain from ${repo}`);
|
|
1331
1670
|
});
|
|
1332
|
-
program.parseAsync().catch((err) => {
|
|
1671
|
+
program.parseAsync(withLegacyNames(process.argv)).catch((err) => {
|
|
1333
1672
|
console.error(err instanceof Error ? err.message : err);
|
|
1334
1673
|
process.exit(1);
|
|
1335
1674
|
});
|