@nanonets/graft 0.18.0 → 0.19.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 (54) hide show
  1. package/README.md +1 -1
  2. package/dist/app/brain-build-process.d.ts +22 -0
  3. package/dist/app/brain-build-process.d.ts.map +1 -0
  4. package/dist/app/brain-build-process.js +129 -0
  5. package/dist/app/brain-build-process.js.map +1 -0
  6. package/dist/app/brain-build-worker.d.ts +35 -0
  7. package/dist/app/brain-build-worker.d.ts.map +1 -0
  8. package/dist/app/brain-build-worker.js +56 -0
  9. package/dist/app/brain-build-worker.js.map +1 -0
  10. package/dist/app/brain-build.d.ts +97 -3
  11. package/dist/app/brain-build.d.ts.map +1 -1
  12. package/dist/app/brain-build.js +148 -39
  13. package/dist/app/brain-build.js.map +1 -1
  14. package/dist/app/history.d.ts +1 -1
  15. package/dist/app/history.d.ts.map +1 -1
  16. package/dist/app/history.js +2 -5
  17. package/dist/app/history.js.map +1 -1
  18. package/dist/app/identity.d.ts +23 -0
  19. package/dist/app/identity.d.ts.map +1 -1
  20. package/dist/app/identity.js +47 -0
  21. package/dist/app/identity.js.map +1 -1
  22. package/dist/app/main.js +2 -0
  23. package/dist/app/main.js.map +1 -1
  24. package/dist/app/server.d.ts +10 -1
  25. package/dist/app/server.d.ts.map +1 -1
  26. package/dist/app/server.js +55 -2
  27. package/dist/app/server.js.map +1 -1
  28. package/dist/app/sources.d.ts +1 -1
  29. package/dist/app/sources.d.ts.map +1 -1
  30. package/dist/app/sources.js +3 -2
  31. package/dist/app/sources.js.map +1 -1
  32. package/dist/brain/link.d.ts +8 -1
  33. package/dist/brain/link.d.ts.map +1 -1
  34. package/dist/brain/link.js +11 -1
  35. package/dist/brain/link.js.map +1 -1
  36. package/dist/brain/signup.d.ts +73 -0
  37. package/dist/brain/signup.d.ts.map +1 -0
  38. package/dist/brain/signup.js +186 -0
  39. package/dist/brain/signup.js.map +1 -0
  40. package/dist/brain/watch.d.ts +136 -0
  41. package/dist/brain/watch.d.ts.map +1 -0
  42. package/dist/brain/watch.js +172 -0
  43. package/dist/brain/watch.js.map +1 -0
  44. package/dist/cli.js +108 -8
  45. package/dist/cli.js.map +1 -1
  46. package/dist/context/node-file.d.ts +4 -10
  47. package/dist/context/node-file.d.ts.map +1 -1
  48. package/dist/context/node-file.js +6 -2
  49. package/dist/context/node-file.js.map +1 -1
  50. package/dist/telemetry/contract.d.ts +12 -0
  51. package/dist/telemetry/contract.d.ts.map +1 -1
  52. package/dist/telemetry/contract.js +19 -0
  53. package/dist/telemetry/contract.js.map +1 -1
  54. package/package.json +1 -1
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Watching a brain being built, from the terminal.
3
+ *
4
+ * `graft brain push` used to end at "it is being mined into rules now — a few
5
+ * minutes. Watch it finish in your browser." and hand the prompt straight back.
6
+ * That sentence is the last thing this process ever says about the work, and it
7
+ * is said BEFORE the part that actually fails: of the repo brains attempted in
8
+ * production, roughly a third die inside the miner, after the push succeeded.
9
+ * The terminal has already returned by then, so on this side the failure does
10
+ * not exist at all — and on a CI runner or over SSH, where nobody is going to
11
+ * open a browser, it exists nowhere.
12
+ *
13
+ * So the push now holds the line and reports the same four stages the browser
14
+ * shows, off the same row. Ctrl-C detaches without cancelling anything, because
15
+ * the work is server-side and killing a watcher must never look like killing
16
+ * the build.
17
+ *
18
+ * What it holds the line UNTIL changed with slicing. The history is now mined in
19
+ * several calls rather than one, and the first of them returns in about ten
20
+ * seconds; filing all of the rules into the graph takes minutes more. Mining is
21
+ * where the failures are — the thing this watcher exists to report — and filing
22
+ * is long, dull and reliable, so the line is held through the first and handed
23
+ * back before the second. Holding a terminal through five minutes of successful
24
+ * filing buys nobody anything, and on a CI runner it is five minutes of a job
25
+ * sitting idle.
26
+ */
27
+ import type { BrainLink } from "./link.js";
28
+ /** The stages, in the order they happen. The wording matches Trail's build
29
+ * screen deliberately: two products describing one process differently is how
30
+ * a person ends up unsure whether they are looking at the same thing. */
31
+ export declare const STAGES: readonly [{
32
+ readonly id: "reach";
33
+ readonly label: "reaching the repository";
34
+ }, {
35
+ readonly id: "read";
36
+ readonly label: "reading its history";
37
+ }, {
38
+ readonly id: "mine";
39
+ readonly label: "mining the rules";
40
+ }, {
41
+ readonly id: "file";
42
+ readonly label: "filing them into the brain";
43
+ }];
44
+ export type StageId = (typeof STAGES)[number]["id"];
45
+ export type StageState = "waiting" | "doing" | "done" | "failed";
46
+ export interface Stage {
47
+ id: StageId;
48
+ label: string;
49
+ state: StageState;
50
+ /** A count worth printing beside a stage that has produced one. */
51
+ detail?: string;
52
+ }
53
+ /** The repo row, as the public endpoint returns it. Only the fields the stages
54
+ * read, so a field added server-side cannot quietly change what is printed. */
55
+ export interface RepoState {
56
+ status: string;
57
+ errorMessage?: string;
58
+ ruleCount: number;
59
+ commitCount: number;
60
+ threadCount: number;
61
+ /** Rules in the graph right now, from the job still running. The row's own
62
+ * `ruleCount` is only written when that job finishes, so mid-build it is
63
+ * zero however many rules are already there.
64
+ *
65
+ * Optional because an older Trail does not send it, and a graft pointed at
66
+ * one must fall back to waiting for the finished row rather than reading a
67
+ * missing field as "no rules". */
68
+ filedSoFar?: number;
69
+ /** Rules the miner has handed over so far. Grows with every slice of the
70
+ * history that comes back, so it is a floor and never a total. */
71
+ foundSoFar?: number;
72
+ }
73
+ export interface BuildView {
74
+ stages: Stage[];
75
+ /**
76
+ * True once rules are in the graph — which is when this watcher has nothing
77
+ * left to report, not when the job ends. The rest of the slices keep mining
78
+ * and filing after the prompt comes back.
79
+ */
80
+ ready: boolean;
81
+ done: boolean;
82
+ /** The server's own words, when it failed. */
83
+ error: string | null;
84
+ }
85
+ /**
86
+ * Which stage the work is in.
87
+ *
88
+ * The rule that matters is where a failure lands. A row that never left
89
+ * `pending` failed at the access check; one that read 412 commits and then died
90
+ * failed in the miner. Reporting both as "could not reach it" would send someone
91
+ * to fix repository access for a problem that has nothing to do with it.
92
+ */
93
+ export declare function stagesFrom(repo: RepoState | null): BuildView;
94
+ /**
95
+ * Read the repo row this brain is building.
96
+ *
97
+ * The same endpoint `fetchExpectedRepo` uses, read for its counts rather than
98
+ * its slug. Null on any failure, so a watcher that cannot reach the API prints
99
+ * nothing new rather than inventing a failure the build did not have.
100
+ */
101
+ export declare function fetchRepoState(link: BrainLink, fetchImpl?: typeof fetch): Promise<RepoState | null>;
102
+ /** One printable line per stage that has moved since it was last printed. */
103
+ export declare function linesFor(view: BuildView, already: Set<string>): string[];
104
+ export interface WatchOptions {
105
+ /** How long to hold before giving up on a build that is still running. */
106
+ timeoutMs?: number;
107
+ pollMs?: number;
108
+ fetchImpl?: typeof fetch;
109
+ /** Where the lines go. Injected so the tests do not need a console. */
110
+ write?: (line: string) => void;
111
+ sleep?: (ms: number) => Promise<void>;
112
+ }
113
+ /**
114
+ * How the watch ended.
115
+ *
116
+ * `building` is the ordinary success: rules are in the brain and the rest of the
117
+ * history is still being read behind the prompt. `completed` is the same thing
118
+ * after the whole job has finished, which only happens for a repository small
119
+ * enough to be mined in one slice.
120
+ */
121
+ export type WatchOutcome = "completed" | "building" | "failed" | "timed_out" | "unreachable";
122
+ /**
123
+ * Hold until the brain has rules, printing each stage as it settles.
124
+ *
125
+ * Returns on the first rules rather than on the finished job: mining is what
126
+ * fails and it is done by then, and filing the rest is minutes of work nobody
127
+ * needs to watch. The caller prints where to see the rest.
128
+ *
129
+ * Fifteen minutes by default. That is far more than the mining now takes, and it
130
+ * stays generous on purpose: a watcher that gives up before the build does would
131
+ * report a healthy build as a timeout, which is the exact confusion this is
132
+ * meant to remove. Giving up says so and says the work continues, because it
133
+ * does.
134
+ */
135
+ export declare function watchBuild(link: BrainLink, opts?: WatchOptions): Promise<WatchOutcome>;
136
+ //# sourceMappingURL=watch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/brain/watch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG3C;;0EAE0E;AAC1E,eAAO,MAAM,MAAM;;;;;;;;;;;;EAKT,CAAC;AAEX,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC;AACpD,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEjE,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,UAAU,CAAC;IAClB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;gFACgF;AAChF,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;uCAMmC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;uEACmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB;;;;OAIG;IACH,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,8CAA8C;IAC9C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAID;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,CAsD5D;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,GAAE,OAAO,KAAa,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CA2BhH;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAcxE;AAED,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,uEAAuE;IACvE,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,UAAU,GAAG,QAAQ,GAAG,WAAW,GAAG,aAAa,CAAC;AAE7F;;;;;;;;;;;;GAYG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CAgChG"}
@@ -0,0 +1,172 @@
1
+ import { baseUrlFor } from "./link.js";
2
+ /** The stages, in the order they happen. The wording matches Trail's build
3
+ * screen deliberately: two products describing one process differently is how
4
+ * a person ends up unsure whether they are looking at the same thing. */
5
+ export const STAGES = [
6
+ { id: "reach", label: "reaching the repository" },
7
+ { id: "read", label: "reading its history" },
8
+ { id: "mine", label: "mining the rules" },
9
+ { id: "file", label: "filing them into the brain" },
10
+ ];
11
+ const num = (v) => (typeof v === "number" && v > 0 ? v : 0);
12
+ /**
13
+ * Which stage the work is in.
14
+ *
15
+ * The rule that matters is where a failure lands. A row that never left
16
+ * `pending` failed at the access check; one that read 412 commits and then died
17
+ * failed in the miner. Reporting both as "could not reach it" would send someone
18
+ * to fix repository access for a problem that has nothing to do with it.
19
+ */
20
+ export function stagesFrom(repo) {
21
+ const commits = num(repo?.commitCount);
22
+ const threads = num(repo?.threadCount);
23
+ const rules = num(repo?.ruleCount);
24
+ const filedSoFar = num(repo?.filedSoFar);
25
+ const foundSoFar = num(repo?.foundSoFar);
26
+ const reached = !!repo && repo.status !== "pending";
27
+ const readIt = commits > 0 || threads > 0;
28
+ // Rules that exist, whether the job has finished writing its count or not.
29
+ const hasRules = filedSoFar > 0 || rules > 0;
30
+ const filed = rules > 0;
31
+ const failed = repo?.status === "failed";
32
+ const failedAt = !failed ? null : !reached || !readIt ? "reach" : !hasRules ? "mine" : "file";
33
+ const order = STAGES.map((s) => s.id);
34
+ const state = (id) => {
35
+ if (failedAt === id)
36
+ return "failed";
37
+ if (failedAt && order.indexOf(id) > order.indexOf(failedAt))
38
+ return "waiting";
39
+ switch (id) {
40
+ case "reach":
41
+ return reached ? "done" : "doing";
42
+ case "read":
43
+ return readIt ? "done" : reached ? "doing" : "waiting";
44
+ case "mine":
45
+ // Done on the first slice's rules. The remaining slices are still being
46
+ // mined at this point, behind the prompt this watcher is about to
47
+ // return — which is the whole change.
48
+ return hasRules ? "done" : readIt ? "doing" : "waiting";
49
+ case "file":
50
+ return filed ? "done" : hasRules ? "doing" : readIt ? "waiting" : "waiting";
51
+ }
52
+ };
53
+ const detail = (id) => {
54
+ if (id === "read" && (commits || threads)) {
55
+ return [commits ? `${commits} commits` : null, threads ? `${threads} discussions` : null].filter(Boolean).join(", ");
56
+ }
57
+ // "so far" because the count grows with each slice that lands. Printing it
58
+ // as a total would promise history that has not been read yet.
59
+ if (id === "mine" && foundSoFar)
60
+ return `${foundSoFar} rules so far`;
61
+ if (id === "file" && rules)
62
+ return `${rules} rules`;
63
+ if (id === "file" && filedSoFar)
64
+ return `${filedSoFar} rules so far`;
65
+ return undefined;
66
+ };
67
+ return {
68
+ stages: STAGES.map((s) => ({ id: s.id, label: s.label, state: state(s.id), detail: detail(s.id) })),
69
+ ready: hasRules && !failed,
70
+ done: repo?.status === "completed" && filed,
71
+ error: failed ? (repo?.errorMessage?.trim() || "the read stopped before it finished") : null,
72
+ };
73
+ }
74
+ /**
75
+ * Read the repo row this brain is building.
76
+ *
77
+ * The same endpoint `fetchExpectedRepo` uses, read for its counts rather than
78
+ * its slug. Null on any failure, so a watcher that cannot reach the API prints
79
+ * nothing new rather than inventing a failure the build did not have.
80
+ */
81
+ export async function fetchRepoState(link, fetchImpl = fetch) {
82
+ try {
83
+ const res = await fetchImpl(`${baseUrlFor(link)}/api/public/brains/${encodeURIComponent(link.brainId)}/repo`, {
84
+ headers: { authorization: `Bearer ${link.token}`, accept: "application/json" },
85
+ signal: AbortSignal.timeout(5000),
86
+ });
87
+ if (!res.ok)
88
+ return null;
89
+ const body = (await res.json());
90
+ if (!body.repo)
91
+ return null;
92
+ return {
93
+ status: String(body.repo.status ?? ""),
94
+ errorMessage: body.repo.error_message,
95
+ ruleCount: num(body.repo.rule_count),
96
+ commitCount: num(body.repo.commit_count),
97
+ threadCount: num(body.repo.thread_count),
98
+ filedSoFar: num(body.build?.filed_so_far),
99
+ foundSoFar: num(body.build?.found_so_far),
100
+ };
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }
106
+ /** One printable line per stage that has moved since it was last printed. */
107
+ export function linesFor(view, already) {
108
+ const out = [];
109
+ for (const s of view.stages) {
110
+ // Only settled states are printed. A line per poll for a stage that is
111
+ // merely still running would bury the four that matter under a hundred
112
+ // that do not — this is a log, not a redrawn frame.
113
+ if (s.state !== "done" && s.state !== "failed")
114
+ continue;
115
+ const key = `${s.id}:${s.state}`;
116
+ if (already.has(key))
117
+ continue;
118
+ already.add(key);
119
+ const mark = s.state === "done" ? "✓" : "✗";
120
+ out.push(` ${mark} ${s.label}${s.detail ? ` — ${s.detail}` : ""}`);
121
+ }
122
+ return out;
123
+ }
124
+ /**
125
+ * Hold until the brain has rules, printing each stage as it settles.
126
+ *
127
+ * Returns on the first rules rather than on the finished job: mining is what
128
+ * fails and it is done by then, and filing the rest is minutes of work nobody
129
+ * needs to watch. The caller prints where to see the rest.
130
+ *
131
+ * Fifteen minutes by default. That is far more than the mining now takes, and it
132
+ * stays generous on purpose: a watcher that gives up before the build does would
133
+ * report a healthy build as a timeout, which is the exact confusion this is
134
+ * meant to remove. Giving up says so and says the work continues, because it
135
+ * does.
136
+ */
137
+ export async function watchBuild(link, opts = {}) {
138
+ const timeoutMs = opts.timeoutMs ?? 15 * 60 * 1000;
139
+ const pollMs = opts.pollMs ?? 4000;
140
+ const write = opts.write ?? ((l) => console.error(l));
141
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms).unref?.()));
142
+ const printed = new Set();
143
+ const startedAt = Date.now();
144
+ // A row we could never read at all, as opposed to one that answered once and
145
+ // then stopped answering: the first is a broken link or a host that is down
146
+ // and is worth saying, the second is a blip the next poll usually covers.
147
+ let everRead = false;
148
+ for (;;) {
149
+ const repo = await fetchRepoState(link, opts.fetchImpl);
150
+ if (repo) {
151
+ everRead = true;
152
+ const view = stagesFrom(repo);
153
+ for (const line of linesFor(view, printed))
154
+ write(line);
155
+ if (view.error) {
156
+ write(`✗ ${view.error}`);
157
+ return "failed";
158
+ }
159
+ if (view.done)
160
+ return "completed";
161
+ // The ordinary ending now. The job is still filing the rules the miner
162
+ // has handed over, and still mining the older slices of the history, and
163
+ // neither is worth a terminal sitting on it.
164
+ if (view.ready)
165
+ return "building";
166
+ }
167
+ if (Date.now() - startedAt >= timeoutMs)
168
+ return everRead ? "timed_out" : "unreachable";
169
+ await sleep(pollMs);
170
+ }
171
+ }
172
+ //# sourceMappingURL=watch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.js","sourceRoot":"","sources":["../../src/brain/watch.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -20,7 +20,9 @@ import { parseBrainArg, connectBrain, pullBrain, brainStatus } from "./brain/con
20
20
  import { rulesForPointers } from "./brain/attach.js";
