@nanonets/graft 0.21.0 → 0.22.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 +549 -315
- package/dist/app/review-process.d.ts.map +1 -1
- package/dist/app/review-process.js +6 -5
- package/dist/app/review-process.js.map +1 -1
- package/dist/app/sources.js +2 -2
- package/dist/app/sources.js.map +1 -1
- package/dist/ask/ask.d.ts +12 -6
- package/dist/ask/ask.d.ts.map +1 -1
- package/dist/ask/ask.js +28 -38
- package/dist/ask/ask.js.map +1 -1
- package/dist/bin/graft.d.ts +3 -0
- package/dist/bin/graft.d.ts.map +1 -0
- package/dist/bin/graft.js +8 -0
- package/dist/bin/graft.js.map +1 -0
- package/dist/bin/trail.d.ts +3 -0
- package/dist/bin/trail.d.ts.map +1 -0
- package/dist/bin/trail.js +6 -0
- package/dist/bin/trail.js.map +1 -0
- package/dist/blast/blast-cli.d.ts.map +1 -1
- package/dist/blast/blast-cli.js +2 -1
- package/dist/blast/blast-cli.js.map +1 -1
- package/dist/blast/render.d.ts.map +1 -1
- package/dist/blast/render.js +3 -2
- package/dist/blast/render.js.map +1 -1
- package/dist/brand.d.ts +97 -0
- package/dist/brand.d.ts.map +1 -0
- package/dist/brand.js +254 -0
- package/dist/brand.js.map +1 -0
- package/dist/check/local-check.d.ts +104 -0
- package/dist/check/local-check.d.ts.map +1 -0
- package/dist/check/local-check.js +830 -0
- package/dist/check/local-check.js.map +1 -0
- package/dist/check/types.d.ts +31 -0
- package/dist/check/types.d.ts.map +1 -0
- package/dist/check/types.js +2 -0
- package/dist/check/types.js.map +1 -0
- package/dist/claude/format.d.ts +44 -1
- package/dist/claude/format.d.ts.map +1 -1
- package/dist/claude/format.js +113 -19
- package/dist/claude/format.js.map +1 -1
- package/dist/claude/hook-layers.d.ts +8 -0
- package/dist/claude/hook-layers.d.ts.map +1 -0
- package/dist/claude/hook-layers.js +72 -0
- package/dist/claude/hook-layers.js.map +1 -0
- package/dist/claude/hooks.d.ts +34 -0
- package/dist/claude/hooks.d.ts.map +1 -1
- package/dist/claude/hooks.js +220 -12
- package/dist/claude/hooks.js.map +1 -1
- package/dist/claude/init.d.ts +12 -1
- package/dist/claude/init.d.ts.map +1 -1
- package/dist/claude/init.js +48 -19
- package/dist/claude/init.js.map +1 -1
- package/dist/claude/session-metrics.d.ts +1 -1
- package/dist/claude/session-metrics.d.ts.map +1 -1
- package/dist/claude/session-metrics.js +11 -8
- package/dist/claude/session-metrics.js.map +1 -1
- package/dist/claude/settings-merge.d.ts +1 -0
- package/dist/claude/settings-merge.d.ts.map +1 -1
- package/dist/claude/settings-merge.js +21 -18
- package/dist/claude/settings-merge.js.map +1 -1
- package/dist/claude/shim-template.d.ts +17 -2
- package/dist/claude/shim-template.d.ts.map +1 -1
- package/dist/claude/shim-template.js +116 -22
- package/dist/claude/shim-template.js.map +1 -1
- package/dist/claude/skill-template.d.ts +10 -1
- package/dist/claude/skill-template.d.ts.map +1 -1
- package/dist/claude/skill-template.js +132 -1
- package/dist/claude/skill-template.js.map +1 -1
- package/dist/claude/state.d.ts +8 -0
- package/dist/claude/state.d.ts.map +1 -1
- package/dist/claude/state.js.map +1 -1
- package/dist/claude/statusline.d.ts.map +1 -1
- package/dist/claude/statusline.js +4 -1
- package/dist/claude/statusline.js.map +1 -1
- package/dist/claude/tally.js +1 -1
- package/dist/claude/tally.js.map +1 -1
- package/dist/cli-epilogue.d.ts +0 -6
- package/dist/cli-epilogue.d.ts.map +1 -1
- package/dist/cli-epilogue.js +5 -4
- package/dist/cli-epilogue.js.map +1 -1
- package/dist/cli-meta.d.ts +8 -0
- package/dist/cli-meta.d.ts.map +1 -1
- package/dist/cli-meta.js +28 -8
- package/dist/cli-meta.js.map +1 -1
- package/dist/cli-picker.d.ts.map +1 -1
- package/dist/cli-picker.js +11 -7
- package/dist/cli-picker.js.map +1 -1
- package/dist/cli.d.ts +0 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1037 -595
- package/dist/cli.js.map +1 -1
- package/dist/cloud/diff.d.ts +18 -0
- package/dist/cloud/diff.d.ts.map +1 -0
- package/dist/cloud/diff.js +58 -0
- package/dist/cloud/diff.js.map +1 -0
- package/dist/cloud/nudge.d.ts +66 -0
- package/dist/cloud/nudge.d.ts.map +1 -0
- package/dist/cloud/nudge.js +269 -0
- package/dist/cloud/nudge.js.map +1 -0
- package/dist/cloud/workspace-sync.d.ts +58 -0
- package/dist/cloud/workspace-sync.d.ts.map +1 -0
- package/dist/cloud/workspace-sync.js +268 -0
- package/dist/cloud/workspace-sync.js.map +1 -0
- package/dist/cloud/workspace.d.ts +159 -0
- package/dist/cloud/workspace.d.ts.map +1 -0
- package/dist/cloud/workspace.js +134 -0
- package/dist/cloud/workspace.js.map +1 -0
- package/dist/context/check.d.ts +1 -1
- package/dist/context/check.d.ts.map +1 -1
- package/dist/context/check.js +9 -8
- package/dist/context/check.js.map +1 -1
- package/dist/context/node-file.js +1 -1
- package/dist/context/node-file.js.map +1 -1
- package/dist/context/savings.d.ts.map +1 -1
- package/dist/context/savings.js +10 -7
- package/dist/context/savings.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +3 -1
- package/dist/engine.js.map +1 -1
- package/dist/graph/build.d.ts +30 -0
- package/dist/graph/build.d.ts.map +1 -1
- package/dist/graph/build.js +65 -19
- package/dist/graph/build.js.map +1 -1
- package/dist/graph/cards.d.ts.map +1 -1
- package/dist/graph/cards.js +4 -2
- package/dist/graph/cards.js.map +1 -1
- package/dist/graph/check.d.ts.map +1 -1
- package/dist/graph/check.js +17 -18
- package/dist/graph/check.js.map +1 -1
- package/dist/graph/container.d.ts.map +1 -1
- package/dist/graph/container.js +7 -2
- package/dist/graph/container.js.map +1 -1
- package/dist/graph/extract-cache.d.ts +4 -0
- package/dist/graph/extract-cache.d.ts.map +1 -1
- package/dist/graph/extract-cache.js.map +1 -1
- package/dist/graph/extract.d.ts +2 -0
- package/dist/graph/extract.d.ts.map +1 -1
- package/dist/graph/extract.js +5 -32
- package/dist/graph/extract.js.map +1 -1
- package/dist/graph/generic.d.ts +7 -1
- package/dist/graph/generic.d.ts.map +1 -1
- package/dist/graph/generic.js +48 -8
- package/dist/graph/generic.js.map +1 -1
- package/dist/graph/native.d.ts +38 -0
- package/dist/graph/native.d.ts.map +1 -0
- package/dist/graph/native.js +169 -0
- package/dist/graph/native.js.map +1 -0
- package/dist/graph/refresh.d.ts.map +1 -1
- package/dist/graph/refresh.js +3 -2
- package/dist/graph/refresh.js.map +1 -1
- package/dist/graph/traverse-cli.d.ts.map +1 -1
- package/dist/graph/traverse-cli.js +4 -3
- package/dist/graph/traverse-cli.js.map +1 -1
- package/dist/graph/workspace-cli.d.ts +2 -0
- package/dist/graph/workspace-cli.d.ts.map +1 -1
- package/dist/graph/workspace-cli.js +13 -1
- package/dist/graph/workspace-cli.js.map +1 -1
- package/dist/graph/workspace.d.ts.map +1 -1
- package/dist/graph/workspace.js +6 -5
- package/dist/graph/workspace.js.map +1 -1
- package/dist/hosts/antigravity.d.ts +10 -3
- package/dist/hosts/antigravity.d.ts.map +1 -1
- package/dist/hosts/antigravity.js +27 -10
- package/dist/hosts/antigravity.js.map +1 -1
- package/dist/hosts/claude-global.d.ts +27 -4
- package/dist/hosts/claude-global.d.ts.map +1 -1
- package/dist/hosts/claude-global.js +90 -9
- package/dist/hosts/claude-global.js.map +1 -1
- package/dist/hosts/codex-hooks.d.ts.map +1 -1
- package/dist/hosts/codex-hooks.js +1 -2
- package/dist/hosts/codex-hooks.js.map +1 -1
- package/dist/hosts/config-write.d.ts.map +1 -1
- package/dist/hosts/config-write.js +2 -1
- package/dist/hosts/config-write.js.map +1 -1
- package/dist/hosts/cursor-hooks.d.ts +5 -2
- package/dist/hosts/cursor-hooks.d.ts.map +1 -1
- package/dist/hosts/cursor-hooks.js +14 -9
- package/dist/hosts/cursor-hooks.js.map +1 -1
- package/dist/hosts/init.d.ts +3 -0
- package/dist/hosts/init.d.ts.map +1 -1
- package/dist/hosts/init.js +32 -10
- package/dist/hosts/init.js.map +1 -1
- package/dist/hosts/instructions.d.ts +9 -4
- package/dist/hosts/instructions.d.ts.map +1 -1
- package/dist/hosts/instructions.js +19 -16
- package/dist/hosts/instructions.js.map +1 -1
- package/dist/hosts/mcp-config.d.ts +19 -4
- package/dist/hosts/mcp-config.d.ts.map +1 -1
- package/dist/hosts/mcp-config.js +81 -30
- package/dist/hosts/mcp-config.js.map +1 -1
- package/dist/hosts/plan.d.ts.map +1 -1
- package/dist/hosts/plan.js +11 -9
- package/dist/hosts/plan.js.map +1 -1
- package/dist/hosts/registry.d.ts +25 -2
- package/dist/hosts/registry.d.ts.map +1 -1
- package/dist/hosts/registry.js +53 -0
- package/dist/hosts/registry.js.map +1 -1
- package/dist/hosts/retract.d.ts.map +1 -1
- package/dist/hosts/retract.js +73 -33
- package/dist/hosts/retract.js.map +1 -1
- package/dist/hosts/sections.d.ts +17 -1
- package/dist/hosts/sections.d.ts.map +1 -1
- package/dist/hosts/sections.js +52 -2
- package/dist/hosts/sections.js.map +1 -1
- package/dist/mcp/instructions.d.ts +1 -25
- package/dist/mcp/instructions.d.ts.map +1 -1
- package/dist/mcp/instructions.js +6 -4
- package/dist/mcp/instructions.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +7 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tool-names.d.ts +10 -3
- package/dist/mcp/tool-names.d.ts.map +1 -1
- package/dist/mcp/tool-names.js +13 -5
- package/dist/mcp/tool-names.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +6 -5
- package/dist/mcp/tools.js.map +1 -1
- package/dist/notes/ask-notes.d.ts +8 -0
- package/dist/notes/ask-notes.d.ts.map +1 -0
- package/dist/notes/ask-notes.js +18 -0
- package/dist/notes/ask-notes.js.map +1 -0
- package/dist/notes/branches.d.ts +85 -0
- package/dist/notes/branches.d.ts.map +1 -0
- package/dist/notes/branches.js +238 -0
- package/dist/notes/branches.js.map +1 -0
- package/dist/notes/cold-start.d.ts +127 -0
- package/dist/notes/cold-start.d.ts.map +1 -0
- package/dist/notes/cold-start.js +476 -0
- package/dist/notes/cold-start.js.map +1 -0
- package/dist/notes/git-facts.d.ts +17 -0
- package/dist/notes/git-facts.d.ts.map +1 -0
- package/dist/notes/git-facts.js +117 -0
- package/dist/notes/git-facts.js.map +1 -0
- package/dist/notes/home.d.ts +87 -0
- package/dist/notes/home.d.ts.map +1 -0
- package/dist/notes/home.js +423 -0
- package/dist/notes/home.js.map +1 -0
- package/dist/notes/notes.d.ts +178 -0
- package/dist/notes/notes.d.ts.map +1 -0
- package/dist/notes/notes.js +498 -0
- package/dist/notes/notes.js.map +1 -0
- package/dist/notes/repo-trail.d.ts +16 -0
- package/dist/notes/repo-trail.d.ts.map +1 -0
- package/dist/notes/repo-trail.js +74 -0
- package/dist/notes/repo-trail.js.map +1 -0
- package/dist/notes/session-cost.d.ts +21 -0
- package/dist/notes/session-cost.d.ts.map +1 -0
- package/dist/notes/session-cost.js +162 -0
- package/dist/notes/session-cost.js.map +1 -0
- package/dist/search/grep-cli.d.ts.map +1 -1
- package/dist/search/grep-cli.js +4 -3
- package/dist/search/grep-cli.js.map +1 -1
- package/dist/skills/skills.d.ts +118 -0
- package/dist/skills/skills.d.ts.map +1 -0
- package/dist/skills/skills.js +370 -0
- package/dist/skills/skills.js.map +1 -0
- package/dist/telemetry/contract.d.ts +2 -14
- package/dist/telemetry/contract.d.ts.map +1 -1
- package/dist/telemetry/contract.js +39 -24
- package/dist/telemetry/contract.js.map +1 -1
- package/dist/telemetry/gate.d.ts.map +1 -1
- package/dist/telemetry/gate.js +2 -1
- package/dist/telemetry/gate.js.map +1 -1
- package/dist/telemetry/notice.d.ts +1 -1
- package/dist/telemetry/notice.d.ts.map +1 -1
- package/dist/telemetry/notice.js +6 -5
- package/dist/telemetry/notice.js.map +1 -1
- package/dist/telemetry/track.d.ts.map +1 -1
- package/dist/telemetry/track.js +5 -0
- package/dist/telemetry/track.js.map +1 -1
- package/dist/upkeep-run.d.ts +7 -0
- package/dist/upkeep-run.d.ts.map +1 -1
- package/dist/upkeep-run.js +52 -12
- package/dist/upkeep-run.js.map +1 -1
- package/dist/upkeep.d.ts +9 -19
- package/dist/upkeep.d.ts.map +1 -1
- package/dist/upkeep.js +38 -59
- package/dist/upkeep.js.map +1 -1
- package/dist/util/browser.d.ts +3 -0
- package/dist/util/browser.d.ts.map +1 -0
- package/dist/util/browser.js +14 -0
- package/dist/util/browser.js.map +1 -0
- package/dist/util/front-matter.d.ts +19 -0
- package/dist/util/front-matter.d.ts.map +1 -0
- package/dist/util/front-matter.js +21 -0
- package/dist/util/front-matter.js.map +1 -0
- package/dist/util/state.d.ts +1 -18
- package/dist/util/state.d.ts.map +1 -1
- package/dist/util/state.js +4 -2
- package/dist/util/state.js.map +1 -1
- package/dist/viz/assemble.js +1 -1
- package/dist/viz/assemble.js.map +1 -1
- package/dist/viz/serve.d.ts.map +1 -1
- package/dist/viz/serve.js +4 -3
- package/dist/viz/serve.js.map +1 -1
- package/package.json +7 -7
- package/scripts/pack-trail.mjs +53 -0
- package/scripts/postinstall.mjs +17 -2
- package/scripts/run-tests.mjs +4 -1
- package/dist/brain/attach.d.ts +0 -44
- package/dist/brain/attach.d.ts.map +0 -1
- package/dist/brain/attach.js +0 -63
- package/dist/brain/attach.js.map +0 -1
- package/dist/brain/claude-md.d.ts +0 -61
- package/dist/brain/claude-md.d.ts.map +0 -1
- package/dist/brain/claude-md.js +0 -171
- package/dist/brain/claude-md.js.map +0 -1
- package/dist/brain/connect.d.ts +0 -59
- package/dist/brain/connect.d.ts.map +0 -1
- package/dist/brain/connect.js +0 -80
- package/dist/brain/connect.js.map +0 -1
- package/dist/brain/context-files.d.ts +0 -79
- package/dist/brain/context-files.d.ts.map +0 -1
- package/dist/brain/context-files.js +0 -245
- package/dist/brain/context-files.js.map +0 -1
- package/dist/brain/link.d.ts +0 -106
- package/dist/brain/link.d.ts.map +0 -1
- package/dist/brain/link.js +0 -167
- package/dist/brain/link.js.map +0 -1
- package/dist/brain/pull.d.ts +0 -17
- package/dist/brain/pull.d.ts.map +0 -1
- package/dist/brain/pull.js +0 -172
- package/dist/brain/pull.js.map +0 -1
- package/dist/brain/push.d.ts +0 -155
- package/dist/brain/push.d.ts.map +0 -1
- package/dist/brain/push.js +0 -304
- package/dist/brain/push.js.map +0 -1
- package/dist/brain/signup.d.ts +0 -117
- package/dist/brain/signup.d.ts.map +0 -1
- package/dist/brain/signup.js +0 -276
- package/dist/brain/signup.js.map +0 -1
- package/dist/brain/upload.d.ts +0 -61
- package/dist/brain/upload.d.ts.map +0 -1
- package/dist/brain/upload.js +0 -210
- package/dist/brain/upload.js.map +0 -1
- package/dist/brain/watch.d.ts +0 -209
- package/dist/brain/watch.d.ts.map +0 -1
- package/dist/brain/watch.js +0 -396
- package/dist/brain/watch.js.map +0 -1
- package/dist/brain/wire.d.ts +0 -38
- package/dist/brain/wire.d.ts.map +0 -1
- package/dist/brain/wire.js +0 -133
- package/dist/brain/wire.js.map +0 -1
- package/dist/legacy-args.d.ts +0 -12
- package/dist/legacy-args.d.ts.map +0 -1
- package/dist/legacy-args.js +0 -31
- package/dist/legacy-args.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* `
|
|
4
|
-
* map, init. Git is the sync: commit
|
|
5
|
-
* workspace parent (≥2 git children)
|
|
3
|
+
* The `trail` CLI, also run as `graft`, its old name. Commands: build, ask,
|
|
4
|
+
* check, viz, mcp, callers, skeleton, grep, map, init. Git is the sync: commit
|
|
5
|
+
* graft/ and a clone has the graph. A workspace parent (≥2 git children)
|
|
6
|
+
* federates query commands across children.
|
|
7
|
+
*
|
|
8
|
+
* Both names run this file. Under `graft` every command keeps its old name and
|
|
9
|
+
* meaning; under `trail` the layout changes (see `TRAIL` below and brand.ts).
|
|
6
10
|
*/
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
7
12
|
import "dotenv/config";
|
|
8
13
|
import { Command } from "commander";
|
|
9
|
-
import {
|
|
14
|
+
import { statSync } from "node:fs";
|
|
15
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
10
16
|
import { fileURLToPath } from "node:url";
|
|
11
17
|
import { Graft } from "./engine.js";
|
|
12
18
|
import { resolveConfig } from "./ai/providers.js";
|
|
@@ -16,17 +22,10 @@ import { buildGraphIfMissing, runInit } from "./claude/init.js";
|
|
|
16
22
|
import { statuslineWanted } from "./claude/settings-merge.js";
|
|
17
23
|
import { runHostsInit } from "./hosts/init.js";
|
|
18
24
|
import { hostIds } from "./hosts/registry.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
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";
|
|
25
|
+
import { checkoutRoot, ensureRepoHome, keepsNotes, rememberRepo, repoName, repoPlace, shownPath } from "./notes/home.js";
|
|
26
|
+
import { brand, cmd, graftNotice, strandedRepoNotice, tag } from "./brand.js";
|
|
27
|
+
import { branchDiff } from "./cloud/diff.js";
|
|
28
|
+
import { openBrowser } from "./util/browser.js";
|
|
30
29
|
import { contextDirFor } from "./context/node-file.js";
|
|
31
30
|
import { loadGraphCached } from "./graph/load.js";
|
|
32
31
|
import { ensureFreshChildren, ensureFreshGraph, refreshNote } from "./graph/refresh.js";
|
|
@@ -41,15 +40,51 @@ import { planRetract, runRetract, changed } from "./hosts/retract.js";
|
|
|
41
40
|
import { compactWrites, formatNonInteractiveHelp, formatPlan, runPicker } from "./cli-picker.js";
|
|
42
41
|
import { buildGraphWithProgress } from "./claude/build-progress.js";
|
|
43
42
|
import { homedir } from "node:os";
|
|
44
|
-
import { formatUpgradeReport, formatVersionReport, getNpmViewVersion, readCurrentVersion, runUpgrade } from "./cli-meta.js";
|
|
43
|
+
import { formatUpgradeReport, formatVersionReport, getNpmViewVersion, readCurrentVersion, readPackageName, runUpgrade } from "./cli-meta.js";
|
|
44
|
+
import { nativeFallbackNotice } from "./graph/native.js";
|
|
45
45
|
import { patchBuildConfig } from "./util/state.js";
|
|
46
46
|
import { normalizePathPrefix } from "./util/paths.js";
|
|
47
47
|
import { latestSession, formatSessionStats, sessionInputRate } from "./claude/session-metrics.js";
|
|
48
|
+
import { listSessionIds, readSession, sessionDir } from "./claude/state.js";
|
|
49
|
+
import { costLabel, listNotes, noteName, notePlace, noteTargets, readingTokens, savedByNote, commitLines, PERSONAL_SAVED_LINE, SECTIONS, sectionLead, shortDate, tokensLabel, writeNote, mentionedFiles } from "./notes/notes.js";
|
|
50
|
+
import { currentSessionCost } from "./notes/session-cost.js";
|
|
51
|
+
import { beginPastRead, finishPastRead } from "./notes/cold-start.js";
|
|
52
|
+
import { currentBranch, noteAuthor, teammatesOn } from "./notes/git-facts.js";
|
|
53
|
+
import { confirm, fold, getSkill, learn, listSkills, publish, skillForAgent, skillName, skillStatus } from "./skills/skills.js";
|
|
54
|
+
import { clearPending, cloudLines, loginLink, sessionKey, waitForConnect } from "./cloud/nudge.js";
|
|
55
|
+
import { addRepo, clearCloud, cloudEnabled, fetchMe, fetchTeam, isError, readCloud, writeCloud } from "./cloud/workspace.js";
|
|
56
|
+
import { inWorkspace, maybeSyncInBackground, recordActivity, runSyncChild, syncRepo } from "./cloud/workspace-sync.js";
|
|
57
|
+
import { checkLocally } from "./check/local-check.js";
|
|
58
|
+
import { formatOverlap, maybeRefreshBranches, overlapsWith, pushedWhen, readBranchCache, refreshBranches } from "./notes/branches.js";
|
|
59
|
+
import { ensureRepoTrail, LEARNINGS_DIR } from "./notes/repo-trail.js";
|
|
48
60
|
import { setInputRate } from "./context/savings.js";
|
|
49
|
-
import { formatUpdateNudge, maybeRefreshInBackground, readStamp, readUpdateCache, refreshUpdateCache, wiredHostIds, writeStamp } from "./upkeep.js";
|
|
50
|
-
import {
|
|
61
|
+
import { formatUpdateNudge, maybeRefreshInBackground, readStamp, readUpdateCache, refreshUpdateCache, wiredHostIds, wiringOpts, writeStamp } from "./upkeep.js";
|
|
62
|
+
import { shouldMoveToTrail } from "./upkeep-run.js";
|
|
63
|
+
import { countBucket, savedTokensBucket, errorCode, filesBucket, durationBucket, formatDebug, formatStatus, firstRunNotice, isTrackedCommand, langsValue, offReason, maybeFlushInBackground, patchState, runFlush, track, trackFirstRunIfNew, } from "./telemetry/index.js";
|
|
51
64
|
const program = new Command();
|
|
52
65
|
const currentVersion = readCurrentVersion(import.meta.url);
|
|
66
|
+
/**
|
|
67
|
+
* Started as `trail` rather than `graft`. Decided once, before any command is
|
|
68
|
+
* registered, because it changes which commands exist: under trail the old
|
|
69
|
+
* `graft trail …` group moves to the top level, `check` hands its name to the
|
|
70
|
+
* team check (the freshness check is `build --check`), and `stats` and
|
|
71
|
+
* `trail status` become one `status`. Under graft nothing moves.
|
|
72
|
+
*/
|
|
73
|
+
const TRAIL = brand() === "trail";
|
|
74
|
+
/** Help-screen groups under trail; graft keeps its one flat list. */
|
|
75
|
+
const GROUP = {
|
|
76
|
+
find: "Find code · your agent runs these:",
|
|
77
|
+
memory: "Team memory:",
|
|
78
|
+
setup: "Setup:",
|
|
79
|
+
cloud: "Workspace · your team's view, across repos:",
|
|
80
|
+
ci: "CI:",
|
|
81
|
+
};
|
|
82
|
+
/** Put a command in a help group, under trail only. */
|
|
83
|
+
function grouped(c, heading) {
|
|
84
|
+
if (TRAIL)
|
|
85
|
+
c.helpGroup(heading);
|
|
86
|
+
return c;
|
|
87
|
+
}
|
|
53
88
|
/**
|
|
54
89
|
* What the `query` telemetry event will say, filled in by the command as it runs
|
|
55
90
|
* and emitted once from the `postAction` hook below.
|
|
@@ -75,8 +110,11 @@ function noteHit(found) {
|
|
|
75
110
|
queryNote.hit = found ? "yes" : "no";
|
|
76
111
|
}
|
|
77
112
|
program
|
|
78
|
-
.name(
|
|
79
|
-
.description(
|
|
113
|
+
.name(brand())
|
|
114
|
+
.description(TRAIL
|
|
115
|
+
? "Memory for your coding agents: a code map, and the learnings each session\n" +
|
|
116
|
+
"leaves in your repo's .trail/, so your whole team's Claude starts from them."
|
|
117
|
+
: "Build a repo's context graph as linked markdown, and keep it in sync with the code.")
|
|
80
118
|
.version(currentVersion, "-v, --version")
|
|
81
119
|
.option("--dir <path>", "context graph directory (default: <repo>/graft)")
|
|
82
120
|
.option("--provider <name>", "LLM wire format: openai | anthropic | litellm | orcarouter (env GRAFT_PROVIDER)")
|
|
@@ -125,7 +163,7 @@ function queryRoot(dir) {
|
|
|
125
163
|
return resolve(dir);
|
|
126
164
|
const { root, levels } = nearestGraftRoot(process.cwd(), program.opts().dir);
|
|
127
165
|
if (levels > 0)
|
|
128
|
-
console.error(
|
|
166
|
+
console.error(`${tag()} no graft/ here — answering from ${root}/graft`);
|
|
129
167
|
return root;
|
|
130
168
|
}
|
|
131
169
|
/**
|
|
@@ -174,7 +212,7 @@ function parseTabs(raw) {
|
|
|
174
212
|
* not editorialize on stderr at startup (`mcp` runs its own upkeep at boot, and
|
|
175
213
|
* `_update-check` IS the fetch).
|
|
176
214
|
*/
|
|
177
|
-
const UPKEEP_SKIP = new Set(["version", "upgrade", "_update-check", "
|
|
215
|
+
const UPKEEP_SKIP = new Set(["version", "upgrade", "_update-check", "_trail-past-read", "mcp"]);
|
|
178
216
|
/**
|
|
179
217
|
* Every other command: top up the cached registry answer in the background and,
|
|
180
218
|
* if a newer graft is out, say so once on stderr. This is what makes the CLI the
|
|
@@ -183,6 +221,16 @@ const UPKEEP_SKIP = new Set(["version", "upgrade", "_update-check", "_brain-refr
|
|
|
183
221
|
program.hook("preAction", (_parent, action) => {
|
|
184
222
|
if (UPKEEP_SKIP.has(action.name()))
|
|
185
223
|
return;
|
|
224
|
+
// Someone typing graft by hand learns its new name, once a day. Agents,
|
|
225
|
+
// hooks, CI and pipes never see it (see graftNotice).
|
|
226
|
+
const renamed = graftNotice({ ran: spelling(action), tty: Boolean(process.stderr.isTTY) });
|
|
227
|
+
if (renamed)
|
|
228
|
+
console.error(renamed);
|
|
229
|
+
// Someone typing trail in a repo whose graft-era hooks can't reach it learns
|
|
230
|
+
// the one command that moves it (init itself is that command).
|
|
231
|
+
const stranded = action.name() === "init" ? null : strandedRepoNotice({ cwd: process.cwd(), tty: Boolean(process.stderr.isTTY) });
|
|
232
|
+
if (stranded)
|
|
233
|
+
console.error(stranded);
|
|
186
234
|
maybeRefreshInBackground();
|
|
187
235
|
const nudge = formatUpdateNudge(currentVersion, readUpdateCache()?.latest);
|
|
188
236
|
if (nudge)
|
|
@@ -203,26 +251,52 @@ program.hook("preAction", (_parent, action) => {
|
|
|
203
251
|
* is the right failure mode for a metric.
|
|
204
252
|
*/
|
|
205
253
|
program.hook("postAction", (_parent, action) => {
|
|
206
|
-
|
|
254
|
+
// `trail build --check` is graft's `check`, and counts as it always has.
|
|
255
|
+
const name = queryNote.command ?? action.name();
|
|
207
256
|
if (!isTrackedCommand(name))
|
|
208
257
|
return;
|
|
209
|
-
track("query", { command: name, surface: "cli", hit: queryNote.hit }, { repo: queryNote.repo });
|
|
258
|
+
track("query", { command: name, surface: "cli", hit: queryNote.hit, notes: queryNote.notes }, { repo: queryNote.repo });
|
|
259
|
+
});
|
|
260
|
+
/** `graft ask`, `graft build`: a command as graft spells it, whatever ran it. */
|
|
261
|
+
function spelling(action) {
|
|
262
|
+
const words = [];
|
|
263
|
+
for (let c = action; c && c.parent; c = c.parent)
|
|
264
|
+
words.unshift(c.name());
|
|
265
|
+
return ["graft", ...words].join(" ");
|
|
266
|
+
}
|
|
267
|
+
// Hidden: spawned detached by maybeSyncInBackground when a workspace repo's
|
|
268
|
+
// learnings changed, or uses are waiting to go. Running it by hand syncs now.
|
|
269
|
+
// A repo that moved from graft to trail at session start reads its past
|
|
270
|
+
// sessions here, detached, so the session doesn't wait on it.
|
|
271
|
+
program
|
|
272
|
+
.command("_trail-past-read", { hidden: true })
|
|
273
|
+
.argument("<dir>")
|
|
274
|
+
.action(async (dir) => {
|
|
275
|
+
const repo = resolve(dir);
|
|
276
|
+
const read = beginPastRead(repo);
|
|
277
|
+
if (read)
|
|
278
|
+
await finishPastRead(repo, read);
|
|
210
279
|
});
|
|
211
|
-
// Hidden from --help: only ever spawned detached by maybeRefreshInBackground.
|
|
212
280
|
program
|
|
213
|
-
.command("
|
|
214
|
-
.description("internal:
|
|
281
|
+
.command("_trail-sync", { hidden: true })
|
|
282
|
+
.description("internal: send this repo's learnings and the waiting uses to the Trail workspace")
|
|
215
283
|
.argument("[dir]", "target repo directory", ".")
|
|
216
284
|
.action(async (dir) => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
285
|
+
await runSyncChild(resolve(dir));
|
|
286
|
+
});
|
|
287
|
+
// Hidden: spawned detached by maybeRefreshBranches.
|
|
288
|
+
program
|
|
289
|
+
.command("_trail-branches", { hidden: true })
|
|
290
|
+
.description("internal: fetch, then note which files teammates' pushed branches change and the learnings they carry")
|
|
291
|
+
.argument("[dir]", "target repo directory", ".")
|
|
292
|
+
.action((dir) => {
|
|
293
|
+
refreshBranches(resolve(dir));
|
|
294
|
+
});
|
|
295
|
+
program
|
|
296
|
+
.command("_trail-claim", { hidden: true })
|
|
297
|
+
.description("internal: wait for a sign-in through the link trail handed the agent, then connect the workspace")
|
|
298
|
+
.action(async () => {
|
|
299
|
+
await waitForConnect();
|
|
226
300
|
});
|
|
227
301
|
program
|
|
228
302
|
.command("_update-check", { hidden: true })
|
|
@@ -238,8 +312,7 @@ program
|
|
|
238
312
|
.action(async () => {
|
|
239
313
|
await runFlush();
|
|
240
314
|
});
|
|
241
|
-
program
|
|
242
|
-
.command("telemetry")
|
|
315
|
+
grouped(program.command("telemetry"), GROUP.setup)
|
|
243
316
|
.description("Show, inspect, or turn off the anonymous usage stats (see TELEMETRY.md)")
|
|
244
317
|
.argument("[action]", "status (default) | enable | disable | debug", "status")
|
|
245
318
|
.action((action) => {
|
|
@@ -249,7 +322,7 @@ program
|
|
|
249
322
|
return;
|
|
250
323
|
case "enable":
|
|
251
324
|
patchState({ enabled: true });
|
|
252
|
-
console.log(
|
|
325
|
+
console.log(`telemetry: on — anonymous, aggregate-only. \`${cmd("graft telemetry status")}\` for details.`);
|
|
253
326
|
return;
|
|
254
327
|
case "disable":
|
|
255
328
|
// Also stamp the notice as shown: someone who has just opted out should
|
|
@@ -265,27 +338,49 @@ program
|
|
|
265
338
|
process.exit(1);
|
|
266
339
|
}
|
|
267
340
|
});
|
|
341
|
+
// Hidden under trail, where -v says the same; listed under graft as it was.
|
|
268
342
|
program
|
|
269
|
-
.command("version")
|
|
343
|
+
.command("version", { hidden: TRAIL })
|
|
270
344
|
.description("Print the installed version and the latest published on npm")
|
|
271
345
|
.action(() => {
|
|
272
346
|
const latest = getNpmViewVersion();
|
|
273
347
|
console.log(formatVersionReport(currentVersion, latest));
|
|
274
348
|
});
|
|
275
|
-
program
|
|
276
|
-
.
|
|
277
|
-
.description("Upgrade the globally installed graft to the latest version on npm")
|
|
349
|
+
grouped(program.command("upgrade"), GROUP.setup)
|
|
350
|
+
.description(`Upgrade the globally installed ${brand()} to the latest version on npm`)
|
|
278
351
|
.action(() => {
|
|
279
352
|
const result = runUpgrade(import.meta.url);
|
|
280
353
|
console.log(formatUpgradeReport(result));
|
|
281
354
|
if (result.ran && !result.ok)
|
|
282
355
|
process.exit(1);
|
|
356
|
+
if (!TRAIL && result.ok && result.trailVersion)
|
|
357
|
+
moveHereToTrail(process.cwd());
|
|
283
358
|
});
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
359
|
+
/**
|
|
360
|
+
* After `graft upgrade` brought the trail command along, the repo it ran in
|
|
361
|
+
* moves to trail now, with the agents and flags its last init chose, and its
|
|
362
|
+
* past sessions are read for learnings. Every other repo wired as graft moves
|
|
363
|
+
* the next time a session opens in it (upkeep-run.ts).
|
|
364
|
+
*/
|
|
365
|
+
function moveHereToTrail(dir) {
|
|
366
|
+
const root = checkoutRoot(dir);
|
|
367
|
+
if (!root || !shouldMoveToTrail(root))
|
|
368
|
+
return;
|
|
369
|
+
const stamp = readStamp(root);
|
|
370
|
+
const hosts = [...new Set([...(stamp?.hosts ?? []), ...wiredHostIds(root)])];
|
|
371
|
+
const o = wiringOpts(stamp);
|
|
372
|
+
const flags = [o.global ? null : "--no-global", o.mcp ? null : "--no-mcp", o.hooks ? null : "--no-hooks", o.statusline ? null : "--no-statusline"].filter((f) => f !== null);
|
|
373
|
+
console.log(`· moving this repo to trail: trail init ${hosts.length ? `--agents ${hosts.join(" ")} ` : ""}${flags.join(" ")}`.trimEnd());
|
|
374
|
+
spawnSync("trail", ["init", root, "--agents", ...hosts, ...flags], { stdio: "inherit" });
|
|
375
|
+
}
|
|
376
|
+
grouped(program.command("build"), GROUP.setup)
|
|
377
|
+
.description(TRAIL
|
|
378
|
+
? "Rebuild the code map in graft/ ($0, no key). --check fails if it's stale; --deep adds LLM summaries."
|
|
379
|
+
: "Build graft/ from your code — wiring graph + per-file cards ($0, no key). " +
|
|
380
|
+
"Add --deep for the LLM concept map + per-symbol summaries/crux.")
|
|
288
381
|
.argument("[dir]", "repository root", ".")
|
|
382
|
+
.option("--check", "build nothing: fail if graft/ is stale relative to the code (for CI)")
|
|
383
|
+
.option("--json", "with --check: output the drift as JSON")
|
|
289
384
|
.option("--deep", "run the LLM pass: concept nodes (graft/*.md) + per-symbol summary/crux")
|
|
290
385
|
.option("-e, --extensions <exts...>", 'code extensions to include (e.g. ".ts" ".py"); an extension with no parser is ignored with a warning that lists the supported set')
|
|
291
386
|
.option("-j, --concurrency <n>", "files summarized in parallel during --deep (default 5)")
|
|
@@ -305,6 +400,13 @@ program
|
|
|
305
400
|
.option("--no-gitignore", "skip writing graft/ into .gitignore (same as GRAFT_NO_GITIGNORE=1)")
|
|
306
401
|
.option("--no-ignore", "skip writing .ignore for ripgrep re-admit (same as GRAFT_NO_IGNORE=1)")
|
|
307
402
|
.action(async (dir, opts, command) => {
|
|
403
|
+
if (opts.check) {
|
|
404
|
+
// The freshness check, which was `graft check`. Counted as `check` so the
|
|
405
|
+
// metric carries on across the rename; the dir defaults like a query's.
|
|
406
|
+
queryNote.command = "check";
|
|
407
|
+
await runCheckCommand(command.args[0], { extensions: opts.extensions, json: opts.json }, cmd("graft check"));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
308
410
|
const buildStartedAt = Date.now();
|
|
309
411
|
if (opts.gitignore === false)
|
|
310
412
|
process.env.GRAFT_NO_GITIGNORE = "1";
|
|
@@ -378,7 +480,7 @@ program
|
|
|
378
480
|
deep = false;
|
|
379
481
|
console.error("⚠ no API key set — falling back to the structural build (no LLM summaries).\n" +
|
|
380
482
|
" Set GRAFT_API_KEY (and GRAFT_PROVIDER / GRAFT_BASE_URL / GRAFT_MODEL for your\n" +
|
|
381
|
-
|
|
483
|
+
` provider) and re-run \`${cmd("graft build --deep")}\` to add concept nodes and summaries.`);
|
|
382
484
|
}
|
|
383
485
|
if (deep && resolved.usedLegacyEnv) {
|
|
384
486
|
console.error("⚠ using OPENROUTER_API_KEY (deprecated) — prefer GRAFT_API_KEY + GRAFT_BASE_URL.");
|
|
@@ -396,6 +498,7 @@ program
|
|
|
396
498
|
includeDirs: opts.includeDir,
|
|
397
499
|
followSubmodules: followSubmodulesWasExplicit ? opts.followSubmodules : undefined,
|
|
398
500
|
followNestedRepos: followNestedReposWasExplicit ? opts.followNestedRepos : undefined,
|
|
501
|
+
packageName: readPackageName(import.meta.url),
|
|
399
502
|
});
|
|
400
503
|
return;
|
|
401
504
|
}
|
|
@@ -453,12 +556,17 @@ program
|
|
|
453
556
|
}, { repo: buildRoot });
|
|
454
557
|
for (const e of g.errors)
|
|
455
558
|
console.error(`✗ ${e}`);
|
|
559
|
+
// A language whose native grammar didn't load was parsed by its WASM stand-in:
|
|
560
|
+
// say which, and the install that fixes it. Nothing when everything loaded.
|
|
561
|
+
const fallbackNotice = nativeFallbackNotice(g.nativeFallback, readPackageName(import.meta.url));
|
|
562
|
+
if (fallbackNotice)
|
|
563
|
+
console.error(fallbackNotice);
|
|
456
564
|
const rel = relative(process.cwd(), g.contextDir) || "graft";
|
|
457
565
|
if (process.env.GRAFT_NO_GITIGNORE) {
|
|
458
566
|
console.log(` ${rel}/ is a local cache — add it to your gitignore if you want it untracked.`);
|
|
459
567
|
}
|
|
460
568
|
else {
|
|
461
|
-
console.log(` ${rel}/ is git-ignored (added automatically) — a local cache; teammates run
|
|
569
|
+
console.log(` ${rel}/ is git-ignored (added automatically) — a local cache; teammates run \`${cmd("graft build")}\` to get their own.`);
|
|
462
570
|
}
|
|
463
571
|
// #127: a --deep run whose LLM calls failed used to print the same success
|
|
464
572
|
// footer and exit 0, so a quota-exhausted build looked identical to a clean
|
|
@@ -485,15 +593,58 @@ program
|
|
|
485
593
|
if (conceptErrors.length > 0)
|
|
486
594
|
console.error(` ${conceptErrors.length} concept-pass error(s).`);
|
|
487
595
|
console.error(` meaning coverage: ${ready}/${total} symbols (${pct}%).`);
|
|
488
|
-
console.error(
|
|
596
|
+
console.error(` Nothing computed was lost: re-run \`${cmd("graft build --deep")}\` to resume from what is cached.\n` +
|
|
489
597
|
" Pass --allow-partial to accept a degraded meaning tier and exit 0.");
|
|
490
598
|
if (!opts.allowPartial)
|
|
491
599
|
process.exitCode = 1;
|
|
492
600
|
}
|
|
493
601
|
}
|
|
494
602
|
});
|
|
495
|
-
|
|
496
|
-
|
|
603
|
+
/** The file part of a hit's `file:Lx-Ly` pointer. */
|
|
604
|
+
function pointerFile(pointer) {
|
|
605
|
+
return pointer.replace(/:L\d+(-L?\d+)?$/, "");
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* After `ask`, under trail: which shown learnings are a teammate's, which
|
|
609
|
+
* teammates' pushed branches change the files the answer points into, and the
|
|
610
|
+
* shown learnings recorded as a use, which is what the team page counts as
|
|
611
|
+
* reuse. Then the background upkeep that keeps branches and the workspace
|
|
612
|
+
* current. Spawns no git itself.
|
|
613
|
+
*/
|
|
614
|
+
function afterAsk(r, repo) {
|
|
615
|
+
if (!r.notesChecked)
|
|
616
|
+
return;
|
|
617
|
+
rememberRepo(repo);
|
|
618
|
+
maybeRefreshBranches(repo);
|
|
619
|
+
const me = noteAuthor(repo);
|
|
620
|
+
const files = [...new Set(r.hits.map((h) => pointerFile(h.pointer)))];
|
|
621
|
+
const overlaps = overlapsWith(repo, files, me).slice(0, 2);
|
|
622
|
+
if (overlaps.length) {
|
|
623
|
+
r.overlaps = overlaps.map((o) => formatOverlap(o));
|
|
624
|
+
if (!process.stdout.isTTY)
|
|
625
|
+
r.overlaps.push(`${tag()} Start your reply with the overlap line${overlaps.length === 1 ? "" : "s"} above, word for word, so the user knows.`);
|
|
626
|
+
}
|
|
627
|
+
const shown = r.notes ?? [];
|
|
628
|
+
if (shown.length) {
|
|
629
|
+
for (const h of shown)
|
|
630
|
+
h.note.teammate = !!h.note.author && !!me && h.note.author !== me;
|
|
631
|
+
recordActivity({
|
|
632
|
+
repo: repoPlace(repo).key,
|
|
633
|
+
kind: "search",
|
|
634
|
+
asker: me,
|
|
635
|
+
session: sessionKey(repo) ?? `${me}-${new Date().toISOString().slice(0, 10)}`,
|
|
636
|
+
query: r.query,
|
|
637
|
+
files,
|
|
638
|
+
shown_paths: shown.map((h) => noteName(h.note)),
|
|
639
|
+
shown_authors: shown.map((h) => h.note.author),
|
|
640
|
+
at: new Date().toISOString(),
|
|
641
|
+
});
|
|
642
|
+
const saved = shown.reduce((sum, h) => sum + savedByNote(h.note), 0);
|
|
643
|
+
track("learning_reused", { teammate: String(shown.some((h) => h.note.teammate)), saved_tokens_bucket: savedTokensBucket(saved) }, { repo });
|
|
644
|
+
}
|
|
645
|
+
maybeSyncInBackground(repo);
|
|
646
|
+
}
|
|
647
|
+
grouped(program.command("ask"), GROUP.find)
|
|
497
648
|
.description("Query the graft/ graph — returns ranked nodes + exact file:line, routed to prose or wiring ($0, no key)")
|
|
498
649
|
.argument("<query>", "what you want to understand, in plain words")
|
|
499
650
|
.argument(...DIR_ARG)
|
|
@@ -525,16 +676,22 @@ program
|
|
|
525
676
|
return;
|
|
526
677
|
}
|
|
527
678
|
noteHit(r.hits.length > 0);
|
|
679
|
+
// Teammates' branches on the same files, and the learnings shown, counted as a use.
|
|
680
|
+
if (TRAIL)
|
|
681
|
+
afterAsk(r, dir);
|
|
682
|
+
// How many notes reached the agent: the demand side of `trail note`.
|
|
683
|
+
if (r.notesChecked)
|
|
684
|
+
queryNote.notes = String(Math.min(r.notes?.length ?? 0, 2));
|
|
528
685
|
if (opts.json) {
|
|
529
686
|
console.log(JSON.stringify(r, null, 2));
|
|
530
687
|
}
|
|
531
688
|
else {
|
|
532
689
|
const { formatAsk } = await import("./ask/ask.js");
|
|
533
690
|
process.stdout.write(formatAsk(r));
|
|
691
|
+
printLines(cloudLines(dir, { command: "ask" }));
|
|
534
692
|
}
|
|
535
693
|
});
|
|
536
|
-
program
|
|
537
|
-
.command("skeleton")
|
|
694
|
+
grouped(program.command("skeleton"), GROUP.find)
|
|
538
695
|
.description("Signatures-only view of one file from the wiring graph — the cheapest way to see a file's API surface")
|
|
539
696
|
.argument("<file>", "repo-relative path (or unique basename) of the file")
|
|
540
697
|
.argument(...DIR_ARG)
|
|
@@ -551,13 +708,24 @@ program
|
|
|
551
708
|
else
|
|
552
709
|
process.stdout.write(formatSkeleton(r));
|
|
553
710
|
});
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
711
|
+
if (!TRAIL) {
|
|
712
|
+
program
|
|
713
|
+
.command("check")
|
|
714
|
+
.description("Fail if graft/ is stale relative to the code (for CI)")
|
|
715
|
+
.argument(...DIR_ARG)
|
|
716
|
+
.option("-e, --extensions <exts...>", "code extensions to include")
|
|
717
|
+
.option("--json", "output the drift as JSON")
|
|
718
|
+
.action(async (dirArg, opts) => {
|
|
719
|
+
await runCheckCommand(dirArg, opts, "graft check");
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
// Under trail, `check` is the team check (teamCheckCommand, with the cloud
|
|
723
|
+
// commands below); the freshness check is `trail build --check`.
|
|
724
|
+
/**
|
|
725
|
+
* The freshness check: `graft check`, and `trail build --check`. `label` is how
|
|
726
|
+
* the report names the command, so each name prints its own spelling.
|
|
727
|
+
*/
|
|
728
|
+
async function runCheckCommand(dirArg, opts, label) {
|
|
561
729
|
warnUnsupportedExtensions(opts.extensions);
|
|
562
730
|
const dir = noteQuery(queryRoot(dirArg));
|
|
563
731
|
const checkGlobalDir = program.opts().dir;
|
|
@@ -577,24 +745,25 @@ program
|
|
|
577
745
|
console.log(JSON.stringify({ context: r, graph: g.missing ? null : g }, null, 2));
|
|
578
746
|
}
|
|
579
747
|
else if (bothMissing) {
|
|
580
|
-
console.log(
|
|
748
|
+
console.log(`${label}: NO GRAPH\n\nNo graft/ graph found. Run \`${cmd("graft build")}\` first.`);
|
|
581
749
|
}
|
|
582
750
|
else {
|
|
583
751
|
if (r.missing) {
|
|
584
|
-
console.log(
|
|
752
|
+
console.log(`deep layer: not built (run \`${cmd("graft build --deep")}\` for concept nodes) — wiring graph is the source of truth`);
|
|
585
753
|
}
|
|
586
754
|
else {
|
|
587
|
-
console.log(formatCheckReport(r));
|
|
755
|
+
console.log(formatCheckReport(r, label));
|
|
588
756
|
}
|
|
589
757
|
if (!g.missing)
|
|
590
758
|
console.log("\n" + formatGraphCheckReport(g));
|
|
591
759
|
}
|
|
592
760
|
if (bothMissing || markdownFail || wiringFail)
|
|
593
761
|
process.exit(1);
|
|
594
|
-
}
|
|
762
|
+
}
|
|
763
|
+
// Under trail this is part of `status`; the old name still runs, unlisted.
|
|
595
764
|
program
|
|
596
|
-
.command("stats")
|
|
597
|
-
.description(
|
|
765
|
+
.command("stats", { hidden: TRAIL })
|
|
766
|
+
.description(`Show this agent session's ${brand()}-vs-source usage mix and tokens saved`)
|
|
598
767
|
.argument(...DIR_ARG)
|
|
599
768
|
.option("--json", "output the session stats as JSON")
|
|
600
769
|
.action((dirArg, opts) => {
|
|
@@ -610,7 +779,7 @@ program
|
|
|
610
779
|
console.log(formatSessionStats(s));
|
|
611
780
|
});
|
|
612
781
|
program
|
|
613
|
-
.command("viz")
|
|
782
|
+
.command("viz", { hidden: TRAIL })
|
|
614
783
|
.description("Serve an interactive visualization of the context graph (and graph.json when present)")
|
|
615
784
|
.argument(...DIR_ARG)
|
|
616
785
|
.option("-p, --port <port>", "port to serve on", "4400")
|
|
@@ -634,7 +803,7 @@ program
|
|
|
634
803
|
const globalOpts = program.opts();
|
|
635
804
|
const contextDir = contextDirFor(root, globalOpts.dir);
|
|
636
805
|
if (!existsSync(contextDir)) {
|
|
637
|
-
console.error(`✗ no context graph at ${contextDir} — run
|
|
806
|
+
console.error(`✗ no context graph at ${contextDir} — run \`${cmd("graft build --deep")}\` first`);
|
|
638
807
|
process.exit(1);
|
|
639
808
|
}
|
|
640
809
|
const viewerDir = fileURLToPath(new URL("./viewer/", import.meta.url)); // prebuilt
|
|
@@ -649,7 +818,7 @@ program
|
|
|
649
818
|
tabs,
|
|
650
819
|
});
|
|
651
820
|
const kb = Math.round(out.bytes / 1024);
|
|
652
|
-
console.log(
|
|
821
|
+
console.log(`${cmd("graft viz")} → ${out.file} (${kb} kB, ${out.contextNodes} concept nodes, ${out.codeNodes} code nodes)`);
|
|
653
822
|
return;
|
|
654
823
|
}
|
|
655
824
|
const srv = await startVizServer({
|
|
@@ -658,14 +827,14 @@ program
|
|
|
658
827
|
port: Number(opts.port),
|
|
659
828
|
repoName: basename(root),
|
|
660
829
|
});
|
|
661
|
-
console.log(
|
|
830
|
+
console.log(`${cmd("graft viz")} → ${srv.url} (ctrl-c to stop)`);
|
|
662
831
|
if (opts.open) {
|
|
663
832
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
664
833
|
spawn(opener, [srv.url], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
|
|
665
834
|
}
|
|
666
835
|
});
|
|
667
836
|
program
|
|
668
|
-
.command("mcp")
|
|
837
|
+
.command("mcp", { hidden: TRAIL })
|
|
669
838
|
.description("Serve the graph over MCP (stdio) — exposes graft_find_code, graft_trace_calls, graft_find_all, graft_file_api, graft_repo_map and graft_check_freshness as tools")
|
|
670
839
|
.argument(...DIR_ARG)
|
|
671
840
|
.action(async (dirArg) => {
|
|
@@ -674,8 +843,7 @@ program
|
|
|
674
843
|
const globalOpts = program.opts();
|
|
675
844
|
startMcpServer(dir, globalOpts.dir, currentVersion);
|
|
676
845
|
});
|
|
677
|
-
program
|
|
678
|
-
.command("callers")
|
|
846
|
+
grouped(program.command("callers"), GROUP.find)
|
|
679
847
|
.description("Who calls/references a symbol ($0, no LLM). --direction out gives callees (what it calls); --depth N (or all) walks transitively for full blast radius")
|
|
680
848
|
.argument("<symbol>", "bare name, qualified (Class.method), or package-qualified (pkg.Fn)")
|
|
681
849
|
.argument(...DIR_ARG)
|
|
@@ -707,8 +875,7 @@ program
|
|
|
707
875
|
globalDir: globalOpts.dir,
|
|
708
876
|
});
|
|
709
877
|
});
|
|
710
|
-
program
|
|
711
|
-
.command("blast")
|
|
878
|
+
grouped(program.command("blast"), GROUP.ci)
|
|
712
879
|
.description("Blast radius of a diff: what depends on the lines this change touched ($0, no LLM). " +
|
|
713
880
|
"Built for CI — `--format markdown` is a PR comment with a Mermaid diagram.")
|
|
714
881
|
.argument(...DIR_ARG)
|
|
@@ -737,8 +904,7 @@ program
|
|
|
737
904
|
globalDir: program.opts().dir,
|
|
738
905
|
});
|
|
739
906
|
});
|
|
740
|
-
program
|
|
741
|
-
.command("grep")
|
|
907
|
+
grouped(program.command("grep"), GROUP.find)
|
|
742
908
|
.description("Regex search over indexed files, hits grouped by enclosing symbol and ranked by coupling ($0, no LLM)")
|
|
743
909
|
.argument("<pattern>", "regex pattern (or literal string with --fixed)")
|
|
744
910
|
.argument(...DIR_ARG)
|
|
@@ -766,8 +932,7 @@ program
|
|
|
766
932
|
globalDir: globalOpts.dir,
|
|
767
933
|
});
|
|
768
934
|
});
|
|
769
|
-
program
|
|
770
|
-
.command("map")
|
|
935
|
+
grouped(program.command("map"), GROUP.find)
|
|
771
936
|
.description("Token-budgeted repo orientation — directory clusters, per-directory hubs, and global hotspots from the wiring graph ($0, no LLM)")
|
|
772
937
|
.argument(...DIR_ARG)
|
|
773
938
|
.option("--max-dirs <n>", "max directory entries shown, rest counted into dropped (default 16)")
|
|
@@ -796,7 +961,7 @@ program
|
|
|
796
961
|
const contextDir = contextDirFor(root, globalOpts.dir);
|
|
797
962
|
const graph = loadGraphCached(contextDir);
|
|
798
963
|
if (!graph) {
|
|
799
|
-
console.error(
|
|
964
|
+
console.error(`✗ no graph — run ${cmd("graft build")} first`);
|
|
800
965
|
process.exit(1);
|
|
801
966
|
return;
|
|
802
967
|
}
|
|
@@ -807,9 +972,175 @@ program
|
|
|
807
972
|
}
|
|
808
973
|
process.stdout.write(formatRepoMap(map));
|
|
809
974
|
});
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
975
|
+
// `trail note`: what a session worked out, saved for the next person. Listed
|
|
976
|
+
// under trail; under graft it still runs, unlisted, for a teammate on graft in
|
|
977
|
+
// a repo where someone else set up trail.
|
|
978
|
+
grouped(program.command("note", { hidden: !TRAIL }), GROUP.memory)
|
|
979
|
+
.description("Save what this session decided, tried and ruled out, as a learning in the repo's .trail/learnings/ (the text on stdin)")
|
|
980
|
+
.requiredOption("--title <text>", "what the session was about, in a few words")
|
|
981
|
+
.option("-m, --body <text>", "the learning itself, instead of stdin")
|
|
982
|
+
.option("--personal", "about your own setup or habits, not the code: keep it in ~/.trail, out of the repo")
|
|
983
|
+
.option("--touches <list>", "comma-separated files or file#Symbol it's about (default: the files this session changed, in every repo it changed)")
|
|
984
|
+
.option("--author <name>", "who wrote it (default: the first word of git config user.name)")
|
|
985
|
+
.option("--minutes <n>", "what it took to work out, when the agent's transcript can't say")
|
|
986
|
+
.option("--tokens <n>", "the same, in tokens")
|
|
987
|
+
.option("--json", "print the saved learning as JSON")
|
|
988
|
+
.argument("[dir]", "repository root", ".")
|
|
989
|
+
.action(async (dir, opts) => {
|
|
990
|
+
await runNoteCommand(resolve(dir), opts);
|
|
991
|
+
});
|
|
992
|
+
// `trail learn` and `trail skills`: corrections become takeaways on a skill,
|
|
993
|
+
// kept on this machine, and takeaways fold into its SKILL.md. Unlisted under
|
|
994
|
+
// graft, like `note`, but they run.
|
|
995
|
+
// Still runs; unlisted while learnings are the one thing trail keeps.
|
|
996
|
+
grouped(program.command("learn", { hidden: true }), GROUP.memory)
|
|
997
|
+
.description("Turn a correction into a takeaway on a skill, kept in ~/.trail (the takeaway on stdin)")
|
|
998
|
+
.argument("<skill>", "the skill it belongs to, e.g. pdf-coords; a new name makes a new skill")
|
|
999
|
+
.argument("[dir]", "repository root", ".")
|
|
1000
|
+
.option("-m, --text <text>", "the takeaway itself, instead of stdin")
|
|
1001
|
+
.option("--description <text>", "for a new skill: what it covers")
|
|
1002
|
+
.option("--section <heading>", "the SKILL.md heading it belongs under when folded")
|
|
1003
|
+
.option("--author <name>", "who taught it (default: the first word of git config user.name)")
|
|
1004
|
+
.action(async (skill, dir, opts) => {
|
|
1005
|
+
const repo = resolve(dir);
|
|
1006
|
+
const text = (opts.text ?? (await readStdin())).trim();
|
|
1007
|
+
if (!text) {
|
|
1008
|
+
console.error(`✗ a takeaway needs its text, on stdin or with -m:`);
|
|
1009
|
+
console.error(` ${cmd("graft learn")} ${skill} <<'EOF'\n Read rotation with page.Rotation(), never pdfcpu: it drops /Rotate on linearized files.\n EOF`);
|
|
1010
|
+
process.exitCode = 1;
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
const author = opts.author?.trim() || noteAuthor(repo);
|
|
1014
|
+
let r;
|
|
1015
|
+
try {
|
|
1016
|
+
r = learn(repo, { skill, text, author, date: new Date().toISOString().slice(0, 10), description: opts.description, section: opts.section });
|
|
1017
|
+
}
|
|
1018
|
+
catch (err) {
|
|
1019
|
+
console.error(`✗ ${err instanceof Error ? err.message : String(err)}`);
|
|
1020
|
+
process.exitCode = 1;
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
track("takeaway_saved", { new_skill: String(r.created) }, { repo });
|
|
1024
|
+
const kind = r.created ? "new skill" : r.skill.where === "repo" ? "the repo's skill" : "skill";
|
|
1025
|
+
console.log(`✓ takeaway saved to ${kind} ${r.skill.name}${author ? ` · taught by ${author}` : ""}`);
|
|
1026
|
+
console.log(`· your agent follows it from now on · kept on this machine, in ${shownPath(dirname(dirname(r.takeaway.path)))}`);
|
|
1027
|
+
console.log(`· ${cmd("graft skills")} fold ${r.skill.name} writes it into SKILL.md`);
|
|
1028
|
+
});
|
|
1029
|
+
const skillsCmd = grouped(program.command("skills", { hidden: true }), GROUP.memory)
|
|
1030
|
+
.description("List this repo's skills, and the takeaways waiting to fold into each")
|
|
1031
|
+
.argument("[dir]", "repository root", ".")
|
|
1032
|
+
.option("--json", "output as JSON")
|
|
1033
|
+
.action((dir, opts) => {
|
|
1034
|
+
const repo = resolve(dir);
|
|
1035
|
+
const all = listSkills(repo);
|
|
1036
|
+
if (opts.json) {
|
|
1037
|
+
console.log(JSON.stringify(all, null, 2));
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
if (all.length === 0) {
|
|
1041
|
+
console.log(`no skills yet · ${cmd("graft learn")} <skill> saves the first takeaway and makes the skill`);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
const width = Math.max(...all.map((s) => s.name.length)) + 2;
|
|
1045
|
+
for (const s of all)
|
|
1046
|
+
console.log(`${s.name.padEnd(width)}${skillStatus(s)}`);
|
|
1047
|
+
});
|
|
1048
|
+
/** `✗ no skill called x` and the exit code, for a name that isn't one. */
|
|
1049
|
+
function noSuchSkill(name) {
|
|
1050
|
+
console.error(`✗ no skill called ${skillName(name)} · ${cmd("graft skills")} lists them`);
|
|
1051
|
+
process.exitCode = 1;
|
|
1052
|
+
}
|
|
1053
|
+
skillsCmd
|
|
1054
|
+
.command("show")
|
|
1055
|
+
.description("Print a skill with every takeaway not yet folded in: what an agent reads before working in its area")
|
|
1056
|
+
.argument("<skill>", "the skill")
|
|
1057
|
+
.option("-C, --dir <path>", "repository root", ".")
|
|
1058
|
+
.action((skill, opts) => {
|
|
1059
|
+
const s = getSkill(resolve(opts.dir), skill);
|
|
1060
|
+
if (!s)
|
|
1061
|
+
return noSuchSkill(skill);
|
|
1062
|
+
process.stdout.write(skillForAgent(s));
|
|
1063
|
+
});
|
|
1064
|
+
skillsCmd
|
|
1065
|
+
.command("fold")
|
|
1066
|
+
.description("Write a skill's waiting takeaways into its SKILL.md and bump its version")
|
|
1067
|
+
.argument("[skill]", "one skill; leave out to fold every skill with takeaways waiting")
|
|
1068
|
+
.option("--confirmed", "only the takeaways someone other than their teacher confirmed")
|
|
1069
|
+
.option("-C, --dir <path>", "repository root", ".")
|
|
1070
|
+
.action((skill, opts) => {
|
|
1071
|
+
const repo = resolve(opts.dir);
|
|
1072
|
+
if (skill && !getSkill(repo, skill))
|
|
1073
|
+
return noSuchSkill(skill);
|
|
1074
|
+
const names = skill ? [skill] : listSkills(repo).map((s) => s.name);
|
|
1075
|
+
let inRepo = false;
|
|
1076
|
+
let any = false;
|
|
1077
|
+
for (const n of names) {
|
|
1078
|
+
const r = fold(repo, n, { confirmed: opts.confirmed });
|
|
1079
|
+
if (!r)
|
|
1080
|
+
continue;
|
|
1081
|
+
if (r.folded.length === 0) {
|
|
1082
|
+
if (skill)
|
|
1083
|
+
console.log(`· ${r.skill} v${r.from} · nothing to fold${r.stillWaiting ? `, ${r.stillWaiting} waiting for a second person` : ""}`);
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
any = true;
|
|
1087
|
+
inRepo ||= r.where === "repo";
|
|
1088
|
+
track("skills_folded", { takeaways_bucket: countBucket(r.folded.length) }, { repo });
|
|
1089
|
+
console.log(`✓ ${r.skill} v${r.from} → v${r.to} · folded ${r.folded.length} takeaway${r.folded.length === 1 ? "" : "s"}${r.stillWaiting ? `, ${r.stillWaiting} still waiting` : ""}`);
|
|
1090
|
+
console.log(` M ${r.where === "repo" ? shown(repo, r.file) : shownPath(r.file)} ${r.sections.map((h) => `+ ${h}`).join(" ")}`);
|
|
1091
|
+
}
|
|
1092
|
+
if (!any) {
|
|
1093
|
+
if (!skill)
|
|
1094
|
+
console.log("· nothing to fold");
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (inRepo)
|
|
1098
|
+
console.log("· review with git diff, then commit");
|
|
1099
|
+
else
|
|
1100
|
+
console.log(`· ${cmd("graft skills")} publish <skill> moves a skill into the repo's .claude/skills/, to commit for your team`);
|
|
1101
|
+
});
|
|
1102
|
+
skillsCmd
|
|
1103
|
+
.command("publish")
|
|
1104
|
+
.description("Move a skill kept on this machine into the repo's .claude/skills/, ready to commit")
|
|
1105
|
+
.argument("<skill>", "the skill")
|
|
1106
|
+
.option("-C, --dir <path>", "repository root", ".")
|
|
1107
|
+
.action((skill, opts) => {
|
|
1108
|
+
const repo = resolve(opts.dir);
|
|
1109
|
+
const r = publish(repo, skill);
|
|
1110
|
+
if (!r.ok) {
|
|
1111
|
+
if (r.reason === "unknown")
|
|
1112
|
+
return noSuchSkill(skill);
|
|
1113
|
+
console.log(`· ${skillName(skill)} is already in the repo, in .claude/skills/${skillName(skill)}/`);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
console.log(`✓ ${skillName(skill)} → ${shown(repo, r.file)}`);
|
|
1117
|
+
console.log("· commit it to share it: Claude Code loads it for everyone who pulls. its takeaways stay on this machine");
|
|
1118
|
+
});
|
|
1119
|
+
skillsCmd
|
|
1120
|
+
.command("confirm")
|
|
1121
|
+
.description("Say a teammate's takeaway is right, so `fold --confirmed` takes it")
|
|
1122
|
+
.argument("<skill>", "the skill")
|
|
1123
|
+
.argument("[takeaway]", "one takeaway file (default: every one waiting that someone else taught)")
|
|
1124
|
+
.option("--author <name>", "who is confirming (default: the first word of git config user.name)")
|
|
1125
|
+
.option("-C, --dir <path>", "repository root", ".")
|
|
1126
|
+
.action((skill, takeaway, opts) => {
|
|
1127
|
+
const repo = resolve(opts.dir);
|
|
1128
|
+
if (!getSkill(repo, skill))
|
|
1129
|
+
return noSuchSkill(skill);
|
|
1130
|
+
const who = opts.author?.trim() || noteAuthor(repo);
|
|
1131
|
+
const done = confirm(repo, skill, who, takeaway);
|
|
1132
|
+
if (done.length === 0) {
|
|
1133
|
+
console.log(`· nothing to confirm on ${skillName(skill)} — a takeaway needs someone other than its teacher`);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
for (const t of done)
|
|
1137
|
+
console.log(`✓ confirmed ${t.taughtBy || "a"}'s takeaway from ${shortDate(t.date)} · ${shownPath(t.path)}`);
|
|
1138
|
+
console.log(`· ${cmd("graft skills")} fold ${skillName(skill)} writes it into SKILL.md`);
|
|
1139
|
+
});
|
|
1140
|
+
grouped(program.command("init"), GROUP.setup)
|
|
1141
|
+
.description(TRAIL
|
|
1142
|
+
? "Wire trail into your agents: instruction files and MCP for each, plus hooks and a statusline for Claude Code"
|
|
1143
|
+
: "Wire Graft into the AI coding agents used with this repo (instruction files + MCP server; full hooks + statusline + MCP for Claude Code)")
|
|
813
1144
|
.argument("[dir]", "target repo directory", ".")
|
|
814
1145
|
.option("--no-build", "skip building the graph (wire files only)")
|
|
815
1146
|
.option("--agents <ids...>", `only these agents (${hostIds().join(", ")}, claude)`)
|
|
@@ -822,45 +1153,124 @@ program
|
|
|
822
1153
|
.option("--dry-run", "print every file init would touch, then exit without writing")
|
|
823
1154
|
.option("-y, --yes", "skip the picker and wire every detected agent (the pre-0.8 default)")
|
|
824
1155
|
.option("--no-global", "skip writes outside this repo (the ~/.codex/ config + hooks)")
|
|
825
|
-
.option("--trail <handoff>", "attach a Trail: <brainId>:<token> (or a bare id with GRAFT_BRAIN_TOKEN set)")
|
|
826
1156
|
.option("--verbose", "print every file written, the graph build's own output and the closing banner")
|
|
827
1157
|
.action(async (dir, opts) => {
|
|
828
1158
|
await runInitCommand(dir, opts);
|
|
829
1159
|
});
|
|
830
|
-
/**
|
|
831
|
-
function
|
|
832
|
-
if (
|
|
833
|
-
return
|
|
834
|
-
|
|
1160
|
+
/** Everything on stdin, or "" when nothing is piped in. */
|
|
1161
|
+
async function readStdin() {
|
|
1162
|
+
if (process.stdin.isTTY)
|
|
1163
|
+
return "";
|
|
1164
|
+
const chunks = [];
|
|
1165
|
+
for await (const c of process.stdin)
|
|
1166
|
+
chunks.push(typeof c === "string" ? Buffer.from(c) : c);
|
|
1167
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1168
|
+
}
|
|
1169
|
+
async function runNoteCommand(repo, opts) {
|
|
1170
|
+
const body = (opts.body ?? (await readStdin())).trim();
|
|
1171
|
+
if (!opts.title.trim() || !body) {
|
|
1172
|
+
console.error(`✗ a learning needs a --title and a body, on stdin or with -m:`);
|
|
1173
|
+
console.error(` ${cmd("graft note")} --title "What it was about" <<'EOF'`);
|
|
1174
|
+
console.error(" ## Decided\n …\n ## Tried and ruled out\n …\n ## Watch out\n …\n EOF");
|
|
1175
|
+
process.exitCode = 1;
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
const wholeNumber = (raw, flag) => {
|
|
1179
|
+
if (raw === undefined)
|
|
1180
|
+
return undefined;
|
|
1181
|
+
const n = Number(raw);
|
|
1182
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
1183
|
+
console.error(`✗ ${flag} takes a number, got "${raw}"`);
|
|
1184
|
+
process.exit(1);
|
|
1185
|
+
}
|
|
1186
|
+
return Math.round(n);
|
|
1187
|
+
};
|
|
1188
|
+
const minutes = wholeNumber(opts.minutes, "--minutes");
|
|
1189
|
+
const tokens = wholeNumber(opts.tokens, "--tokens");
|
|
1190
|
+
// What the session spent, from the agent's own transcript when there is one;
|
|
1191
|
+
// the flags win, since only the agent knows where its task began.
|
|
1192
|
+
const spent = currentSessionCost(repo);
|
|
1193
|
+
const cost = minutes !== undefined || tokens !== undefined || spent
|
|
1194
|
+
? { minutes: minutes ?? spent?.minutes, tokens: tokens ?? spent?.tokens }
|
|
1195
|
+
: undefined;
|
|
1196
|
+
// Which repos it goes to: the one it was saved from, plus any other the
|
|
1197
|
+
// session edited. Explicit --touches means just this repo.
|
|
1198
|
+
const targets = opts.touches
|
|
1199
|
+
? [{ dir: repo, touches: opts.touches.split(",").map((t) => t.trim()).filter(Boolean) }]
|
|
1200
|
+
: noteTargets(repo, spent?.edited ?? [], spent?.startedAt).map((t) => ({
|
|
1201
|
+
...t,
|
|
1202
|
+
// And the files the note names, for a session that read code without editing it.
|
|
1203
|
+
touches: [...new Set([...t.touches, ...mentionedFiles(t.dir, body)])].slice(0, 10),
|
|
1204
|
+
}));
|
|
1205
|
+
const author = opts.author?.trim() || noteAuthor(repo);
|
|
1206
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
1207
|
+
const scope = opts.personal ? "personal" : "repo";
|
|
1208
|
+
const earlier = [];
|
|
1209
|
+
const saved = [];
|
|
1210
|
+
for (const t of targets) {
|
|
1211
|
+
earlier.push(...listNotes(t.dir));
|
|
1212
|
+
saved.push(writeNote(t.dir, { title: opts.title, body, author, date, branch: currentBranch(t.dir), cost, touches: t.touches }, scope));
|
|
1213
|
+
}
|
|
1214
|
+
const touches = targets.flatMap((t) => t.touches);
|
|
1215
|
+
track("note_saved", { has_cost: String(cost?.tokens !== undefined || cost?.minutes !== undefined), touches_bucket: countBucket(touches.length), scope: saved[0]?.scope ?? scope }, { repo });
|
|
1216
|
+
if (opts.json) {
|
|
1217
|
+
console.log(JSON.stringify(saved.length === 1 ? saved[0] : saved, null, 2));
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
for (const n of saved)
|
|
1221
|
+
console.log(`✓ ${n.scope === "personal" ? "saved just for you" : "saved for your team"} · ${notePlace(n)}`);
|
|
1222
|
+
const note = saved[0];
|
|
1223
|
+
const took = costLabel(note.cost);
|
|
1224
|
+
if (took) {
|
|
1225
|
+
// Name a code file if the note touches one; docs are rarely what it's about.
|
|
1226
|
+
const about = touches.find((t) => !/\.(md|mdx|txt)$/i.test(t.split("#")[0])) ?? touches[0];
|
|
1227
|
+
const first = about ? ` that touches ${basename(about.split("#")[0])}` : "";
|
|
1228
|
+
const reading = tokensLabel(readingTokens(note)).replace(" tokens", "");
|
|
1229
|
+
console.log(`· this ${took.replace(" to work out", " to figure out")}. the next session${first} gets it for ${reading}`);
|
|
1230
|
+
}
|
|
1231
|
+
// Learnings about the same files, from someone else: this one builds on theirs.
|
|
1232
|
+
const files = new Set(touches.map((t) => t.split("#")[0]));
|
|
1233
|
+
const prior = earlier.find((n) => n.author !== note.author && n.touches.some((t) => files.has(t.split("#")[0])));
|
|
1234
|
+
if (prior)
|
|
1235
|
+
console.log(`· builds on ${prior.author ? `${prior.author}'s` : "a"} learning from ${shortDate(prior.date)}`);
|
|
1236
|
+
if (!SECTIONS.some((s) => sectionLead(body, s.heading) !== null))
|
|
1237
|
+
console.log("· tip: learnings read best under ## Decided, ## Tried and ruled out and ## Watch out");
|
|
1238
|
+
const repoSaved = saved.filter((n) => n.scope === "repo");
|
|
1239
|
+
if (repoSaved.length) {
|
|
1240
|
+
// Who else works on these files, for the case for committing it.
|
|
1241
|
+
const lines = commitLines(note, teammatesOn(repo, [...files].filter((f) => !!f)));
|
|
1242
|
+
console.log(`${lines.uncommitted} · git add ${repoSaved.map(notePlace).join(" ")}`);
|
|
1243
|
+
if (!process.stdout.isTTY && TRAIL)
|
|
1244
|
+
console.log(`[trail] Close your reply with one line, word for word. If this learning is committed by the end of your turn: ${lines.committed}\n` +
|
|
1245
|
+
` If it isn't: ${lines.uncommitted}`);
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
console.log("· just for you: kept on this machine, out of the repo");
|
|
1249
|
+
if (!process.stdout.isTTY && TRAIL)
|
|
1250
|
+
console.log(`[trail] Close your reply with this line, word for word: ${PERSONAL_SAVED_LINE}`);
|
|
1251
|
+
}
|
|
1252
|
+
for (const t of targets) {
|
|
1253
|
+
rememberRepo(t.dir);
|
|
1254
|
+
maybeSyncInBackground(t.dir);
|
|
1255
|
+
}
|
|
1256
|
+
printLines(cloudLines(repo, { command: "note" }));
|
|
1257
|
+
}
|
|
1258
|
+
/** Lines that close a command's output, with a blank line before them. */
|
|
1259
|
+
function printLines(lines) {
|
|
1260
|
+
if (lines.length)
|
|
1261
|
+
console.log(`\n${lines.join("\n")}`);
|
|
835
1262
|
}
|
|
836
1263
|
/** Repo-relative with forward slashes, for printing. */
|
|
837
1264
|
function shown(repo, path) {
|
|
838
1265
|
return relative(repo, path).split("\\").join("/");
|
|
839
1266
|
}
|
|
840
|
-
/**
|
|
841
|
-
|
|
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 = {}) {
|
|
1267
|
+
/** `graft init`. */
|
|
1268
|
+
async function runInitCommand(dir, opts) {
|
|
847
1269
|
if (opts.listAgents) {
|
|
848
1270
|
for (const id of [...hostIds(), "claude"])
|
|
849
1271
|
console.log(id);
|
|
850
1272
|
return null;
|
|
851
1273
|
}
|
|
852
|
-
// Parsed before anything is written: a mistyped handoff should cost the user
|
|
853
|
-
// an error, not a half-wired repo they have to `graft uninstall` out of.
|
|
854
|
-
let brainLink;
|
|
855
|
-
if (opts.trail) {
|
|
856
|
-
const parsed = parseBrainArg(opts.trail);
|
|
857
|
-
if ("error" in parsed) {
|
|
858
|
-
console.error(`✗ --trail: ${parsed.error}`);
|
|
859
|
-
process.exitCode = 1;
|
|
860
|
-
return null;
|
|
861
|
-
}
|
|
862
|
-
brainLink = parsed;
|
|
863
|
-
}
|
|
864
1274
|
const repo = resolve(dir);
|
|
865
1275
|
const explicit = Array.isArray(opts.agents) ? opts.agents : undefined;
|
|
866
1276
|
if (explicit) {
|
|
@@ -897,8 +1307,6 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
897
1307
|
const reason = offReason();
|
|
898
1308
|
const picked = await runPicker(plan, repo, home, {
|
|
899
1309
|
offerTelemetry: reason === null || reason === "disabled",
|
|
900
|
-
// From push, the line above the picker already asks the question.
|
|
901
|
-
...(how.push ? { title: null } : {}),
|
|
902
1310
|
});
|
|
903
1311
|
if (picked === null) {
|
|
904
1312
|
console.error("· cancelled — nothing written");
|
|
@@ -938,11 +1346,28 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
938
1346
|
}
|
|
939
1347
|
const wantClaude = ids.includes("claude");
|
|
940
1348
|
const cliPath = fileURLToPath(import.meta.url);
|
|
1349
|
+
// Under trail, with Claude Code picked: what the person kept telling Claude
|
|
1350
|
+
// in this repo's past sessions, read while the rest of init runs and saved
|
|
1351
|
+
// as learnings at the end. Once per repo, and never asked about first.
|
|
1352
|
+
const pastRead = TRAIL && wantClaude ? beginPastRead(repo) : null;
|
|
1353
|
+
if (pastRead)
|
|
1354
|
+
console.error(`· reading your ${pastRead.sessions} past Claude sessions here for what you keep telling Claude`);
|
|
941
1355
|
if (children.length)
|
|
942
1356
|
console.error(`· workspace: wiring ${repo} and ${children.length} child repo(s) — ${children.join(", ")}`);
|
|
943
1357
|
// Today's output, file by file, behind --verbose. Everything else gets one
|
|
944
1358
|
// line per step: the graph, then one per agent.
|
|
945
1359
|
const verbose = opts.verbose === true;
|
|
1360
|
+
// Under trail, each repo gets its `.trail/learnings/`, committed with the
|
|
1361
|
+
// code, and its folder in ~/.trail for personal learnings and corrections.
|
|
1362
|
+
const notesHome = new Map();
|
|
1363
|
+
if (TRAIL)
|
|
1364
|
+
for (const target of targets) {
|
|
1365
|
+
const { place } = ensureRepoHome(target);
|
|
1366
|
+
const created = place.git ? ensureRepoTrail(place.checkout) : false;
|
|
1367
|
+
notesHome.set(target, { created, dir: place.git ? place.checkout : place.dir });
|
|
1368
|
+
if (place.git)
|
|
1369
|
+
maybeRefreshBranches(place.checkout);
|
|
1370
|
+
}
|
|
946
1371
|
const reports = [];
|
|
947
1372
|
for (const target of targets) {
|
|
948
1373
|
if (verbose && target !== repo)
|
|
@@ -952,7 +1377,6 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
952
1377
|
// The graph, built once after the wiring: a workspace parent's build is
|
|
953
1378
|
// the workspace build, so it covers every child.
|
|
954
1379
|
let graphLine = null;
|
|
955
|
-
let graphNodes = 0;
|
|
956
1380
|
if (!verbose) {
|
|
957
1381
|
const res = await buildGraphWithProgress(repo, { build: opts.build, cliPath });
|
|
958
1382
|
for (const m of res.messages)
|
|
@@ -960,24 +1384,22 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
960
1384
|
const existing = res.built ? null : loadGraphCached(contextDirFor(repo, children.length ? undefined : program.opts().dir));
|
|
961
1385
|
const fmt = (x) => x.toLocaleString("en-US");
|
|
962
1386
|
if (res.graph) {
|
|
963
|
-
graphNodes = res.graph.nodes;
|
|
964
1387
|
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
1388
|
}
|
|
966
1389
|
else if (res.built) {
|
|
967
1390
|
graphLine = "✓ graph built";
|
|
968
1391
|
}
|
|
969
1392
|
else if (res.failed) {
|
|
970
|
-
graphLine =
|
|
1393
|
+
graphLine = `⚠ the graph build failed — run ${cmd("graft build")} to see why`;
|
|
971
1394
|
}
|
|
972
1395
|
else if (existing) {
|
|
973
|
-
graphNodes = existing.meta.nodeCount;
|
|
974
1396
|
graphLine = `✓ graph ready · ${fmt(existing.meta.nodeCount)} nodes, ${fmt(existing.meta.edgeCount)} edges`;
|
|
975
1397
|
}
|
|
976
1398
|
else if (hasGraftIndex(repo)) {
|
|
977
1399
|
graphLine = "✓ graph ready";
|
|
978
1400
|
}
|
|
979
1401
|
else {
|
|
980
|
-
graphLine =
|
|
1402
|
+
graphLine = `· skipped the graph build — run ${cmd("graft build")}`;
|
|
981
1403
|
}
|
|
982
1404
|
}
|
|
983
1405
|
if (!verbose) {
|
|
@@ -988,50 +1410,46 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
988
1410
|
for (const w of reports.flatMap((r) => r.warnings))
|
|
989
1411
|
console.error(w);
|
|
990
1412
|
const writesFor = (id) => (plan.find((p) => p.id === id)?.writes ?? []).filter((w) => opts.global !== false || w.scope !== "global");
|
|
991
|
-
if (
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
-
}
|
|
1413
|
+
if (graphLine)
|
|
1414
|
+
console.error(graphLine);
|
|
1415
|
+
const width = Math.max(...ids.map((id) => id.length));
|
|
1416
|
+
for (const id of ids) {
|
|
1417
|
+
const summary = compactWrites(writesFor(id), repo, home);
|
|
1418
|
+
console.error(`✓ ${id.padEnd(width)} ${summary}`.trimEnd());
|
|
1005
1419
|
}
|
|
1006
1420
|
}
|
|
1007
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
|
|
1010
|
-
if (
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1421
|
+
// Under trail: what init learned from past sessions, where this repo's
|
|
1422
|
+
// learnings are, and how many there already are.
|
|
1423
|
+
let learnedNow = false;
|
|
1424
|
+
if (TRAIL && pastRead) {
|
|
1425
|
+
if (!pastRead.done)
|
|
1426
|
+
console.error("· still reading your past sessions…");
|
|
1427
|
+
const { result, saved } = await finishPastRead(repo, pastRead);
|
|
1428
|
+
learnedNow = saved.length > 0;
|
|
1429
|
+
if (saved.length) {
|
|
1430
|
+
const n = saved.length;
|
|
1431
|
+
console.error(`✓ learned ${n} thing${n === 1 ? "" : "s"} you told Claude more than once, from your last ${result.read} sessions here`);
|
|
1432
|
+
for (const note of saved)
|
|
1433
|
+
console.error(` ${note.scope === "personal" ? "just for you " : "for your team"} · ${note.title}`);
|
|
1434
|
+
console.error("· Claude follows them from your next session");
|
|
1435
|
+
if (saved.some((s) => s.scope === "repo"))
|
|
1436
|
+
console.error(commitLines({}, teammatesOn(repo), "repo").uncommitted);
|
|
1021
1437
|
}
|
|
1022
|
-
else if (
|
|
1023
|
-
console.error(
|
|
1438
|
+
else if (result.reason === "failed") {
|
|
1439
|
+
console.error(`· couldn't read your past sessions this time; ${cmd("graft init")} again tries again`);
|
|
1024
1440
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1441
|
+
}
|
|
1442
|
+
if (TRAIL && !learnedNow) {
|
|
1443
|
+
const home = notesHome.get(repo);
|
|
1444
|
+
if (home) {
|
|
1445
|
+
const learnings = listNotes(repo, { branches: false }).filter((n) => n.scope === "repo");
|
|
1446
|
+
if (learnings.length === 0)
|
|
1447
|
+
console.error(`✓ learnings ${LEARNINGS_DIR}/ · sessions that take real digging leave one there, for your whole team`);
|
|
1448
|
+
else
|
|
1449
|
+
console.error(`✓ learnings ${learnings.length} in ${LEARNINGS_DIR}/, from ${new Set(learnings.map((n) => n.author || "someone")).size} ${new Set(learnings.map((n) => n.author)).size === 1 ? "person" : "people"}`);
|
|
1032
1450
|
}
|
|
1033
1451
|
}
|
|
1034
|
-
if (!verbose
|
|
1452
|
+
if (!verbose) {
|
|
1035
1453
|
// What there is to commit: the top-level entries the picked agents wrote.
|
|
1036
1454
|
const tops = [];
|
|
1037
1455
|
for (const w of selectedWrites(plan, ids)) {
|
|
@@ -1042,9 +1460,18 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
1042
1460
|
if (!tops.includes(top))
|
|
1043
1461
|
tops.push(top);
|
|
1044
1462
|
}
|
|
1045
|
-
console.error(
|
|
1463
|
+
console.error(`· restart your agents so a new session picks up ${brand()}`);
|
|
1464
|
+
if (TRAIL && !tops.includes(".trail/"))
|
|
1465
|
+
tops.push(".trail/");
|
|
1466
|
+
// Only what git sees as new or changed: a teammate's init over committed files has nothing to commit.
|
|
1467
|
+
const changedTops = tops.filter((t) => {
|
|
1468
|
+
const r = spawnSync("git", ["status", "--porcelain", "--", t], { cwd: repo, encoding: "utf8" });
|
|
1469
|
+
return r.status !== 0 || r.stdout.trim().length > 0;
|
|
1470
|
+
});
|
|
1471
|
+
tops.splice(0, tops.length, ...changedTops);
|
|
1046
1472
|
if (tops.length)
|
|
1047
|
-
console.error(`· commit ${tops.slice(0,
|
|
1473
|
+
console.error(`· commit ${tops.slice(0, 5).join(" ")}${tops.length > 5 ? ` +${tops.length - 5} more` : ""} to share it — ` +
|
|
1474
|
+
(TRAIL ? "teammates' Claude reads the learnings even without trail installed; the code map in graft/ stays on this machine" : "graft/ stays local and git-ignored"));
|
|
1048
1475
|
}
|
|
1049
1476
|
// One epilogue for the whole run. A workspace parent holds no nodes of its
|
|
1050
1477
|
// own, so the totals come from the children — the graph the user actually got.
|
|
@@ -1052,7 +1479,7 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
1052
1479
|
const graphs = (children.length ? children.map((c) => join(repo, c)) : [repo])
|
|
1053
1480
|
.map((d) => loadGraphCached(contextDirFor(d, children.length ? undefined : globalDir)))
|
|
1054
1481
|
.filter((g) => g !== null);
|
|
1055
|
-
if (verbose
|
|
1482
|
+
if (verbose)
|
|
1056
1483
|
console.error("\n" +
|
|
1057
1484
|
formatInitEpilogue({
|
|
1058
1485
|
graphBuilt: graphs.length > 0,
|
|
@@ -1091,7 +1518,7 @@ function wireTarget(repo, ids, ctx) {
|
|
|
1091
1518
|
// `global`/`home` are threaded through alongside `statusline`: the claude layer
|
|
1092
1519
|
// writes under `~/.claude` now (hosts/claude-global.ts), so --no-global has to
|
|
1093
1520
|
// reach it or the flag would silently mean "no out-of-repo writes, except three".
|
|
1094
|
-
const res = runInit(repo, { build: opts.build, cliPath, statusline: wantStatusline, global: opts.global, home });
|
|
1521
|
+
const res = runInit(repo, { build: opts.build, cliPath, statusline: wantStatusline, global: opts.global, home, brand: brand() });
|
|
1095
1522
|
say(`✓ wrote ${res.settingsPath}`);
|
|
1096
1523
|
for (const s of res.shims)
|
|
1097
1524
|
say(`✓ wrote ${s}`);
|
|
@@ -1102,7 +1529,7 @@ function wireTarget(repo, ids, ctx) {
|
|
|
1102
1529
|
say(`· mcp claude: ${res.mcp.path} (already registered)`);
|
|
1103
1530
|
else
|
|
1104
1531
|
say(`✓ mcp claude: ${res.mcp.path} (${res.mcp.action}) — restart Claude Code to load the graft MCP server`);
|
|
1105
|
-
say(res.built ?
|
|
1532
|
+
say(res.built ? `✓ built the graph (${cmd("graft build")})` : "· skipped graph build");
|
|
1106
1533
|
if (!wantStatusline)
|
|
1107
1534
|
say("· skipped Claude Code statusLine (--no-statusline)");
|
|
1108
1535
|
for (const w of res.warnings)
|
|
@@ -1118,6 +1545,7 @@ function wireTarget(repo, ids, ctx) {
|
|
|
1118
1545
|
mcp: opts.mcp,
|
|
1119
1546
|
hooks: opts.hooks,
|
|
1120
1547
|
global: opts.global,
|
|
1548
|
+
brand: brand(),
|
|
1121
1549
|
});
|
|
1122
1550
|
for (const w of r.written)
|
|
1123
1551
|
say(`✓ ${w.id}: ${w.path} (${w.action})`);
|
|
@@ -1145,7 +1573,7 @@ function wireTarget(repo, ids, ctx) {
|
|
|
1145
1573
|
if (!wantClaude) {
|
|
1146
1574
|
if (!quiet)
|
|
1147
1575
|
say(buildGraphIfMissing(repo, { build: opts.build, cliPath })
|
|
1148
|
-
?
|
|
1576
|
+
? `✓ built the graph (${cmd("graft build")})`
|
|
1149
1577
|
: "· skipped graph build");
|
|
1150
1578
|
}
|
|
1151
1579
|
return report;
|
|
@@ -1182,9 +1610,8 @@ function formatRetractions(rs, apply) {
|
|
|
1182
1610
|
}
|
|
1183
1611
|
return lines.join("\n").replace(/^\n/, "");
|
|
1184
1612
|
}
|
|
1185
|
-
program
|
|
1186
|
-
.
|
|
1187
|
-
.description("Remove every file and config entry graft has written to this repo (the inverse of init)")
|
|
1613
|
+
grouped(program.command("uninstall"), GROUP.setup)
|
|
1614
|
+
.description(`Remove every file and config entry ${TRAIL ? "trail or graft" : "graft"} has written to this repo (the inverse of init)`)
|
|
1188
1615
|
.argument("[dir]", "target repo directory", ".")
|
|
1189
1616
|
.option("-y, --yes", "actually remove (without this, prints what it would remove and exits)")
|
|
1190
1617
|
.option("--keep-cache", "keep graft/ and the .gitignore entries — wiring only")
|
|
@@ -1205,470 +1632,485 @@ program
|
|
|
1205
1632
|
const bad = changed(done).filter((r) => r.action === "skipped-unparseable");
|
|
1206
1633
|
console.error(bad.length
|
|
1207
1634
|
? `\n⚠ ${bad.length} file(s) could not be parsed and were left as-is — see above.`
|
|
1208
|
-
:
|
|
1635
|
+
: `\n✓ ${brand()} fully removed. \`${cmd("graft init")}\` re-wires from scratch.`);
|
|
1209
1636
|
});
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
.description("The Trail attached to this repo: the rules mined from its own history");
|
|
1637
|
+
/* -------------------------------------------------------------------------- */
|
|
1638
|
+
/* trail login · logout · workspace */
|
|
1639
|
+
/* -------------------------------------------------------------------------- */
|
|
1214
1640
|
/**
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1219
|
-
* every failure prints its own reason first, because the caller only needs to
|
|
1220
|
-
* know whether to carry on. Nothing about the repository has been read or sent
|
|
1221
|
-
* by then, and every message says so.
|
|
1641
|
+
* `trail login`: connect this machine to a Trail workspace by hand. Normally
|
|
1642
|
+
* nobody runs it: Claude offers the same link once there's a learning to
|
|
1643
|
+
* share. Prints the link, opens it, and waits for the person to sign in and
|
|
1644
|
+
* pick repos, then uploads those repos' learnings.
|
|
1222
1645
|
*/
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1646
|
+
function loginCommand(name = "login") {
|
|
1647
|
+
return new Command(name)
|
|
1648
|
+
.description("Connect this machine to a Trail workspace: sign in, pick the repos it gets, invite your team")
|
|
1649
|
+
.argument("[dir]", "the repo you're in, ticked first in the picker", ".")
|
|
1650
|
+
.option("--server <url>", "a self-hosted Trail, e.g. https://trail.acme.internal (default: Trail cloud)")
|
|
1651
|
+
.action(async (dir, opts) => {
|
|
1652
|
+
const repo = resolve(dir);
|
|
1653
|
+
const server = opts.server?.trim().replace(/\/+$/, "");
|
|
1654
|
+
if (server) {
|
|
1655
|
+
if (!/^https?:\/\//.test(server)) {
|
|
1656
|
+
console.error(`✗ --server takes a URL, like https://trail.acme.internal`);
|
|
1657
|
+
process.exitCode = 1;
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
process.env.TRAIL_URL = server;
|
|
1661
|
+
}
|
|
1662
|
+
const cloud = readCloud();
|
|
1663
|
+
if (cloud) {
|
|
1664
|
+
console.error(`✓ already connected to workspace ${cloud.workspace.name} · ${cloud.workspace.url}`);
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
rememberRepo(repo);
|
|
1668
|
+
const url = loginLink(repo);
|
|
1669
|
+
console.error(`· sign in and pick the repos your workspace gets:\n ${url}`);
|
|
1670
|
+
if (process.stdout.isTTY)
|
|
1671
|
+
openBrowser(url);
|
|
1672
|
+
console.error("· waiting for you to finish in the browser (ctrl-c stops; Claude picks it up later too)");
|
|
1673
|
+
const got = await waitForConnect({ waitMs: 10 * 60 * 1000 });
|
|
1674
|
+
if (got && "cloud" in got)
|
|
1675
|
+
printLines(cloudLines(repo, { command: "note", agent: false }));
|
|
1676
|
+
else if (got && "error" in got) {
|
|
1677
|
+
console.error(`✗ ${got.error} · run ${cmd("graft login")} again for a new link`);
|
|
1678
|
+
process.exitCode = 1;
|
|
1679
|
+
}
|
|
1680
|
+
else
|
|
1681
|
+
console.error(`· not connected yet · the link stays good for a day`);
|
|
1249
1682
|
});
|
|
1250
|
-
const got = await Promise.race([handoff.wait(), stopped]);
|
|
1251
|
-
if (onSigint)
|
|
1252
|
-
process.off("SIGINT", onSigint);
|
|
1253
|
-
spinner.stop();
|
|
1254
|
-
if (got === "stopped") {
|
|
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;
|
|
1259
|
-
return null;
|
|
1260
|
-
}
|
|
1261
|
-
if ("error" in got) {
|
|
1262
|
-
console.error(`✗ ${got.error}`);
|
|
1263
|
-
// The category, never the sentence: `got.error` names the repo and the link.
|
|
1264
|
-
track("brain_signup_settled", { outcome: got.reason, mode: "terminal", duration_bucket: durationBucket(Date.now() - startedAt) }, { repo });
|
|
1265
|
-
return null;
|
|
1266
|
-
}
|
|
1267
|
-
writeLink(repo, got.link);
|
|
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 });
|
|
1270
|
-
return got.link;
|
|
1271
1683
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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;
|
|
1684
|
+
function logoutCommand(name = "logout") {
|
|
1685
|
+
return new Command(name)
|
|
1686
|
+
.description("Disconnect this machine from its Trail workspace (learnings stay in your repos)")
|
|
1687
|
+
.action(() => {
|
|
1688
|
+
const cloud = readCloud();
|
|
1689
|
+
clearPending();
|
|
1690
|
+
if (!clearCloud()) {
|
|
1691
|
+
console.error("· not connected to a workspace");
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
console.error(`✓ disconnected from workspace ${cloud?.workspace.name ?? ""} · learnings stay in your repos' .trail/`.replace(" ", " "));
|
|
1695
|
+
});
|
|
1324
1696
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
.
|
|
1329
|
-
.argument("[dir]", "target repo directory", ".")
|
|
1330
|
-
.action(async (handoff, dir) => {
|
|
1331
|
-
const parsed = parseBrainArg(handoff);
|
|
1332
|
-
if ("error" in parsed) {
|
|
1333
|
-
console.error(`✗ ${parsed.error}`);
|
|
1334
|
-
process.exitCode = 1;
|
|
1335
|
-
return;
|
|
1336
|
-
}
|
|
1337
|
-
const repo = resolve(dir);
|
|
1338
|
-
const res = await connectBrain(repo, parsed, { home: homedir() });
|
|
1339
|
-
if (res.warning) {
|
|
1340
|
-
console.error(`⚠ ${res.warning}`);
|
|
1341
|
-
return;
|
|
1342
|
-
}
|
|
1343
|
-
if (res.ruleCount === 0) {
|
|
1344
|
-
// The normal case on the local route: the brain exists but nothing has
|
|
1345
|
-
// read the repo yet. Saying "0 rules" without saying why reads as a
|
|
1346
|
-
// failure, and the next step is the whole point.
|
|
1347
|
-
console.error("✓ attached this repo to the brain — it has no rules yet");
|
|
1348
|
-
console.error("· run `graft trail push` to read this repository into it");
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
console.error(`✓ pulled ${res.ruleCount} rule(s) from ${parsed.brainId}`);
|
|
1352
|
-
for (const w of res.writes)
|
|
1353
|
-
console.error(`✓ ${w.path} (${w.action})`);
|
|
1354
|
-
});
|
|
1355
|
-
brain
|
|
1356
|
-
.command("pull")
|
|
1357
|
-
.description("Refresh the trail's rules and write every change you accepted in Trail into this repo's context files")
|
|
1358
|
-
.argument("[dir]", "target repo directory", ".")
|
|
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) {
|
|
1365
|
-
const repo = resolve(dir);
|
|
1366
|
-
const link = readLink(repo);
|
|
1367
|
-
if (!link) {
|
|
1368
|
-
console.error("✗ this repo has no trail yet — run graft trail push first");
|
|
1369
|
-
process.exitCode = 1;
|
|
1370
|
-
return;
|
|
1371
|
-
}
|
|
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;
|
|
1697
|
+
/** The repo at `repo`, as the workspace names it. */
|
|
1698
|
+
function workspaceRepo(repo) {
|
|
1699
|
+
const place = repoPlace(repo);
|
|
1700
|
+
return { key: place.key, name: repoName(place.key, place.checkout), checkout: place.checkout, git: place.git };
|
|
1377
1701
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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);
|
|
1702
|
+
function workspaceCommand(name = "workspace") {
|
|
1703
|
+
const ws = new Command(name)
|
|
1704
|
+
.description("Your Trail workspace: its repos, the team page and the invite link")
|
|
1705
|
+
.argument("[dir]", "repository root", ".")
|
|
1706
|
+
.option("--json", "output as JSON")
|
|
1707
|
+
.action(async (dir, opts) => {
|
|
1708
|
+
const repo = resolve(dir);
|
|
1709
|
+
const cloud = readCloud();
|
|
1710
|
+
if (!cloud) {
|
|
1711
|
+
console.log("· not connected to a workspace");
|
|
1712
|
+
console.log(`· Claude offers a link once this repo has learnings, or run ${cmd("graft login")}`);
|
|
1713
|
+
return;
|
|
1398
1714
|
}
|
|
1715
|
+
const me = await fetchMe(cloud);
|
|
1716
|
+
if (isError(me)) {
|
|
1717
|
+
console.error(`✗ ${me.error}`);
|
|
1718
|
+
process.exitCode = 1;
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
// What the workspace has now, for every command that reads cloud.json.
|
|
1722
|
+
writeCloud({ ...cloud, workspace: me.workspace, repos: me.repos.map((r) => r.key) });
|
|
1723
|
+
if (opts.json) {
|
|
1724
|
+
console.log(JSON.stringify(me, null, 2));
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
console.log(`${me.workspace.name} · ${me.workspace.url}`);
|
|
1728
|
+
console.log(`invite ${me.workspace.invite_url}`);
|
|
1729
|
+
console.log("");
|
|
1730
|
+
const here = workspaceRepo(repo);
|
|
1731
|
+
const width = Math.max(...me.repos.map((r) => r.name.length), here.name.length) + 3;
|
|
1732
|
+
for (const r of me.repos) {
|
|
1733
|
+
const by = r.added_by ? ` · added by ${r.added_by}` : "";
|
|
1734
|
+
console.log(` ✓ ${r.name.padEnd(width)}${r.learnings} learning${r.learnings === 1 ? "" : "s"}${by}`);
|
|
1735
|
+
}
|
|
1736
|
+
if (here.git && !me.repos.some((r) => r.key === here.key)) {
|
|
1737
|
+
console.log(` · ${here.name.padEnd(width)}not in the workspace · ${cmd("graft workspace add")} shares its learnings`);
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
ws.command("add")
|
|
1741
|
+
.description("Add this repo to your workspace and share its learnings")
|
|
1742
|
+
.argument("[dir]", "repository root", ".")
|
|
1743
|
+
.action(async (dir) => {
|
|
1744
|
+
const repo = resolve(dir);
|
|
1745
|
+
const cloud = readCloud();
|
|
1746
|
+
if (!cloud) {
|
|
1747
|
+
console.error(`✗ not connected to a workspace · run ${cmd("graft login")}`);
|
|
1748
|
+
process.exitCode = 1;
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
const here = workspaceRepo(repo);
|
|
1752
|
+
if (!here.git) {
|
|
1753
|
+
console.error("✗ not a git repository");
|
|
1754
|
+
process.exitCode = 1;
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
const added = await addRepo(cloud, { key: here.key, name: here.name });
|
|
1758
|
+
if (isError(added)) {
|
|
1759
|
+
console.error(`✗ ${added.error}`);
|
|
1760
|
+
process.exitCode = 1;
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
const next = { ...cloud, repos: added.repos.map((r) => r.key) };
|
|
1764
|
+
writeCloud(next);
|
|
1765
|
+
rememberRepo(repo);
|
|
1766
|
+
const res = await syncRepo(here.checkout, next);
|
|
1767
|
+
if (isError(res)) {
|
|
1768
|
+
console.error(`✓ added ${here.name} to ${cloud.workspace.name} · its learnings go up in the background (${res.error})`);
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
console.error(`✓ added ${here.name} to ${cloud.workspace.name} · shared ${res.learnings} learning${res.learnings === 1 ? "" : "s"}`);
|
|
1772
|
+
console.error(`· ${cloud.workspace.url}`);
|
|
1773
|
+
});
|
|
1774
|
+
return ws;
|
|
1775
|
+
}
|
|
1776
|
+
/* -------------------------------------------------------------------------- */
|
|
1777
|
+
/* trail team · trail check */
|
|
1778
|
+
/* -------------------------------------------------------------------------- */
|
|
1779
|
+
/** Fold a long line at `width`, indenting what wraps. */
|
|
1780
|
+
function wrap(text, width, indent) {
|
|
1781
|
+
const out = [];
|
|
1782
|
+
let line = "";
|
|
1783
|
+
for (const word of text.split(/\s+/)) {
|
|
1784
|
+
if (line && line.length + 1 + word.length > width) {
|
|
1785
|
+
out.push(line);
|
|
1786
|
+
line = indent + word;
|
|
1787
|
+
}
|
|
1788
|
+
else
|
|
1789
|
+
line = line ? `${line} ${word}` : word;
|
|
1399
1790
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
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}`;
|
|
1791
|
+
if (line)
|
|
1792
|
+
out.push(line);
|
|
1793
|
+
return out;
|
|
1414
1794
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
let browserOpen = false;
|
|
1435
|
-
if (!link) {
|
|
1436
|
-
if (!here) {
|
|
1437
|
-
console.error("✗ this directory has no GitHub origin remote — graft can only push a GitHub repository today");
|
|
1795
|
+
function teamCommand(name = "team") {
|
|
1796
|
+
return new Command(name)
|
|
1797
|
+
.description("What the team learned this week, who's working on what, and where work overlaps")
|
|
1798
|
+
.argument("[dir]", "repository root", ".")
|
|
1799
|
+
.option("--days <n>", "how far back to look", "7")
|
|
1800
|
+
.option("--json", "output as JSON")
|
|
1801
|
+
.action(async (dir, opts) => {
|
|
1802
|
+
const repo = resolve(dir);
|
|
1803
|
+
const days = Math.max(1, Math.min(90, Number(opts.days) || 7));
|
|
1804
|
+
const cloud = readCloud();
|
|
1805
|
+
if (!cloud) {
|
|
1806
|
+
localTeam(repo, days, opts.json === true);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
maybeSyncInBackground(repo);
|
|
1810
|
+
const here = workspaceRepo(repo);
|
|
1811
|
+
const t = await fetchTeam(cloud, { repo: cloud.repos.includes(here.key) ? here.key : undefined, days });
|
|
1812
|
+
if (isError(t)) {
|
|
1813
|
+
console.error(`✗ ${t.error}`);
|
|
1438
1814
|
process.exitCode = 1;
|
|
1439
1815
|
return;
|
|
1440
1816
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
if (!process.exitCode)
|
|
1444
|
-
process.exitCode = 1;
|
|
1817
|
+
if (opts.json) {
|
|
1818
|
+
console.log(JSON.stringify(t, null, 2));
|
|
1445
1819
|
return;
|
|
1446
1820
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
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");
|
|
1464
|
-
process.exitCode = 1;
|
|
1465
|
-
return;
|
|
1466
|
-
}
|
|
1467
|
-
const firstPush = !expected || expected.ruleCount === 0;
|
|
1468
|
-
// The instruction files and the newest pull requests go first, when the
|
|
1469
|
-
// trail takes them: its suggestions start from those while the rest of the
|
|
1470
|
-
// history is still being read here. In the background, from here on — the
|
|
1471
|
-
// picker, the graph build and the full read all happen while it goes, and
|
|
1472
|
-
// the full read reuses every GitHub request it made.
|
|
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 });
|
|
1821
|
+
const period = days === 7 ? "this week" : `the last ${days} days`;
|
|
1822
|
+
console.log(`${period} in ${t.repo || t.workspace}`);
|
|
1823
|
+
console.log(`${t.people} ${t.people === 1 ? "person" : "people"} · ${t.sessions} session${t.sessions === 1 ? "" : "s"} · ` +
|
|
1824
|
+
`${t.new_learnings} new learning${t.new_learnings === 1 ? "" : "s"} · reused ${t.reused}×`);
|
|
1825
|
+
if (t.members.length) {
|
|
1826
|
+
console.log("");
|
|
1827
|
+
const nameW = Math.max(...t.members.map((m) => m.name.length)) + 3;
|
|
1828
|
+
const topicW = Math.min(36, Math.max(...t.members.map((m) => m.topics.join(", ").length)) + 3);
|
|
1829
|
+
for (const m of t.members) {
|
|
1830
|
+
const learnings = m.learnings ? `${m.learnings} learning${m.learnings === 1 ? "" : "s"}` : "";
|
|
1831
|
+
const reused = m.reused ? ` reused ${m.reused}×` : "";
|
|
1832
|
+
console.log(` ${m.name.padEnd(nameW)}${m.topics.join(", ").padEnd(topicW)}${learnings.padEnd(12)}${reused}`.trimEnd());
|
|
1833
|
+
}
|
|
1491
1834
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1835
|
+
const overlaps = [...t.overlaps.map((o) => o.text), ...branchOverlapTexts(repo)];
|
|
1836
|
+
if (overlaps.length) {
|
|
1837
|
+
console.log("\noverlap");
|
|
1838
|
+
for (const o of overlaps)
|
|
1839
|
+
for (const [i, l] of wrap(o, 66, " ").entries())
|
|
1840
|
+
console.log(i === 0 ? ` ● ${l}` : l);
|
|
1841
|
+
}
|
|
1842
|
+
if (t.url)
|
|
1843
|
+
console.log(`\n team page: ${t.url}`);
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
/** Pairs of teammates' pushed branches that change the same files, from this clone's branch cache. */
|
|
1847
|
+
function branchOverlapTexts(repo, now = Date.now()) {
|
|
1848
|
+
const branches = readBranchCache(repo)?.branches ?? [];
|
|
1849
|
+
const out = [];
|
|
1850
|
+
for (let i = 0; i < branches.length; i++) {
|
|
1851
|
+
for (let j = i + 1; j < branches.length; j++) {
|
|
1852
|
+
const a = branches[i];
|
|
1853
|
+
const b = branches[j];
|
|
1854
|
+
if (!a.author || !b.author || a.author === b.author)
|
|
1855
|
+
continue;
|
|
1856
|
+
const shared = a.files.filter((f) => !f.startsWith(".trail/") && b.files.includes(f));
|
|
1857
|
+
if (!shared.length)
|
|
1858
|
+
continue;
|
|
1859
|
+
const files = shared.length <= 2 ? shared.join(" and ") : `${shared.slice(0, 2).join(", ")} and ${shared.length - 2} more`;
|
|
1860
|
+
out.push(`${a.author}'s ${a.ref.replace(/^origin\//, "")} (pushed ${pushedWhen(a.at, now)}) and ${b.author}'s ${b.ref.replace(/^origin\//, "")} (pushed ${pushedWhen(b.at, now)}) both change ${files}`);
|
|
1861
|
+
if (out.length >= 5)
|
|
1862
|
+
return out;
|
|
1494
1863
|
}
|
|
1495
1864
|
}
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1865
|
+
return out;
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* `trail team` before this machine is connected: what this clone can see on
|
|
1869
|
+
* its own. Who wrote which learnings lately, and where teammates' pushed
|
|
1870
|
+
* branches overlap. Reuse and the team page need the workspace.
|
|
1871
|
+
*/
|
|
1872
|
+
function localTeam(repo, days, json) {
|
|
1873
|
+
maybeRefreshBranches(repo);
|
|
1874
|
+
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
1875
|
+
const recent = listNotes(repo).filter((n) => n.scope === "repo" && n.date >= since);
|
|
1876
|
+
const byAuthor = new Map();
|
|
1877
|
+
for (const n of recent)
|
|
1878
|
+
byAuthor.set(n.author || "someone", [...(byAuthor.get(n.author || "someone") ?? []), n]);
|
|
1879
|
+
const overlaps = branchOverlapTexts(repo);
|
|
1880
|
+
if (json) {
|
|
1881
|
+
console.log(JSON.stringify({ days, learnings: recent.length, members: [...byAuthor].map(([name, ns]) => ({ name, learnings: ns.length, titles: ns.map((n) => n.title) })), overlaps }, null, 2));
|
|
1508
1882
|
return;
|
|
1509
1883
|
}
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
return;
|
|
1884
|
+
const here = workspaceRepo(repo);
|
|
1885
|
+
console.log(`${days === 7 ? "this week" : `the last ${days} days`} in ${here.name} · from this clone`);
|
|
1886
|
+
console.log(`${byAuthor.size} ${byAuthor.size === 1 ? "person" : "people"} · ${recent.length} new learning${recent.length === 1 ? "" : "s"}`);
|
|
1887
|
+
if (byAuthor.size) {
|
|
1888
|
+
console.log("");
|
|
1889
|
+
const nameW = Math.max(...[...byAuthor.keys()].map((k) => k.length)) + 3;
|
|
1890
|
+
for (const [name, ns] of byAuthor) {
|
|
1891
|
+
const latest = ns[0].title.length > 40 ? `${ns[0].title.slice(0, 39)}…` : ns[0].title;
|
|
1892
|
+
console.log(` ${name.padEnd(nameW)}${latest.padEnd(42)}${ns.length} learning${ns.length === 1 ? "" : "s"}`);
|
|
1893
|
+
}
|
|
1521
1894
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
const buildPage = brainUrl(link.brainId);
|
|
1528
|
-
// Held rather than handed back. Everything above this line succeeded even
|
|
1529
|
-
// in the runs that end badly: the push lands, and then the miner fails —
|
|
1530
|
-
// which is where roughly a third of production's repo trails die. Returning
|
|
1531
|
-
// the prompt here is what made that invisible from this side, on CI and
|
|
1532
|
-
// over SSH permanently so. `--no-watch` is for a caller that genuinely
|
|
1533
|
-
// wants fire-and-forget.
|
|
1534
|
-
if (opts.watch === false) {
|
|
1535
|
-
console.error("· building now — the rules reach this repo on their own; watch it at");
|
|
1536
|
-
console.error(` ${buildPage}`);
|
|
1537
|
-
return;
|
|
1895
|
+
if (overlaps.length) {
|
|
1896
|
+
console.log("\noverlap");
|
|
1897
|
+
for (const o of overlaps)
|
|
1898
|
+
for (const [i, l] of wrap(o, 66, " ").entries())
|
|
1899
|
+
console.log(i === 0 ? ` ● ${l}` : l);
|
|
1538
1900
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
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
|
-
},
|
|
1901
|
+
console.log("\n· reuse and the team page, across your repos, are in a Trail workspace");
|
|
1902
|
+
printLines(cloudLines(repo, { command: "check" }));
|
|
1903
|
+
}
|
|
1904
|
+
function teamCheckCommand(name = "check") {
|
|
1905
|
+
return new Command(name)
|
|
1906
|
+
.description("Check your branch's diff against everything the team has learned: the repo's learnings, yours and your skills")
|
|
1907
|
+
.argument("[dir]", "repository root", ".")
|
|
1908
|
+
.option("--base <ref>", "compare against where the branch left this ref (default: origin's default branch)")
|
|
1909
|
+
.option("--json", "output as JSON")
|
|
1910
|
+
.action(async (dir, opts) => {
|
|
1911
|
+
await runTeamCheck(resolve(dir), opts);
|
|
1561
1912
|
});
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
if (outcome === "completed") {
|
|
1581
|
-
console.error(`✓ trail built · ${rules}, reaching this repo on their own`);
|
|
1582
|
-
closing();
|
|
1583
|
-
return;
|
|
1584
|
-
}
|
|
1585
|
-
// The ordinary ending for anything but a small repository. The history is
|
|
1586
|
-
// mined in slices, and the prompt comes back on the first of them: mining is
|
|
1587
|
-
// the part that fails and it has now succeeded, and the rest is filing,
|
|
1588
|
-
// which is minutes of reliable work nobody gains by watching.
|
|
1589
|
-
if (outcome === "building") {
|
|
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();
|
|
1913
|
+
}
|
|
1914
|
+
/** A unified diff without the sections for `.trail/` files: a learning is never the change being checked. */
|
|
1915
|
+
function withoutTrailFiles(diff) {
|
|
1916
|
+
return diff
|
|
1917
|
+
.split(/(?=^diff --git )/m)
|
|
1918
|
+
.filter((part) => !/^diff --git a\/\.trail\//.test(part))
|
|
1919
|
+
.join("");
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* `trail check`, on this machine: the branch's diff against the learnings in
|
|
1923
|
+
* the repo's `.trail/` (teammates' pushed branches included), this person's
|
|
1924
|
+
* own, and the repo's skills. No sign-in, no server. Exits 1 on a conflict.
|
|
1925
|
+
*/
|
|
1926
|
+
async function runTeamCheck(repo, opts) {
|
|
1927
|
+
const d = branchDiff(repo, opts.base);
|
|
1928
|
+
if (!d) {
|
|
1929
|
+
console.error("✗ not a git repository");
|
|
1930
|
+
process.exitCode = 1;
|
|
1594
1931
|
return;
|
|
1595
1932
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
// should hear about it the way it hears about any other failure. The
|
|
1600
|
-
// watcher has already printed where it stopped and why.
|
|
1601
|
-
process.exitCode = 1;
|
|
1933
|
+
const files = d.files.filter((f) => !f.startsWith(".trail/"));
|
|
1934
|
+
if (files.length === 0) {
|
|
1935
|
+
console.log(`· nothing to check: no changes against ${d.base}`);
|
|
1602
1936
|
return;
|
|
1603
1937
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1938
|
+
maybeRefreshBranches(repo);
|
|
1939
|
+
if (d.truncated)
|
|
1940
|
+
console.error("⚠ the diff is over 400 KB; only the first 400 KB is checked");
|
|
1941
|
+
const me = noteAuthor(repo);
|
|
1942
|
+
const r = checkLocally({ diff: withoutTrailFiles(d.diff), files, notes: listNotes(repo), skills: listSkills(repo), asker: me });
|
|
1943
|
+
const sources = r.findings.map((f) => f.source).filter((s) => s.path);
|
|
1944
|
+
recordActivity({
|
|
1945
|
+
repo: repoPlace(repo).key,
|
|
1946
|
+
kind: "check",
|
|
1947
|
+
asker: me,
|
|
1948
|
+
session: sessionKey(repo) ?? `${me}-${new Date().toISOString().slice(0, 10)}`,
|
|
1949
|
+
query: "",
|
|
1950
|
+
files,
|
|
1951
|
+
shown_paths: [...new Set(sources.map((s) => s.path.slice(s.path.lastIndexOf("/") + 1)))],
|
|
1952
|
+
shown_authors: [...new Set(sources.map((s) => s.author ?? ""))],
|
|
1953
|
+
at: new Date().toISOString(),
|
|
1954
|
+
});
|
|
1955
|
+
const overlaps = overlapsWith(repo, files, me).slice(0, 3);
|
|
1956
|
+
if (opts.json) {
|
|
1957
|
+
console.log(JSON.stringify({ ...r, overlaps }, null, 2));
|
|
1606
1958
|
}
|
|
1607
1959
|
else {
|
|
1608
|
-
|
|
1960
|
+
printCheck(r);
|
|
1961
|
+
if (overlaps.length)
|
|
1962
|
+
console.log(`\n${overlaps.map((o) => formatOverlap(o)).join("\n")}`);
|
|
1963
|
+
printLines(cloudLines(repo, { command: "check" }));
|
|
1609
1964
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
.
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
});
|
|
1626
|
-
|
|
1627
|
-
.command("status")
|
|
1628
|
-
.description("Show the attached brain, how many rules are cached, and how many still match the code")
|
|
1629
|
-
.argument("[dir]", "target repo directory", ".")
|
|
1630
|
-
.option("--json", "machine-readable output")
|
|
1631
|
-
.action((dir, opts) => {
|
|
1632
|
-
const repo = resolve(dir);
|
|
1633
|
-
const { link, rules, fetchedAt } = brainStatus(repo);
|
|
1634
|
-
// Resolve every cached rule against the CURRENT graph, so the count of stale
|
|
1635
|
-
// rules is the real one rather than what was true at pull time. This is the
|
|
1636
|
-
// number worth surfacing: it says how far the codebase has drifted from what
|
|
1637
|
-
// the team decided.
|
|
1638
|
-
const graph = loadGraphCached(contextDirFor(repo, program.opts().dir));
|
|
1639
|
-
const applied = rulesForPointers((graph?.nodes ?? []).map((n) => `${n.path}:${n.span}`), rules, graph);
|
|
1640
|
-
if (opts.json) {
|
|
1641
|
-
console.log(JSON.stringify({ brainId: link?.brainId ?? null, cached: rules.length, fetchedAt, anchored: applied.length, stale: applied.filter((a) => a.stale).length }, null, 2));
|
|
1642
|
-
return;
|
|
1643
|
-
}
|
|
1644
|
-
if (!link) {
|
|
1645
|
-
console.error("· no brain attached — run `graft trail connect <brainId>:<token>`");
|
|
1646
|
-
return;
|
|
1965
|
+
maybeSyncInBackground(repo);
|
|
1966
|
+
if (r.findings.some((f) => f.verdict === "conflict"))
|
|
1967
|
+
process.exitCode = 1;
|
|
1968
|
+
}
|
|
1969
|
+
function printCheck(r) {
|
|
1970
|
+
console.log(`checking ${r.files} changed file${r.files === 1 ? "" : "s"} against ${r.notes} learning${r.notes === 1 ? "" : "s"} and ${r.skills} skill${r.skills === 1 ? "" : "s"}\n`);
|
|
1971
|
+
const conflicts = r.findings.filter((f) => f.verdict === "conflict");
|
|
1972
|
+
for (const f of [...conflicts, ...r.findings.filter((x) => x.verdict !== "conflict")]) {
|
|
1973
|
+
const mark = f.verdict === "conflict" ? "✗" : "✓";
|
|
1974
|
+
console.log(`${mark} ${f.file}${f.line ? `:${f.line}` : ""}`);
|
|
1975
|
+
console.log(` ${f.summary}`);
|
|
1976
|
+
const s = f.source;
|
|
1977
|
+
if (f.verdict === "conflict" && s.quote) {
|
|
1978
|
+
const who = [s.author, s.date ? shortDate(s.date) : ""].filter(Boolean).join(" · ");
|
|
1979
|
+
for (const l of wrap(`${who ? `${who}: ` : ""}${s.quote}`, 70, " "))
|
|
1980
|
+
console.log(` ${l.trimStart()}`);
|
|
1981
|
+
}
|
|
1647
1982
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1983
|
+
if (r.findings.length === 0)
|
|
1984
|
+
console.log("✓ nothing the team has learned bears on this change");
|
|
1985
|
+
console.log("");
|
|
1986
|
+
console.log(conflicts.length
|
|
1987
|
+
? `· ${conflicts.length} conflict${conflicts.length === 1 ? "" : "s"} · fix ${conflicts.length === 1 ? "it" : "them"}, or say why in the commit message or PR description`
|
|
1988
|
+
: "· no conflicts");
|
|
1989
|
+
}
|
|
1990
|
+
/** Tokens saved by this repo's agent sessions touched in the last 7 days. */
|
|
1991
|
+
function savedThisWeek(repo, now = Date.now()) {
|
|
1992
|
+
const since = now - 7 * 24 * 60 * 60 * 1000;
|
|
1993
|
+
let total = 0;
|
|
1994
|
+
for (const id of listSessionIds(repo)) {
|
|
1995
|
+
try {
|
|
1996
|
+
if (statSync(join(sessionDir(repo), `${id}.json`)).mtimeMs < since)
|
|
1997
|
+
continue;
|
|
1998
|
+
}
|
|
1999
|
+
catch {
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
total += readSession(repo, id).savedTokens ?? 0;
|
|
1653
2003
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
2004
|
+
return total;
|
|
2005
|
+
}
|
|
2006
|
+
/** 212345 → `212k`, 1_250_000 → `1.3M`. */
|
|
2007
|
+
function shortCount(n) {
|
|
2008
|
+
if (n >= 1_000_000)
|
|
2009
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
2010
|
+
if (n >= 1_000)
|
|
2011
|
+
return `${Math.round(n / 1_000)}k`;
|
|
2012
|
+
return String(n);
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* `trail status`: what was `graft stats`, on one
|
|
2016
|
+
* screen. The code map and whether it matches the code, the learnings for
|
|
2017
|
+
* this repo, the workspace, and what the agents saved this week.
|
|
2018
|
+
*/
|
|
2019
|
+
function statusCommand(name = "status") {
|
|
2020
|
+
return new Command(name)
|
|
2021
|
+
.description("Code map, learnings and tokens saved, on one screen")
|
|
2022
|
+
.argument(...DIR_ARG)
|
|
2023
|
+
.option("--json", "machine-readable output")
|
|
2024
|
+
.action(async (dirArg, opts) => {
|
|
2025
|
+
const repo = queryRoot(dirArg);
|
|
2026
|
+
const graph = loadGraphCached(contextDirFor(repo, program.opts().dir));
|
|
2027
|
+
const g = graph ? await engineFrom().checkGraph(repo) : null;
|
|
2028
|
+
const fresh = g && !g.missing ? g.ok : null;
|
|
2029
|
+
const kept = keepsNotes(repo);
|
|
2030
|
+
const all = kept ? listNotes(repo) : [];
|
|
2031
|
+
const inRepo = all.filter((n) => n.scope === "repo" && !n.ref).length;
|
|
2032
|
+
const onBranches = all.filter((n) => n.ref).length;
|
|
2033
|
+
const personal = all.filter((n) => n.scope === "personal").length;
|
|
2034
|
+
const cloud = readCloud();
|
|
2035
|
+
const shared = cloud ? inWorkspace(repo, cloud) : false;
|
|
2036
|
+
const saved = savedThisWeek(repo);
|
|
2037
|
+
if (opts.json) {
|
|
2038
|
+
console.log(JSON.stringify({
|
|
2039
|
+
codeMap: graph ? { nodes: graph.meta.nodeCount, inSync: fresh } : null,
|
|
2040
|
+
learnings: kept ? { repo: inRepo, branches: onBranches, personal } : null,
|
|
2041
|
+
workspace: cloud ? { name: cloud.workspace.name, url: cloud.workspace.url, repos: cloud.repos.length, thisRepo: shared } : null,
|
|
2042
|
+
week: { savedTokens: saved },
|
|
2043
|
+
}, null, 2));
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
const fmt = (x) => x.toLocaleString("en-US");
|
|
2047
|
+
const row = (k, v) => console.log(`${k.padEnd(11)} ${v}`);
|
|
2048
|
+
if (!graph)
|
|
2049
|
+
row("code map", `✗ not built · run ${cmd("graft build")}`);
|
|
2050
|
+
else if (fresh === false)
|
|
2051
|
+
row("code map", `⚠ ${fmt(graph.meta.nodeCount)} nodes · behind the code, refreshed on the next query`);
|
|
2052
|
+
else
|
|
2053
|
+
row("code map", `✓ ${fmt(graph.meta.nodeCount)} nodes · in sync with the code`);
|
|
2054
|
+
if (kept) {
|
|
2055
|
+
const parts = [`${inRepo} for the team in ${LEARNINGS_DIR}/`];
|
|
2056
|
+
if (onBranches)
|
|
2057
|
+
parts.push(`${onBranches} on teammates' branches`);
|
|
2058
|
+
if (personal)
|
|
2059
|
+
parts.push(`${personal} just for you, on this machine`);
|
|
2060
|
+
row("learnings", parts.join(" · "));
|
|
2061
|
+
}
|
|
2062
|
+
if (!cloud) {
|
|
2063
|
+
if (cloudEnabled())
|
|
2064
|
+
row("workspace", "not connected · Claude offers a link once there are learnings to share");
|
|
2065
|
+
}
|
|
2066
|
+
else
|
|
2067
|
+
row("workspace", `${cloud.workspace.name} · ${cloud.repos.length} repo${cloud.repos.length === 1 ? "" : "s"}${shared ? ", this one included" : `, not this one (${cmd("graft workspace add")})`} · ${cloud.workspace.url}`);
|
|
2068
|
+
row("this week", saved > 0 ? `~${shortCount(saved)} tokens saved` : "nothing saved yet");
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
if (TRAIL) {
|
|
2072
|
+
for (const c of [teamCheckCommand(), teamCommand()])
|
|
2073
|
+
program.addCommand(c.helpGroup(GROUP.memory));
|
|
2074
|
+
// Signing in waits until it gives a team more than committing .trail/ does.
|
|
2075
|
+
for (const c of [workspaceCommand(), loginCommand(), logoutCommand()])
|
|
2076
|
+
program.addCommand(c.helpGroup(GROUP.cloud), { hidden: !cloudEnabled() });
|
|
2077
|
+
program.addCommand(statusCommand().helpGroup(GROUP.setup));
|
|
2078
|
+
// Help lists the groups in this order, and the commands within each.
|
|
2079
|
+
const order = ["ask", "grep", "skeleton", "callers", "map", "note", "check", "team", "learn", "skills", "init", "build", "status", "upgrade", "uninstall", "telemetry", "workspace", "login", "logout", "blast"];
|
|
2080
|
+
const rank = (c) => {
|
|
2081
|
+
const i = order.indexOf(c.name());
|
|
2082
|
+
return i === -1 ? order.length : i;
|
|
2083
|
+
};
|
|
2084
|
+
program.commands.sort((a, b) => rank(a) - rank(b));
|
|
2085
|
+
// One line each in the list; `trail <command> --help` keeps the long text.
|
|
2086
|
+
const summaries = {
|
|
2087
|
+
ask: "ranked answer, with the code inlined at each file:line",
|
|
2088
|
+
grep: "every occurrence, grouped by enclosing symbol",
|
|
2089
|
+
skeleton: "a file's whole API in ~200 tokens",
|
|
2090
|
+
callers: "who calls it, or what it calls with --direction out",
|
|
2091
|
+
map: "directory clusters, hubs and hotspots",
|
|
2092
|
+
note: "save what this session worked out, as a learning in .trail/",
|
|
2093
|
+
learn: "turn a correction into a takeaway on a skill",
|
|
2094
|
+
skills: "list skills, show one, fold takeaways in, publish to the repo",
|
|
2095
|
+
init: "wire trail into your agents",
|
|
2096
|
+
build: "rebuild the code map · --check fails if it's stale",
|
|
2097
|
+
status: "code map, learnings and tokens saved",
|
|
2098
|
+
upgrade: "install the latest trail",
|
|
2099
|
+
uninstall: "remove everything trail or graft wrote to this repo",
|
|
2100
|
+
telemetry: "show or turn off the anonymous usage stats",
|
|
2101
|
+
check: "check your diff against everything the team has learned",
|
|
2102
|
+
team: "what the team learned this week, and where work overlaps",
|
|
2103
|
+
workspace: "your workspace's repos, team page and invite link",
|
|
2104
|
+
login: "connect a Trail workspace (Claude offers you the link)",
|
|
2105
|
+
logout: "disconnect this machine from its workspace",
|
|
2106
|
+
blast: "what depends on the lines this diff touched",
|
|
2107
|
+
};
|
|
2108
|
+
for (const c of program.commands)
|
|
2109
|
+
if (summaries[c.name()])
|
|
2110
|
+
c.summary(summaries[c.name()]);
|
|
2111
|
+
program.addHelpText("after", "\ntrail <command> --help for flags · every graft command still works");
|
|
2112
|
+
}
|
|
2113
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1672
2114
|
console.error(err instanceof Error ? err.message : err);
|
|
1673
2115
|
process.exit(1);
|
|
1674
2116
|
});
|