@nanonets/graft 0.21.1 → 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 +1039 -559
- 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 -48
- package/dist/brain/pull.d.ts.map +0 -1
- package/dist/brain/pull.js +0 -257
- 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 -397
- 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,16 +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, suggestionsLine } from "./brain/pull.js";
|
|
28
|
-
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";
|
|
29
29
|
import { contextDirFor } from "./context/node-file.js";
|
|
30
30
|
import { loadGraphCached } from "./graph/load.js";
|
|
31
31
|
import { ensureFreshChildren, ensureFreshGraph, refreshNote } from "./graph/refresh.js";
|
|
@@ -40,15 +40,51 @@ import { planRetract, runRetract, changed } from "./hosts/retract.js";
|
|
|
40
40
|
import { compactWrites, formatNonInteractiveHelp, formatPlan, runPicker } from "./cli-picker.js";
|
|
41
41
|
import { buildGraphWithProgress } from "./claude/build-progress.js";
|
|
42
42
|
import { homedir } from "node:os";
|
|
43
|
-
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";
|
|
44
45
|
import { patchBuildConfig } from "./util/state.js";
|
|
45
46
|
import { normalizePathPrefix } from "./util/paths.js";
|
|
46
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";
|
|
47
60
|
import { setInputRate } from "./context/savings.js";
|
|
48
|
-
import { formatUpdateNudge, maybeRefreshInBackground, readStamp, readUpdateCache, refreshUpdateCache, wiredHostIds, writeStamp } from "./upkeep.js";
|
|
49
|
-
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";
|
|
50
64
|
const program = new Command();
|
|
51
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
|
+
}
|
|
52
88
|
/**
|
|
53
89
|
* What the `query` telemetry event will say, filled in by the command as it runs
|
|
54
90
|
* and emitted once from the `postAction` hook below.
|
|
@@ -74,8 +110,11 @@ function noteHit(found) {
|
|
|
74
110
|
queryNote.hit = found ? "yes" : "no";
|
|
75
111
|
}
|
|
76
112
|
program
|
|
77
|
-
.name(
|
|
78
|
-
.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.")
|
|
79
118
|
.version(currentVersion, "-v, --version")
|
|
80
119
|
.option("--dir <path>", "context graph directory (default: <repo>/graft)")
|
|
81
120
|
.option("--provider <name>", "LLM wire format: openai | anthropic | litellm | orcarouter (env GRAFT_PROVIDER)")
|
|
@@ -124,7 +163,7 @@ function queryRoot(dir) {
|
|
|
124
163
|
return resolve(dir);
|
|
125
164
|
const { root, levels } = nearestGraftRoot(process.cwd(), program.opts().dir);
|
|
126
165
|
if (levels > 0)
|
|
127
|
-
console.error(
|
|
166
|
+
console.error(`${tag()} no graft/ here — answering from ${root}/graft`);
|
|
128
167
|
return root;
|
|
129
168
|
}
|
|
130
169
|
/**
|
|
@@ -173,7 +212,7 @@ function parseTabs(raw) {
|
|
|
173
212
|
* not editorialize on stderr at startup (`mcp` runs its own upkeep at boot, and
|
|
174
213
|
* `_update-check` IS the fetch).
|
|
175
214
|
*/
|
|
176
|
-
const UPKEEP_SKIP = new Set(["version", "upgrade", "_update-check", "
|
|
215
|
+
const UPKEEP_SKIP = new Set(["version", "upgrade", "_update-check", "_trail-past-read", "mcp"]);
|
|
177
216
|
/**
|
|
178
217
|
* Every other command: top up the cached registry answer in the background and,
|
|
179
218
|
* if a newer graft is out, say so once on stderr. This is what makes the CLI the
|
|
@@ -182,6 +221,16 @@ const UPKEEP_SKIP = new Set(["version", "upgrade", "_update-check", "_brain-refr
|
|
|
182
221
|
program.hook("preAction", (_parent, action) => {
|
|
183
222
|
if (UPKEEP_SKIP.has(action.name()))
|
|
184
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);
|
|
185
234
|
maybeRefreshInBackground();
|
|
186
235
|
const nudge = formatUpdateNudge(currentVersion, readUpdateCache()?.latest);
|
|
187
236
|
if (nudge)
|
|
@@ -202,26 +251,52 @@ program.hook("preAction", (_parent, action) => {
|
|
|
202
251
|
* is the right failure mode for a metric.
|
|
203
252
|
*/
|
|
204
253
|
program.hook("postAction", (_parent, action) => {
|
|
205
|
-
|
|
254
|
+
// `trail build --check` is graft's `check`, and counts as it always has.
|
|
255
|
+
const name = queryNote.command ?? action.name();
|
|
206
256
|
if (!isTrackedCommand(name))
|
|
207
257
|
return;
|
|
208
|
-
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);
|
|
209
279
|
});
|
|
210
|
-
// Hidden from --help: only ever spawned detached by maybeRefreshInBackground.
|
|
211
280
|
program
|
|
212
|
-
.command("
|
|
213
|
-
.description("internal:
|
|
281
|
+
.command("_trail-sync", { hidden: true })
|
|
282
|
+
.description("internal: send this repo's learnings and the waiting uses to the Trail workspace")
|
|
214
283
|
.argument("[dir]", "target repo directory", ".")
|
|
215
284
|
.action(async (dir) => {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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();
|
|
225
300
|
});
|
|
226
301
|
program
|
|
227
302
|
.command("_update-check", { hidden: true })
|
|
@@ -237,8 +312,7 @@ program
|
|
|
237
312
|
.action(async () => {
|
|
238
313
|
await runFlush();
|
|
239
314
|
});
|
|
240
|
-
program
|
|
241
|
-
.command("telemetry")
|
|
315
|
+
grouped(program.command("telemetry"), GROUP.setup)
|
|
242
316
|
.description("Show, inspect, or turn off the anonymous usage stats (see TELEMETRY.md)")
|
|
243
317
|
.argument("[action]", "status (default) | enable | disable | debug", "status")
|
|
244
318
|
.action((action) => {
|
|
@@ -248,7 +322,7 @@ program
|
|
|
248
322
|
return;
|
|
249
323
|
case "enable":
|
|
250
324
|
patchState({ enabled: true });
|
|
251
|
-
console.log(
|
|
325
|
+
console.log(`telemetry: on — anonymous, aggregate-only. \`${cmd("graft telemetry status")}\` for details.`);
|
|
252
326
|
return;
|
|
253
327
|
case "disable":
|
|
254
328
|
// Also stamp the notice as shown: someone who has just opted out should
|
|
@@ -264,27 +338,49 @@ program
|
|
|
264
338
|
process.exit(1);
|
|
265
339
|
}
|
|
266
340
|
});
|
|
341
|
+
// Hidden under trail, where -v says the same; listed under graft as it was.
|
|
267
342
|
program
|
|
268
|
-
.command("version")
|
|
343
|
+
.command("version", { hidden: TRAIL })
|
|
269
344
|
.description("Print the installed version and the latest published on npm")
|
|
270
345
|
.action(() => {
|
|
271
346
|
const latest = getNpmViewVersion();
|
|
272
347
|
console.log(formatVersionReport(currentVersion, latest));
|
|
273
348
|
});
|
|
274
|
-
program
|
|
275
|
-
.
|
|
276
|
-
.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`)
|
|
277
351
|
.action(() => {
|
|
278
352
|
const result = runUpgrade(import.meta.url);
|
|
279
353
|
console.log(formatUpgradeReport(result));
|
|
280
354
|
if (result.ran && !result.ok)
|
|
281
355
|
process.exit(1);
|
|
356
|
+
if (!TRAIL && result.ok && result.trailVersion)
|
|
357
|
+
moveHereToTrail(process.cwd());
|
|
282
358
|
});
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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.")
|
|
287
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")
|
|
288
384
|
.option("--deep", "run the LLM pass: concept nodes (graft/*.md) + per-symbol summary/crux")
|
|
289
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')
|
|
290
386
|
.option("-j, --concurrency <n>", "files summarized in parallel during --deep (default 5)")
|
|
@@ -304,6 +400,13 @@ program
|
|
|
304
400
|
.option("--no-gitignore", "skip writing graft/ into .gitignore (same as GRAFT_NO_GITIGNORE=1)")
|
|
305
401
|
.option("--no-ignore", "skip writing .ignore for ripgrep re-admit (same as GRAFT_NO_IGNORE=1)")
|
|
306
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
|
+
}
|
|
307
410
|
const buildStartedAt = Date.now();
|
|
308
411
|
if (opts.gitignore === false)
|
|
309
412
|
process.env.GRAFT_NO_GITIGNORE = "1";
|
|
@@ -377,7 +480,7 @@ program
|
|
|
377
480
|
deep = false;
|
|
378
481
|
console.error("⚠ no API key set — falling back to the structural build (no LLM summaries).\n" +
|
|
379
482
|
" Set GRAFT_API_KEY (and GRAFT_PROVIDER / GRAFT_BASE_URL / GRAFT_MODEL for your\n" +
|
|
380
|
-
|
|
483
|
+
` provider) and re-run \`${cmd("graft build --deep")}\` to add concept nodes and summaries.`);
|
|
381
484
|
}
|
|
382
485
|
if (deep && resolved.usedLegacyEnv) {
|
|
383
486
|
console.error("⚠ using OPENROUTER_API_KEY (deprecated) — prefer GRAFT_API_KEY + GRAFT_BASE_URL.");
|
|
@@ -395,6 +498,7 @@ program
|
|
|
395
498
|
includeDirs: opts.includeDir,
|
|
396
499
|
followSubmodules: followSubmodulesWasExplicit ? opts.followSubmodules : undefined,
|
|
397
500
|
followNestedRepos: followNestedReposWasExplicit ? opts.followNestedRepos : undefined,
|
|
501
|
+
packageName: readPackageName(import.meta.url),
|
|
398
502
|
});
|
|
399
503
|
return;
|
|
400
504
|
}
|
|
@@ -452,12 +556,17 @@ program
|
|
|
452
556
|
}, { repo: buildRoot });
|
|
453
557
|
for (const e of g.errors)
|
|
454
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);
|
|
455
564
|
const rel = relative(process.cwd(), g.contextDir) || "graft";
|
|
456
565
|
if (process.env.GRAFT_NO_GITIGNORE) {
|
|
457
566
|
console.log(` ${rel}/ is a local cache — add it to your gitignore if you want it untracked.`);
|
|
458
567
|
}
|
|
459
568
|
else {
|
|
460
|
-
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.`);
|
|
461
570
|
}
|
|
462
571
|
// #127: a --deep run whose LLM calls failed used to print the same success
|
|
463
572
|
// footer and exit 0, so a quota-exhausted build looked identical to a clean
|
|
@@ -484,15 +593,58 @@ program
|
|
|
484
593
|
if (conceptErrors.length > 0)
|
|
485
594
|
console.error(` ${conceptErrors.length} concept-pass error(s).`);
|
|
486
595
|
console.error(` meaning coverage: ${ready}/${total} symbols (${pct}%).`);
|
|
487
|
-
console.error(
|
|
596
|
+
console.error(` Nothing computed was lost: re-run \`${cmd("graft build --deep")}\` to resume from what is cached.\n` +
|
|
488
597
|
" Pass --allow-partial to accept a degraded meaning tier and exit 0.");
|
|
489
598
|
if (!opts.allowPartial)
|
|
490
599
|
process.exitCode = 1;
|
|
491
600
|
}
|
|
492
601
|
}
|
|
493
602
|
});
|
|
494
|
-
|
|
495
|
-
|
|
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)
|
|
496
648
|
.description("Query the graft/ graph — returns ranked nodes + exact file:line, routed to prose or wiring ($0, no key)")
|
|
497
649
|
.argument("<query>", "what you want to understand, in plain words")
|
|
498
650
|
.argument(...DIR_ARG)
|
|
@@ -524,16 +676,22 @@ program
|
|
|
524
676
|
return;
|
|
525
677
|
}
|
|
526
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));
|
|
527
685
|
if (opts.json) {
|
|
528
686
|
console.log(JSON.stringify(r, null, 2));
|
|
529
687
|
}
|
|
530
688
|
else {
|
|
531
689
|
const { formatAsk } = await import("./ask/ask.js");
|
|
532
690
|
process.stdout.write(formatAsk(r));
|
|
691
|
+
printLines(cloudLines(dir, { command: "ask" }));
|
|
533
692
|
}
|
|
534
693
|
});
|
|
535
|
-
program
|
|
536
|
-
.command("skeleton")
|
|
694
|
+
grouped(program.command("skeleton"), GROUP.find)
|
|
537
695
|
.description("Signatures-only view of one file from the wiring graph — the cheapest way to see a file's API surface")
|
|
538
696
|
.argument("<file>", "repo-relative path (or unique basename) of the file")
|
|
539
697
|
.argument(...DIR_ARG)
|
|
@@ -550,13 +708,24 @@ program
|
|
|
550
708
|
else
|
|
551
709
|
process.stdout.write(formatSkeleton(r));
|
|
552
710
|
});
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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) {
|
|
560
729
|
warnUnsupportedExtensions(opts.extensions);
|
|
561
730
|
const dir = noteQuery(queryRoot(dirArg));
|
|
562
731
|
const checkGlobalDir = program.opts().dir;
|
|
@@ -576,24 +745,25 @@ program
|
|
|
576
745
|
console.log(JSON.stringify({ context: r, graph: g.missing ? null : g }, null, 2));
|
|
577
746
|
}
|
|
578
747
|
else if (bothMissing) {
|
|
579
|
-
console.log(
|
|
748
|
+
console.log(`${label}: NO GRAPH\n\nNo graft/ graph found. Run \`${cmd("graft build")}\` first.`);
|
|
580
749
|
}
|
|
581
750
|
else {
|
|
582
751
|
if (r.missing) {
|
|
583
|
-
console.log(
|
|
752
|
+
console.log(`deep layer: not built (run \`${cmd("graft build --deep")}\` for concept nodes) — wiring graph is the source of truth`);
|
|
584
753
|
}
|
|
585
754
|
else {
|
|
586
|
-
console.log(formatCheckReport(r));
|
|
755
|
+
console.log(formatCheckReport(r, label));
|
|
587
756
|
}
|
|
588
757
|
if (!g.missing)
|
|
589
758
|
console.log("\n" + formatGraphCheckReport(g));
|
|
590
759
|
}
|
|
591
760
|
if (bothMissing || markdownFail || wiringFail)
|
|
592
761
|
process.exit(1);
|
|
593
|
-
}
|
|
762
|
+
}
|
|
763
|
+
// Under trail this is part of `status`; the old name still runs, unlisted.
|
|
594
764
|
program
|
|
595
|
-
.command("stats")
|
|
596
|
-
.description(
|
|
765
|
+
.command("stats", { hidden: TRAIL })
|
|
766
|
+
.description(`Show this agent session's ${brand()}-vs-source usage mix and tokens saved`)
|
|
597
767
|
.argument(...DIR_ARG)
|
|
598
768
|
.option("--json", "output the session stats as JSON")
|
|
599
769
|
.action((dirArg, opts) => {
|
|
@@ -609,7 +779,7 @@ program
|
|
|
609
779
|
console.log(formatSessionStats(s));
|
|
610
780
|
});
|
|
611
781
|
program
|
|
612
|
-
.command("viz")
|
|
782
|
+
.command("viz", { hidden: TRAIL })
|
|
613
783
|
.description("Serve an interactive visualization of the context graph (and graph.json when present)")
|
|
614
784
|
.argument(...DIR_ARG)
|
|
615
785
|
.option("-p, --port <port>", "port to serve on", "4400")
|
|
@@ -633,7 +803,7 @@ program
|
|
|
633
803
|
const globalOpts = program.opts();
|
|
634
804
|
const contextDir = contextDirFor(root, globalOpts.dir);
|
|
635
805
|
if (!existsSync(contextDir)) {
|
|
636
|
-
console.error(`✗ no context graph at ${contextDir} — run
|
|
806
|
+
console.error(`✗ no context graph at ${contextDir} — run \`${cmd("graft build --deep")}\` first`);
|
|
637
807
|
process.exit(1);
|
|
638
808
|
}
|
|
639
809
|
const viewerDir = fileURLToPath(new URL("./viewer/", import.meta.url)); // prebuilt
|
|
@@ -648,7 +818,7 @@ program
|
|
|
648
818
|
tabs,
|
|
649
819
|
});
|
|
650
820
|
const kb = Math.round(out.bytes / 1024);
|
|
651
|
-
console.log(
|
|
821
|
+
console.log(`${cmd("graft viz")} → ${out.file} (${kb} kB, ${out.contextNodes} concept nodes, ${out.codeNodes} code nodes)`);
|
|
652
822
|
return;
|
|
653
823
|
}
|
|
654
824
|
const srv = await startVizServer({
|
|
@@ -657,14 +827,14 @@ program
|
|
|
657
827
|
port: Number(opts.port),
|
|
658
828
|
repoName: basename(root),
|
|
659
829
|
});
|
|
660
|
-
console.log(
|
|
830
|
+
console.log(`${cmd("graft viz")} → ${srv.url} (ctrl-c to stop)`);
|
|
661
831
|
if (opts.open) {
|
|
662
832
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
663
833
|
spawn(opener, [srv.url], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
|
|
664
834
|
}
|
|
665
835
|
});
|
|
666
836
|
program
|
|
667
|
-
.command("mcp")
|
|
837
|
+
.command("mcp", { hidden: TRAIL })
|
|
668
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")
|
|
669
839
|
.argument(...DIR_ARG)
|
|
670
840
|
.action(async (dirArg) => {
|
|
@@ -673,8 +843,7 @@ program
|
|
|
673
843
|
const globalOpts = program.opts();
|
|
674
844
|
startMcpServer(dir, globalOpts.dir, currentVersion);
|
|
675
845
|
});
|
|
676
|
-
program
|
|
677
|
-
.command("callers")
|
|
846
|
+
grouped(program.command("callers"), GROUP.find)
|
|
678
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")
|
|
679
848
|
.argument("<symbol>", "bare name, qualified (Class.method), or package-qualified (pkg.Fn)")
|
|
680
849
|
.argument(...DIR_ARG)
|
|
@@ -706,8 +875,7 @@ program
|
|
|
706
875
|
globalDir: globalOpts.dir,
|
|
707
876
|
});
|
|
708
877
|
});
|
|
709
|
-
program
|
|
710
|
-
.command("blast")
|
|
878
|
+
grouped(program.command("blast"), GROUP.ci)
|
|
711
879
|
.description("Blast radius of a diff: what depends on the lines this change touched ($0, no LLM). " +
|
|
712
880
|
"Built for CI — `--format markdown` is a PR comment with a Mermaid diagram.")
|
|
713
881
|
.argument(...DIR_ARG)
|
|
@@ -736,8 +904,7 @@ program
|
|
|
736
904
|
globalDir: program.opts().dir,
|
|
737
905
|
});
|
|
738
906
|
});
|
|
739
|
-
program
|
|
740
|
-
.command("grep")
|
|
907
|
+
grouped(program.command("grep"), GROUP.find)
|
|
741
908
|
.description("Regex search over indexed files, hits grouped by enclosing symbol and ranked by coupling ($0, no LLM)")
|
|
742
909
|
.argument("<pattern>", "regex pattern (or literal string with --fixed)")
|
|
743
910
|
.argument(...DIR_ARG)
|
|
@@ -765,8 +932,7 @@ program
|
|
|
765
932
|
globalDir: globalOpts.dir,
|
|
766
933
|
});
|
|
767
934
|
});
|
|
768
|
-
program
|
|
769
|
-
.command("map")
|
|
935
|
+
grouped(program.command("map"), GROUP.find)
|
|
770
936
|
.description("Token-budgeted repo orientation — directory clusters, per-directory hubs, and global hotspots from the wiring graph ($0, no LLM)")
|
|
771
937
|
.argument(...DIR_ARG)
|
|
772
938
|
.option("--max-dirs <n>", "max directory entries shown, rest counted into dropped (default 16)")
|
|
@@ -795,7 +961,7 @@ program
|
|
|
795
961
|
const contextDir = contextDirFor(root, globalOpts.dir);
|
|
796
962
|
const graph = loadGraphCached(contextDir);
|
|
797
963
|
if (!graph) {
|
|
798
|
-
console.error(
|
|
964
|
+
console.error(`✗ no graph — run ${cmd("graft build")} first`);
|
|
799
965
|
process.exit(1);
|
|
800
966
|
return;
|
|
801
967
|
}
|
|
@@ -806,9 +972,175 @@ program
|
|
|
806
972
|
}
|
|
807
973
|
process.stdout.write(formatRepoMap(map));
|
|
808
974
|
});
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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)")
|
|
812
1144
|
.argument("[dir]", "target repo directory", ".")
|
|
813
1145
|
.option("--no-build", "skip building the graph (wire files only)")
|
|
814
1146
|
.option("--agents <ids...>", `only these agents (${hostIds().join(", ")}, claude)`)
|
|
@@ -821,45 +1153,124 @@ program
|
|
|
821
1153
|
.option("--dry-run", "print every file init would touch, then exit without writing")
|
|
822
1154
|
.option("-y, --yes", "skip the picker and wire every detected agent (the pre-0.8 default)")
|
|
823
1155
|
.option("--no-global", "skip writes outside this repo (the ~/.codex/ config + hooks)")
|
|
824
|
-
.option("--trail <handoff>", "attach a Trail: <brainId>:<token> (or a bare id with GRAFT_BRAIN_TOKEN set)")
|
|
825
1156
|
.option("--verbose", "print every file written, the graph build's own output and the closing banner")
|
|
826
1157
|
.action(async (dir, opts) => {
|
|
827
1158
|
await runInitCommand(dir, opts);
|
|
828
1159
|
});
|
|
829
|
-
/**
|
|
830
|
-
function
|
|
831
|
-
if (
|
|
832
|
-
return
|
|
833
|
-
|
|
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")}`);
|
|
834
1262
|
}
|
|
835
1263
|
/** Repo-relative with forward slashes, for printing. */
|
|
836
1264
|
function shown(repo, path) {
|
|
837
1265
|
return relative(repo, path).split("\\").join("/");
|
|
838
1266
|
}
|
|
839
|
-
/**
|
|
840
|
-
|
|
841
|
-
* up in — the Trail pages start people at push, and a trail whose rules no
|
|
842
|
-
* coding agent reads is only half set up. `epilogue: false` leaves out the
|
|
843
|
-
* closing banner when push is going to keep talking.
|
|
844
|
-
*/
|
|
845
|
-
async function runInitCommand(dir, opts, how = {}) {
|
|
1267
|
+
/** `graft init`. */
|
|
1268
|
+
async function runInitCommand(dir, opts) {
|
|
846
1269
|
if (opts.listAgents) {
|
|
847
1270
|
for (const id of [...hostIds(), "claude"])
|
|
848
1271
|
console.log(id);
|
|
849
1272
|
return null;
|
|
850
1273
|
}
|
|
851
|
-
// Parsed before anything is written: a mistyped handoff should cost the user
|
|
852
|
-
// an error, not a half-wired repo they have to `graft uninstall` out of.
|
|
853
|
-
let brainLink;
|
|
854
|
-
if (opts.trail) {
|
|
855
|
-
const parsed = parseBrainArg(opts.trail);
|
|
856
|
-
if ("error" in parsed) {
|
|
857
|
-
console.error(`✗ --trail: ${parsed.error}`);
|
|
858
|
-
process.exitCode = 1;
|
|
859
|
-
return null;
|
|
860
|
-
}
|
|
861
|
-
brainLink = parsed;
|
|
862
|
-
}
|
|
863
1274
|
const repo = resolve(dir);
|
|
864
1275
|
const explicit = Array.isArray(opts.agents) ? opts.agents : undefined;
|
|
865
1276
|
if (explicit) {
|
|
@@ -896,8 +1307,6 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
896
1307
|
const reason = offReason();
|
|
897
1308
|
const picked = await runPicker(plan, repo, home, {
|
|
898
1309
|
offerTelemetry: reason === null || reason === "disabled",
|
|
899
|
-
// From push, the line above the picker already asks the question.
|
|
900
|
-
...(how.push ? { title: null } : {}),
|
|
901
1310
|
});
|
|
902
1311
|
if (picked === null) {
|
|
903
1312
|
console.error("· cancelled — nothing written");
|
|
@@ -937,11 +1346,28 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
937
1346
|
}
|
|
938
1347
|
const wantClaude = ids.includes("claude");
|
|
939
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`);
|
|
940
1355
|
if (children.length)
|
|
941
1356
|
console.error(`· workspace: wiring ${repo} and ${children.length} child repo(s) — ${children.join(", ")}`);
|
|
942
1357
|
// Today's output, file by file, behind --verbose. Everything else gets one
|
|
943
1358
|
// line per step: the graph, then one per agent.
|
|
944
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
|
+
}
|
|
945
1371
|
const reports = [];
|
|
946
1372
|
for (const target of targets) {
|
|
947
1373
|
if (verbose && target !== repo)
|
|
@@ -951,7 +1377,6 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
951
1377
|
// The graph, built once after the wiring: a workspace parent's build is
|
|
952
1378
|
// the workspace build, so it covers every child.
|
|
953
1379
|
let graphLine = null;
|
|
954
|
-
let graphNodes = 0;
|
|
955
1380
|
if (!verbose) {
|
|
956
1381
|
const res = await buildGraphWithProgress(repo, { build: opts.build, cliPath });
|
|
957
1382
|
for (const m of res.messages)
|
|
@@ -959,24 +1384,22 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
959
1384
|
const existing = res.built ? null : loadGraphCached(contextDirFor(repo, children.length ? undefined : program.opts().dir));
|
|
960
1385
|
const fmt = (x) => x.toLocaleString("en-US");
|
|
961
1386
|
if (res.graph) {
|
|
962
|
-
graphNodes = res.graph.nodes;
|
|
963
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"}` : ""}`;
|
|
964
1388
|
}
|
|
965
1389
|
else if (res.built) {
|
|
966
1390
|
graphLine = "✓ graph built";
|
|
967
1391
|
}
|
|
968
1392
|
else if (res.failed) {
|
|
969
|
-
graphLine =
|
|
1393
|
+
graphLine = `⚠ the graph build failed — run ${cmd("graft build")} to see why`;
|
|
970
1394
|
}
|
|
971
1395
|
else if (existing) {
|
|
972
|
-
graphNodes = existing.meta.nodeCount;
|
|
973
1396
|
graphLine = `✓ graph ready · ${fmt(existing.meta.nodeCount)} nodes, ${fmt(existing.meta.edgeCount)} edges`;
|
|
974
1397
|
}
|
|
975
1398
|
else if (hasGraftIndex(repo)) {
|
|
976
1399
|
graphLine = "✓ graph ready";
|
|
977
1400
|
}
|
|
978
1401
|
else {
|
|
979
|
-
graphLine =
|
|
1402
|
+
graphLine = `· skipped the graph build — run ${cmd("graft build")}`;
|
|
980
1403
|
}
|
|
981
1404
|
}
|
|
982
1405
|
if (!verbose) {
|
|
@@ -987,50 +1410,46 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
987
1410
|
for (const w of reports.flatMap((r) => r.warnings))
|
|
988
1411
|
console.error(w);
|
|
989
1412
|
const writesFor = (id) => (plan.find((p) => p.id === id)?.writes ?? []).filter((w) => opts.global !== false || w.scope !== "global");
|
|
990
|
-
if (
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
else {
|
|
997
|
-
if (graphLine)
|
|
998
|
-
console.error(graphLine);
|
|
999
|
-
const width = Math.max(...ids.map((id) => id.length));
|
|
1000
|
-
for (const id of ids) {
|
|
1001
|
-
const summary = compactWrites(writesFor(id), repo, home);
|
|
1002
|
-
console.error(`✓ ${id.padEnd(width)} ${summary}`.trimEnd());
|
|
1003
|
-
}
|
|
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());
|
|
1004
1419
|
}
|
|
1005
1420
|
}
|
|
1006
|
-
//
|
|
1007
|
-
//
|
|
1008
|
-
|
|
1009
|
-
if (
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
console.error(`⚠ trail: ${res.warning}`);
|
|
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);
|
|
1023
1437
|
}
|
|
1024
|
-
else if (
|
|
1025
|
-
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`);
|
|
1026
1440
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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"}`);
|
|
1031
1450
|
}
|
|
1032
1451
|
}
|
|
1033
|
-
if (!verbose
|
|
1452
|
+
if (!verbose) {
|
|
1034
1453
|
// What there is to commit: the top-level entries the picked agents wrote.
|
|
1035
1454
|
const tops = [];
|
|
1036
1455
|
for (const w of selectedWrites(plan, ids)) {
|
|
@@ -1041,9 +1460,18 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
1041
1460
|
if (!tops.includes(top))
|
|
1042
1461
|
tops.push(top);
|
|
1043
1462
|
}
|
|
1044
|
-
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);
|
|
1045
1472
|
if (tops.length)
|
|
1046
|
-
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"));
|
|
1047
1475
|
}
|
|
1048
1476
|
// One epilogue for the whole run. A workspace parent holds no nodes of its
|
|
1049
1477
|
// own, so the totals come from the children — the graph the user actually got.
|
|
@@ -1051,7 +1479,7 @@ async function runInitCommand(dir, opts, how = {}) {
|
|
|
1051
1479
|
const graphs = (children.length ? children.map((c) => join(repo, c)) : [repo])
|
|
1052
1480
|
.map((d) => loadGraphCached(contextDirFor(d, children.length ? undefined : globalDir)))
|
|
1053
1481
|
.filter((g) => g !== null);
|
|
1054
|
-
if (verbose
|
|
1482
|
+
if (verbose)
|
|
1055
1483
|
console.error("\n" +
|
|
1056
1484
|
formatInitEpilogue({
|
|
1057
1485
|
graphBuilt: graphs.length > 0,
|
|
@@ -1090,7 +1518,7 @@ function wireTarget(repo, ids, ctx) {
|
|
|
1090
1518
|
// `global`/`home` are threaded through alongside `statusline`: the claude layer
|
|
1091
1519
|
// writes under `~/.claude` now (hosts/claude-global.ts), so --no-global has to
|
|
1092
1520
|
// reach it or the flag would silently mean "no out-of-repo writes, except three".
|
|
1093
|
-
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() });
|
|
1094
1522
|
say(`✓ wrote ${res.settingsPath}`);
|
|
1095
1523
|
for (const s of res.shims)
|
|
1096
1524
|
say(`✓ wrote ${s}`);
|
|
@@ -1101,7 +1529,7 @@ function wireTarget(repo, ids, ctx) {
|
|
|
1101
1529
|
say(`· mcp claude: ${res.mcp.path} (already registered)`);
|
|
1102
1530
|
else
|
|
1103
1531
|
say(`✓ mcp claude: ${res.mcp.path} (${res.mcp.action}) — restart Claude Code to load the graft MCP server`);
|
|
1104
|
-
say(res.built ?
|
|
1532
|
+
say(res.built ? `✓ built the graph (${cmd("graft build")})` : "· skipped graph build");
|
|
1105
1533
|
if (!wantStatusline)
|
|
1106
1534
|
say("· skipped Claude Code statusLine (--no-statusline)");
|
|
1107
1535
|
for (const w of res.warnings)
|
|
@@ -1117,6 +1545,7 @@ function wireTarget(repo, ids, ctx) {
|
|
|
1117
1545
|
mcp: opts.mcp,
|
|
1118
1546
|
hooks: opts.hooks,
|
|
1119
1547
|
global: opts.global,
|
|
1548
|
+
brand: brand(),
|
|
1120
1549
|
});
|
|
1121
1550
|
for (const w of r.written)
|
|
1122
1551
|
say(`✓ ${w.id}: ${w.path} (${w.action})`);
|
|
@@ -1144,7 +1573,7 @@ function wireTarget(repo, ids, ctx) {
|
|
|
1144
1573
|
if (!wantClaude) {
|
|
1145
1574
|
if (!quiet)
|
|
1146
1575
|
say(buildGraphIfMissing(repo, { build: opts.build, cliPath })
|
|
1147
|
-
?
|
|
1576
|
+
? `✓ built the graph (${cmd("graft build")})`
|
|
1148
1577
|
: "· skipped graph build");
|
|
1149
1578
|
}
|
|
1150
1579
|
return report;
|
|
@@ -1181,9 +1610,8 @@ function formatRetractions(rs, apply) {
|
|
|
1181
1610
|
}
|
|
1182
1611
|
return lines.join("\n").replace(/^\n/, "");
|
|
1183
1612
|
}
|
|
1184
|
-
program
|
|
1185
|
-
.
|
|
1186
|
-
.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)`)
|
|
1187
1615
|
.argument("[dir]", "target repo directory", ".")
|
|
1188
1616
|
.option("-y, --yes", "actually remove (without this, prints what it would remove and exits)")
|
|
1189
1617
|
.option("--keep-cache", "keep graft/ and the .gitignore entries — wiring only")
|
|
@@ -1204,433 +1632,485 @@ program
|
|
|
1204
1632
|
const bad = changed(done).filter((r) => r.action === "skipped-unparseable");
|
|
1205
1633
|
console.error(bad.length
|
|
1206
1634
|
? `\n⚠ ${bad.length} file(s) could not be parsed and were left as-is — see above.`
|
|
1207
|
-
:
|
|
1635
|
+
: `\n✓ ${brand()} fully removed. \`${cmd("graft init")}\` re-wires from scratch.`);
|
|
1208
1636
|
});
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
.description("The Trail attached to this repo: the rules mined from its own history");
|
|
1637
|
+
/* -------------------------------------------------------------------------- */
|
|
1638
|
+
/* trail login · logout · workspace */
|
|
1639
|
+
/* -------------------------------------------------------------------------- */
|
|
1213
1640
|
/**
|
|
1214
|
-
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
*
|
|
1218
|
-
* every failure prints its own reason first, because the caller only needs to
|
|
1219
|
-
* know whether to carry on. Nothing about the repository has been read or sent
|
|
1220
|
-
* 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.
|
|
1221
1645
|
*/
|
|
1222
|
-
|
|
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
|
-
|
|
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`);
|
|
1248
1682
|
});
|
|
1249
|
-
const got = await Promise.race([handoff.wait(), stopped]);
|
|
1250
|
-
if (onSigint)
|
|
1251
|
-
process.off("SIGINT", onSigint);
|
|
1252
|
-
spinner.stop();
|
|
1253
|
-
if (got === "stopped") {
|
|
1254
|
-
handoff.close();
|
|
1255
|
-
console.error("· stopped — nothing was sent; run graft trail push again when you're ready");
|
|
1256
|
-
track("brain_signup_settled", { outcome: "stopped", mode: "terminal", duration_bucket: durationBucket(Date.now() - startedAt) }, { repo });
|
|
1257
|
-
process.exitCode = 130;
|
|
1258
|
-
return null;
|
|
1259
|
-
}
|
|
1260
|
-
if ("error" in got) {
|
|
1261
|
-
console.error(`✗ ${got.error}`);
|
|
1262
|
-
// The category, never the sentence: `got.error` names the repo and the link.
|
|
1263
|
-
track("brain_signup_settled", { outcome: got.reason, mode: "terminal", duration_bucket: durationBucket(Date.now() - startedAt) }, { repo });
|
|
1264
|
-
return null;
|
|
1265
|
-
}
|
|
1266
|
-
writeLink(repo, got.link);
|
|
1267
|
-
console.error(`✓ trail connected · ${slug} — your browser shows it building`);
|
|
1268
|
-
track("brain_signup_settled", { outcome: "linked", mode: "terminal", duration_bucket: durationBucket(Date.now() - startedAt) }, { repo });
|
|
1269
|
-
return got.link;
|
|
1270
1683
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
const startedAt = Date.now();
|
|
1284
|
-
const settled = (outcome) => track("brain_signup_settled", { outcome, mode: "agent", duration_bucket: durationBucket(Date.now() - startedAt) }, { repo });
|
|
1285
|
-
const pending = readPendingSignup(repo, slug, PENDING_SIGNUP_TTL_MS);
|
|
1286
|
-
if (!pending) {
|
|
1287
|
-
const state = newSignupState();
|
|
1288
|
-
writePendingSignup(repo, { state, repo: slug, createdAt: Date.now() });
|
|
1289
|
-
const url = signupUrl({ repo: slug, state });
|
|
1290
|
-
track("brain_signup_opened", { mode: "agent" }, { repo });
|
|
1291
|
-
openBrowser(url);
|
|
1292
|
-
console.error(`· ${slug} has no trail yet — opened Trail's sign-up page in the browser:`);
|
|
1293
|
-
console.error(` ${url}`);
|
|
1294
|
-
console.error("· nothing has been read or sent yet.");
|
|
1295
|
-
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.");
|
|
1296
|
-
settled("agent_link_opened");
|
|
1297
|
-
process.exitCode = 1;
|
|
1298
|
-
return null;
|
|
1299
|
-
}
|
|
1300
|
-
const url = signupUrl({ repo: slug, state: pending.state });
|
|
1301
|
-
console.error(`· waiting up to ${Math.round(AGENT_WAIT_MS / 1000)} s for the sign-up for ${slug} to finish:`);
|
|
1302
|
-
console.error(` ${url}`);
|
|
1303
|
-
const got = await waitForSignup(pending.state, apiBaseUrl());
|
|
1304
|
-
if ("link" in got) {
|
|
1305
|
-
clearPendingSignup(repo);
|
|
1306
|
-
writeLink(repo, got.link);
|
|
1307
|
-
console.error(`✓ trail connected · ${slug}`);
|
|
1308
|
-
settled("linked");
|
|
1309
|
-
return got.link;
|
|
1310
|
-
}
|
|
1311
|
-
if ("pending" in got) {
|
|
1312
|
-
console.error("· not signed up yet — nothing has been read or sent.");
|
|
1313
|
-
console.error("· next: run `graft trail push` again to keep waiting. The link above stays valid for about 15 minutes.");
|
|
1314
|
-
settled("still_waiting");
|
|
1315
|
-
process.exitCode = 1;
|
|
1316
|
-
return null;
|
|
1317
|
-
}
|
|
1318
|
-
clearPendingSignup(repo);
|
|
1319
|
-
console.error(`✗ ${got.error}`);
|
|
1320
|
-
settled(got.reason);
|
|
1321
|
-
process.exitCode = 1;
|
|
1322
|
-
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
|
+
});
|
|
1323
1696
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
.
|
|
1328
|
-
.argument("[dir]", "target repo directory", ".")
|
|
1329
|
-
.action(async (handoff, dir) => {
|
|
1330
|
-
const parsed = parseBrainArg(handoff);
|
|
1331
|
-
if ("error" in parsed) {
|
|
1332
|
-
console.error(`✗ ${parsed.error}`);
|
|
1333
|
-
process.exitCode = 1;
|
|
1334
|
-
return;
|
|
1335
|
-
}
|
|
1336
|
-
const repo = resolve(dir);
|
|
1337
|
-
const res = await connectBrain(repo, parsed, { home: homedir() });
|
|
1338
|
-
if (res.warning) {
|
|
1339
|
-
console.error(`⚠ ${res.warning}`);
|
|
1340
|
-
return;
|
|
1341
|
-
}
|
|
1342
|
-
if (res.ruleCount === 0) {
|
|
1343
|
-
// The normal case on the local route: the brain exists but nothing has
|
|
1344
|
-
// read the repo yet. Saying "0 rules" without saying why reads as a
|
|
1345
|
-
// failure, and the next step is the whole point.
|
|
1346
|
-
console.error("✓ attached this repo to the brain — it has no rules yet");
|
|
1347
|
-
console.error("· run `graft trail push` to read this repository into it");
|
|
1348
|
-
return;
|
|
1349
|
-
}
|
|
1350
|
-
console.error(`✓ pulled ${res.ruleCount} rule(s) from ${parsed.brainId}`);
|
|
1351
|
-
for (const w of res.writes)
|
|
1352
|
-
console.error(`✓ ${w.path} (${w.action})`);
|
|
1353
|
-
});
|
|
1354
|
-
brain
|
|
1355
|
-
.command("pull")
|
|
1356
|
-
.description("Refresh the trail's rules and write every change you accepted in Trail into this repo's context files")
|
|
1357
|
-
.argument("[dir]", "target repo directory", ".")
|
|
1358
|
-
.option("--dry-run", "show what would change without writing anything or telling Trail")
|
|
1359
|
-
.action(async (dir, opts) => {
|
|
1360
|
-
await runTrailPullCommand(dir, opts);
|
|
1361
|
-
});
|
|
1362
|
-
/** `graft trail pull`, and `graft claude-md pull` which is now the same thing. */
|
|
1363
|
-
async function runTrailPullCommand(dir, opts) {
|
|
1364
|
-
const repo = resolve(dir);
|
|
1365
|
-
const link = readLink(repo);
|
|
1366
|
-
if (!link) {
|
|
1367
|
-
console.error("✗ this repo has no trail yet — run graft trail push first");
|
|
1368
|
-
process.exitCode = 1;
|
|
1369
|
-
return;
|
|
1370
|
-
}
|
|
1371
|
-
const stamp = readStamp(repo);
|
|
1372
|
-
const wired = [...new Set([...(stamp?.hosts ?? []), ...wiredHostIds(repo)])];
|
|
1373
|
-
const code = await runTrailPull(repo, link, { home: homedir(), wired, dryRun: opts.dryRun });
|
|
1374
|
-
if (code !== 0)
|
|
1375
|
-
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 };
|
|
1376
1701
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
let link = readLink(repo);
|
|
1394
|
-
// Set when this run opened the browser, which then sits on Trail's build
|
|
1395
|
-
// page: the closing lines point at that tab instead of printing a new link.
|
|
1396
|
-
let browserOpen = false;
|
|
1397
|
-
if (!link) {
|
|
1398
|
-
if (!here) {
|
|
1399
|
-
console.error("✗ this directory has no GitHub origin remote — graft can only push a GitHub repository today");
|
|
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;
|
|
1714
|
+
}
|
|
1715
|
+
const me = await fetchMe(cloud);
|
|
1716
|
+
if (isError(me)) {
|
|
1717
|
+
console.error(`✗ ${me.error}`);
|
|
1400
1718
|
process.exitCode = 1;
|
|
1401
1719
|
return;
|
|
1402
1720
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
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));
|
|
1407
1725
|
return;
|
|
1408
1726
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
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;
|
|
1428
1790
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
// A repo graft was never set up in gets `graft init` first. Same picker as
|
|
1447
|
-
// init: it is how someone agrees to what gets written, so a
|
|
1448
|
-
// non-interactive push only says what to run.
|
|
1449
|
-
if (wiredHostIds(repo).length === 0) {
|
|
1450
|
-
if (process.stdin.isTTY && tty) {
|
|
1451
|
-
console.error("· graft isn't set up here yet — pick the agents your team uses:");
|
|
1452
|
-
await runInitCommand(repo, { build: true, mcp: true, hooks: true, statusline: true, global: true, verbose }, { epilogue: false, push: !verbose });
|
|
1791
|
+
if (line)
|
|
1792
|
+
out.push(line);
|
|
1793
|
+
return out;
|
|
1794
|
+
}
|
|
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;
|
|
1453
1808
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
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}`);
|
|
1814
|
+
process.exitCode = 1;
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
if (opts.json) {
|
|
1818
|
+
console.log(JSON.stringify(t, null, 2));
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
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
|
+
}
|
|
1834
|
+
}
|
|
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;
|
|
1456
1863
|
}
|
|
1457
1864
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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));
|
|
1470
1882
|
return;
|
|
1471
1883
|
}
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
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
|
+
}
|
|
1483
1894
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
const buildPage = brainUrl(link.brainId);
|
|
1490
|
-
// Held rather than handed back. Everything above this line succeeded even
|
|
1491
|
-
// in the runs that end badly: the push lands, and then the miner fails —
|
|
1492
|
-
// which is where roughly a third of production's repo trails die. Returning
|
|
1493
|
-
// the prompt here is what made that invisible from this side, on CI and
|
|
1494
|
-
// over SSH permanently so. `--no-watch` is for a caller that genuinely
|
|
1495
|
-
// wants fire-and-forget.
|
|
1496
|
-
if (opts.watch === false) {
|
|
1497
|
-
console.error("· building now — the rules reach this repo on their own; watch it at");
|
|
1498
|
-
console.error(` ${buildPage}`);
|
|
1499
|
-
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);
|
|
1500
1900
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
process.exit(130);
|
|
1513
|
-
};
|
|
1514
|
-
process.once("SIGINT", onSigint);
|
|
1515
|
-
const outcome = await watchBuild(link, {
|
|
1516
|
-
events: expected?.events === true,
|
|
1517
|
-
stageLines: !live,
|
|
1518
|
-
write: (line) => (spinner ? spinner.print(line) : console.error(line)),
|
|
1519
|
-
onState: (view, repoState) => {
|
|
1520
|
-
last = repoState;
|
|
1521
|
-
spinner?.update(`building the trail · ${DOING_LABEL[currentStage(view)]} · Ctrl-C detaches`);
|
|
1522
|
-
},
|
|
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);
|
|
1523
1912
|
});
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
if (outcome === "completed") {
|
|
1543
|
-
console.error(`✓ trail built · ${rules}, reaching this repo on their own`);
|
|
1544
|
-
closing();
|
|
1545
|
-
return;
|
|
1546
|
-
}
|
|
1547
|
-
// The ordinary ending for anything but a small repository. The history is
|
|
1548
|
-
// mined in slices, and the prompt comes back on the first of them: mining is
|
|
1549
|
-
// the part that fails and it has now succeeded, and the rest is filing,
|
|
1550
|
-
// which is minutes of reliable work nobody gains by watching.
|
|
1551
|
-
if (outcome === "building") {
|
|
1552
|
-
console.error(firstPush
|
|
1553
|
-
? `✓ trail has its first ${rules} · they reach this repo on their own`
|
|
1554
|
-
: `✓ trail updated · ${rules}, reaching this repo on their own`);
|
|
1555
|
-
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;
|
|
1556
1931
|
return;
|
|
1557
1932
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
// should hear about it the way it hears about any other failure. The
|
|
1562
|
-
// watcher has already printed where it stopped and why.
|
|
1563
|
-
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}`);
|
|
1564
1936
|
return;
|
|
1565
1937
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
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));
|
|
1568
1958
|
}
|
|
1569
1959
|
else {
|
|
1570
|
-
|
|
1960
|
+
printCheck(r);
|
|
1961
|
+
if (overlaps.length)
|
|
1962
|
+
console.log(`\n${overlaps.map((o) => formatOverlap(o)).join("\n")}`);
|
|
1963
|
+
printLines(cloudLines(repo, { command: "check" }));
|
|
1571
1964
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
.
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
});
|
|
1588
|
-
|
|
1589
|
-
.command("status")
|
|
1590
|
-
.description("Show the attached brain, how many rules are cached, and how many still match the code")
|
|
1591
|
-
.argument("[dir]", "target repo directory", ".")
|
|
1592
|
-
.option("--json", "machine-readable output")
|
|
1593
|
-
.action((dir, opts) => {
|
|
1594
|
-
const repo = resolve(dir);
|
|
1595
|
-
const { link, rules, fetchedAt } = brainStatus(repo);
|
|
1596
|
-
// Resolve every cached rule against the CURRENT graph, so the count of stale
|
|
1597
|
-
// rules is the real one rather than what was true at pull time. This is the
|
|
1598
|
-
// number worth surfacing: it says how far the codebase has drifted from what
|
|
1599
|
-
// the team decided.
|
|
1600
|
-
const graph = loadGraphCached(contextDirFor(repo, program.opts().dir));
|
|
1601
|
-
const applied = rulesForPointers((graph?.nodes ?? []).map((n) => `${n.path}:${n.span}`), rules, graph);
|
|
1602
|
-
if (opts.json) {
|
|
1603
|
-
console.log(JSON.stringify({ brainId: link?.brainId ?? null, cached: rules.length, fetchedAt, anchored: applied.length, stale: applied.filter((a) => a.stale).length }, null, 2));
|
|
1604
|
-
return;
|
|
1605
|
-
}
|
|
1606
|
-
if (!link) {
|
|
1607
|
-
console.error("· no brain attached — run `graft trail connect <brainId>:<token>`");
|
|
1608
|
-
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
|
+
}
|
|
1609
1982
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
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;
|
|
1615
2003
|
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
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) => {
|
|
1634
2114
|
console.error(err instanceof Error ? err.message : err);
|
|
1635
2115
|
process.exit(1);
|
|
1636
2116
|
});
|