@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.
Files changed (75) hide show
  1. package/README.md +9 -4
  2. package/dist/app/history.d.ts +36 -1
  3. package/dist/app/history.d.ts.map +1 -1
  4. package/dist/app/history.js +75 -22
  5. package/dist/app/history.js.map +1 -1
  6. package/dist/app/sources.d.ts +13 -0
  7. package/dist/app/sources.d.ts.map +1 -1
  8. package/dist/app/sources.js +129 -1
  9. package/dist/app/sources.js.map +1 -1
  10. package/dist/ask/ask.js +1 -1
  11. package/dist/brain/claude-md.d.ts +61 -0
  12. package/dist/brain/claude-md.d.ts.map +1 -0
  13. package/dist/brain/claude-md.js +171 -0
  14. package/dist/brain/claude-md.js.map +1 -0
  15. package/dist/brain/connect.d.ts +3 -3
  16. package/dist/brain/connect.js +7 -7
  17. package/dist/brain/context-files.d.ts +79 -0
  18. package/dist/brain/context-files.d.ts.map +1 -0
  19. package/dist/brain/context-files.js +245 -0
  20. package/dist/brain/context-files.js.map +1 -0
  21. package/dist/brain/link.d.ts +18 -1
  22. package/dist/brain/link.d.ts.map +1 -1
  23. package/dist/brain/link.js +22 -1
  24. package/dist/brain/link.js.map +1 -1
  25. package/dist/brain/pull.d.ts +17 -0
  26. package/dist/brain/pull.d.ts.map +1 -0
  27. package/dist/brain/pull.js +172 -0
  28. package/dist/brain/pull.js.map +1 -0
  29. package/dist/brain/push.d.ts +86 -3
  30. package/dist/brain/push.d.ts.map +1 -1
  31. package/dist/brain/push.js +149 -36
  32. package/dist/brain/push.js.map +1 -1
  33. package/dist/brain/signup.d.ts +46 -2
  34. package/dist/brain/signup.d.ts.map +1 -1
  35. package/dist/brain/signup.js +103 -13
  36. package/dist/brain/signup.js.map +1 -1
  37. package/dist/brain/upload.d.ts +61 -0
  38. package/dist/brain/upload.d.ts.map +1 -0
  39. package/dist/brain/upload.js +210 -0
  40. package/dist/brain/upload.js.map +1 -0
  41. package/dist/brain/watch.d.ts +77 -4
  42. package/dist/brain/watch.d.ts.map +1 -1
  43. package/dist/brain/watch.js +263 -39
  44. package/dist/brain/watch.js.map +1 -1
  45. package/dist/claude/build-progress.d.ts +29 -0
  46. package/dist/claude/build-progress.d.ts.map +1 -0
  47. package/dist/claude/build-progress.js +76 -0
  48. package/dist/claude/build-progress.js.map +1 -0
  49. package/dist/claude/init.d.ts.map +1 -1
  50. package/dist/claude/init.js +3 -1
  51. package/dist/claude/init.js.map +1 -1
  52. package/dist/cli-picker.d.ts +17 -2
  53. package/dist/cli-picker.d.ts.map +1 -1
  54. package/dist/cli-picker.js +103 -22
  55. package/dist/cli-picker.js.map +1 -1
  56. package/dist/cli.js +462 -123
  57. package/dist/cli.js.map +1 -1
  58. package/dist/legacy-args.d.ts +12 -0
  59. package/dist/legacy-args.d.ts.map +1 -0
  60. package/dist/legacy-args.js +31 -0
  61. package/dist/legacy-args.js.map +1 -0
  62. package/dist/telemetry/contract.d.ts +17 -4
  63. package/dist/telemetry/contract.d.ts.map +1 -1
  64. package/dist/telemetry/contract.js +25 -7
  65. package/dist/telemetry/contract.js.map +1 -1
  66. package/dist/upkeep.d.ts +1 -1
  67. package/dist/upkeep.js +1 -1
  68. package/dist/util/spinner.d.ts +15 -0
  69. package/dist/util/spinner.d.ts.map +1 -0
  70. package/dist/util/spinner.js +52 -0
  71. package/dist/util/spinner.js.map +1 -0
  72. package/dist/util/state.d.ts +8 -0
  73. package/dist/util/state.d.ts.map +1 -1
  74. package/dist/util/state.js.map +1 -1
  75. 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 { watchBuild } from "./brain/watch.js";