21
21
  import { clearLink } from "./brain/link.js";
22
22
  import { buildLocalDigest, fetchExpectedRepo, pushDigest, repoSlugFromGit, sameRepo } from "./brain/push.js";
23
- import { readLink } from "./brain/link.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";
24
26
  import { contextDirFor } from "./context/node-file.js";
25
27
  import { loadGraphCached } from "./graph/load.js";
26
28
  import { ensureFreshChildren, ensureFreshGraph, refreshNote } from "./graph/refresh.js";
@@ -1079,6 +1081,53 @@ program
1079
1081
  const brain = program
1080
1082
  .command("brain")
1081
1083
  .description("The Trail brain attached to this repo: the rules mined from its own history");
1084
+ /**
1085
+ * Get this repo a brain from the terminal, by sending the user through signup
1086
+ * in their browser and catching the handoff on loopback.
1087
+ *
1088
+ * Returns the link, already saved, or null when the user should be left alone —
1089
+ * every failure prints its own reason first, because the caller only needs to
1090
+ * know whether to carry on.
1091
+ */
1092
+ async function signUpForBrain(repo, slug) {
1093
+ const handoff = await startHandoff();
1094
+ const url = signupUrl({ repo: slug, port: handoff.port, state: handoff.state });
1095
+ const startedAt = Date.now();
1096
+ // Queued before the link is printed rather than after the outcome, because
1097
+ // the outcome is the one thing a terminal handoff can lose: a user who reads
1098
+ // the URL and walks away kills the process, and only an event already on disk
1099
+ // survives that. This is the denominator; `brain_signup_settled` is not.
1100
+ track("brain_signup_opened", {}, { repo });
1101
+ // Printed before the browser opens, and printed whether or not it opens: on a
1102
+ // remote shell nothing can open, and on a desktop the window sometimes lands
1103
+ // 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:`);
1105
+ 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 });
1114
+ handoff.close();
1115
+ return null;
1116
+ }
1117
+ openBrowser(url);
1118
+ console.error("· waiting for you to finish signing up…");
1119
+ const got = await handoff.wait();
1120
+ if ("error" in got) {
1121
+ console.error(`✗ ${got.error}`);
1122
+ // 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 });
1124
+ return null;
1125
+ }
1126
+ 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 });
1129
+ return got.link;
1130
+ }
1082
1131
  brain