25
- import { openBrowser, signupUrl, startHandoff } from "./brain/signup.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
+ 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("--brain <handoff>", "attach a Trail brain: <brainId>:<token> (or a bare brain id with GRAFT_BRAIN_TOKEN set)")
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.brain) {
831
- const parsed = parseBrainArg(opts.brain);
855
+ if (opts.trail) {
856
+ const parsed = parseBrainArg(opts.trail);
832
857
  if ("error" in parsed) {
833
- console.error(`✗ --brain: ${parsed.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 brain status` can only report how many of them resolve
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 (res.warning)
927
- console.error(`⚠ brain: ${res.warning}`);
928
- else
929
- console.error(`✓ brain: pulled ${res.ruleCount} rule(s) from ${brainLink.brainId}`);
930
- for (const w of res.writes)
931
- console.error(`✓ brain rules: ${w.path} (${w.action})`);
932
- if (res.ruleCount > 0 && res.writes.length === 0)
933
- console.error("· no instruction file to write rules into — graft ask still carries them");
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
- console.error("\n" +
942
- formatInitEpilogue({
943
- graphBuilt: graphs.length > 0,
944
- nodes: graphs.reduce((n, g) => n + g.meta.nodeCount, 0),
945
- edges: graphs.reduce((n, g) => n + g.meta.edgeCount, 0),
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, opts } = ctx;
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
- console.error(`- removed ${r.path} (${r.what}) — agent not selected`);
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
- console.error(`✓ wrote ${res.settingsPath}`);
1095
+ say(`✓ wrote ${res.settingsPath}`);
969
1096
  for (const s of res.shims)
970
- console.error(`✓ wrote ${s}`);
971
- console.error(`✓ wrote ${res.skill}`);
1097
+ say(`✓ wrote ${s}`);
1098
+ say(`✓ wrote ${res.skill}`);
972
1099
  if (res.mcp.action === "skipped-unparseable")
973
- console.error(`⚠ .mcp.json: ${res.mcp.path} left unchanged (not valid JSON) — add the graft server manually`);
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
- console.error(`· mcp claude: ${res.mcp.path} (already registered)`);
1102
+ say(`· mcp claude: ${res.mcp.path} (already registered)`);
976
1103
  else
977
- console.error(`✓ mcp claude: ${res.mcp.path} (${res.mcp.action}) — restart Claude Code to load the graft MCP server`);
978
- console.error(res.built ? "✓ built the graph (graft build)" : "· skipped graph build");
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
- console.error("· skipped Claude Code statusLine (--no-statusline)");
1107
+ say("· skipped Claude Code statusLine (--no-statusline)");
981
1108
  for (const w of res.warnings)
982
- console.error(`⚠ ${w}`);
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
- console.error(`✓ ${w.id}: ${w.path} (${w.action})`);
1123
+ say(`✓ ${w.id}: ${w.path} (${w.action})`);
997
1124
  for (const m of r.mcp)
998
- console.error(`✓ mcp ${m.id}: ${m.path} (${m.action})`);
1125
+ say(`✓ mcp ${m.id}: ${m.path} (${m.action})`);
999
1126
  for (const h of r.hooks)
1000
- console.error(`✓ hook ${h.id}: ${h.path} (${h.action})`);
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
- console.error("· skipped out-of-repo writes (--no-global)");
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
- console.error(buildGraphIfMissing(repo, { build: opts.build, cliPath })
1020
- ? "✓ built the graph (graft build)"
1021
- : "· skipped graph build");
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("brain")
1083
- .description("The Trail brain attached to this repo: the rules mined from its own history");
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 brain from the terminal, by sending the user through signup
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 brain yet. Opening your browser to make one:`);
1239
+ console.error(`· ${slug} has no trail yet — opening your browser to make one:`);
1105
1240
  console.error(` ${url}`);
1106
- // A non-interactive shell has nobody to click anything, so waiting five
1107
- // minutes for a browser that will never come is worse than saying so now.
1108
- if (!process.stderr.isTTY) {
1109
- console.error("· not a terminal — open that link, then run `graft brain connect <brainId>:<token>` here");
1110
- // Its own outcome, not a timeout: nothing here could have opened a browser,
1111
- // so folding the two together would read as people abandoning signup when
1112
- // it is only a remote shell doing what it has to.
1113
- track("brain_signup_settled", { outcome: "no_tty", duration_bucket: durationBucket(Date.now() - startedAt) }, { repo });
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(`✓ brain connected to ${slug}`);
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 brain push` to read this repository into it");
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("Re-pull the attached brain's rules and rewrite the agent instruction files")
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
- .action(async (dir) => {
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 res = await pullBrain(repo, { home: homedir() });
1168
- if (!res) {
1169
- console.error("· no brain attached — run `graft brain connect <brainId>:<token>`");
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
- console.error(`✓ pulled ${res.ruleCount} rule(s)`);
1178
- for (const w of res.writes)
1179
- console.error(`✓ ${w.path} (${w.action})`);
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 brain — no GitHub App, works on private repos")
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
- // brain and Trail creates that brain FOR a named repository. Without a slug
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 `origin` remote — graft can only push a GitHub repository today");
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 = 1;
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 brain is for. Checked BEFORE any reading, so
1209
- // standing in the wrong checkout costs a message rather than a brain full
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 brain is for ${expected.slug}, but you are in ${here.owner}/${here.name}`);
1215
- console.error(` cd into ${expected.slug} and run this again, or attach a different brain here.`);
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 `graft build` first so rules can be anchored to symbols");
1225
- const label = expected?.slug ?? (here ? `${here.owner}/${here.name}` : "this repository");
1226
- console.error(`· reading ${label} — commit messages, pull-request discussion and the docs in the tree.`);
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 brainLabel = expected?.brainName ? `“${expected.brainName}”` : link.brainId;
1245
- console.error(`✓ sent ${d.owner}/${d.name} to ${brainLabel}`);
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 brains die. Returning
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, and it prints the old two lines instead.
1533
+ // wants fire-and-forget.
1252
1534
  if (opts.watch === false) {
1253
- console.error("· it is being mined into rules now — a few minutes. Watch it finish in your browser.");
1254
- console.error(" The rules reach this repo on their own; nothing else to run.");
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
- console.error("· building the brain — Ctrl-C detaches, it keeps going without you");
1258
- const outcome = await watchBuild(link);
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("✓ the brain is built — its rules reach this repo on their own; nothing else to run");
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. The rules
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("✓ the brain has its first rules — they reach this repo on their own; nothing else to run");
1270
- console.error(" The rest of the history is still being read. Watch it fill in your browser.");
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 brain, and a CI step that ran the push
1276
- // should hear about it the way it hears about any other failure.
1277
- console.error(" Nothing was lost — the brain is still there. Run `graft brain push` again to retry the read.");
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("· could not reach Trail to follow the build — it is still running. Watch it finish in your browser.");
1283
- return;
1605
+ console.error("· lost touch with Trail — the build keeps going; watch it at the link below");
1284
1606
  }
1285
- console.error("· still building after 15 minutes — it has not failed, it is just long. Watch it finish in your browser.");
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 brain connect <brainId>:<token>`");
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
  });