1083
1132
  .command("connect")
1084
1133
  .description("Attach a brain to this repo and pull its rules")
@@ -1134,19 +1183,32 @@ brain
1134
1183
  .description("Read THIS repo on your machine and build its brain — no GitHub App, works on private repos")
1135
1184
  .argument("[dir]", "target repo directory", ".")
1136
1185
  .option("--no-approve", "leave the mined rules as drafts for review")
1186
+ .option("--no-watch", "return as soon as the push is sent, without following the build")
1137
1187
  .action(async (dir, opts) => {
1138
1188
  const repo = resolve(dir);
1139
- const link = readLink(repo);
1189
+ // 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
1191
+ // there is nothing to name it after — and the digest builder would fail on
1192
+ // the same missing remote a moment later regardless.
1193
+ const here = repoSlugFromGit(repo);
1194
+ let link = readLink(repo);
1140
1195
  if (!link) {
1141
- console.error("· no brain attached — run `graft brain connect <brainId>:<token>` first");
1142
- process.exitCode = 1;
1143
- return;
1196
+ if (!here) {
1197
+ console.error("✗ this directory has no GitHub `origin` remote — graft can only push a GitHub repository today");
1198
+ process.exitCode = 1;
1199
+ return;
1200
+ }
1201
+ const signedUp = await signUpForBrain(repo, `${here.owner}/${here.name}`);
1202
+ if (!signedUp) {
1203
+ process.exitCode = 1;
1204
+ return;
1205
+ }
1206
+ link = signedUp;
1144
1207
  }
1145
1208
  // What the website said this brain is for. Checked BEFORE any reading, so
1146
1209
  // standing in the wrong checkout costs a message rather than a brain full
1147
1210
  // of another repository's rules — a mistake that is silent afterwards,
1148
1211
  // because the rules look perfectly plausible, just not about your code.
1149
- const here = repoSlugFromGit(repo);
1150
1212
  const expected = await fetchExpectedRepo(link);
1151
1213
  if (expected && here && !sameRepo(expected.slug, `${here.owner}/${here.name}`)) {
1152
1214
  console.error(`✗ this brain is for ${expected.slug}, but you are in ${here.owner}/${here.name}`);
@@ -1181,8 +1243,46 @@ brain
1181
1243
  }
1182
1244
  const brainLabel = expected?.brainName ? `“${expected.brainName}”` : link.brainId;
1183
1245
  console.error(`✓ sent ${d.owner}/${d.name} to ${brainLabel}`);
1184
- console.error("· it is being mined into rules now — a few minutes. Watch it finish in your browser.");
1185
- console.error(" The rules reach this repo on their own; nothing else to run.");
1246
+ // Held rather than handed back. Everything above this line succeeded even
1247
+ // 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
1249
+ // the prompt here is what made that invisible from this side, on CI and
1250
+ // 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.
1252
+ 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.");
1255
+ return;
1256
+ }
1257
+ console.error("· building the brain — Ctrl-C detaches, it keeps going without you");
1258
+ const outcome = await watchBuild(link);
1259
+ if (outcome === "completed") {
1260
+ console.error("✓ the brain is built — its rules reach this repo on their own; nothing else to run");
1261
+ return;
1262
+ }
1263
+ // The ordinary ending for anything but a small repository. The history is
1264
+ // mined in slices, and the prompt comes back on the first of them: mining is
1265
+ // 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.
1268
+ 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.");
1271
+ return;
1272
+ }
1273
+ if (outcome === "failed") {
1274
+ // 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.");
1278
+ process.exitCode = 1;
1279
+ return;
1280
+ }
1281
+ 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;
1284
+ }
1285
+ console.error("· still building after 15 minutes — it has not failed, it is just long. Watch it finish in your browser.");
1186
1286
  });
1187
1287
  brain
1188
1288
  .command("status")