@nanonets/graft 0.23.0 → 0.24.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 +4 -4
- package/dist/ai/crux.d.ts +31 -3
- package/dist/ai/crux.d.ts.map +1 -1
- package/dist/ai/crux.js +88 -10
- package/dist/ai/crux.js.map +1 -1
- package/dist/ai/llm/openai.d.ts +101 -0
- package/dist/ai/llm/openai.d.ts.map +1 -1
- package/dist/ai/llm/openai.js +396 -6
- package/dist/ai/llm/openai.js.map +1 -1
- package/dist/ai/llm/recover-tool.d.ts +2 -0
- package/dist/ai/llm/recover-tool.d.ts.map +1 -1
- package/dist/ai/llm/recover-tool.js +1 -1
- package/dist/ai/llm/recover-tool.js.map +1 -1
- package/dist/ai/synthesize.d.ts.map +1 -1
- package/dist/ai/synthesize.js +100 -1
- package/dist/ai/synthesize.js.map +1 -1
- package/dist/claude/format.d.ts +21 -0
- package/dist/claude/format.d.ts.map +1 -1
- package/dist/claude/format.js +46 -1
- package/dist/claude/format.js.map +1 -1
- package/dist/claude/hooks.d.ts +5 -9
- package/dist/claude/hooks.d.ts.map +1 -1
- package/dist/claude/hooks.js +71 -58
- package/dist/claude/hooks.js.map +1 -1
- package/dist/claude/init.js +3 -3
- package/dist/claude/init.js.map +1 -1
- package/dist/claude/note-signals.d.ts +28 -0
- package/dist/claude/note-signals.d.ts.map +1 -0
- package/dist/claude/note-signals.js +325 -0
- package/dist/claude/note-signals.js.map +1 -0
- package/dist/claude/shim-template.d.ts.map +1 -1
- package/dist/claude/shim-template.js +17 -6
- package/dist/claude/shim-template.js.map +1 -1
- package/dist/claude/skill-template.d.ts.map +1 -1
- package/dist/claude/skill-template.js +49 -18
- package/dist/claude/skill-template.js.map +1 -1
- package/dist/claude/state.d.ts +4 -3
- package/dist/claude/state.d.ts.map +1 -1
- package/dist/claude/state.js.map +1 -1
- package/dist/claude/sync-run.js +1 -1
- package/dist/claude/sync-run.js.map +1 -1
- package/dist/cli-picker.d.ts +12 -5
- package/dist/cli-picker.d.ts.map +1 -1
- package/dist/cli-picker.js +24 -8
- package/dist/cli-picker.js.map +1 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +149 -43
- package/dist/cli.js.map +1 -1
- package/dist/context/batches.d.ts +29 -0
- package/dist/context/batches.d.ts.map +1 -0
- package/dist/context/batches.js +130 -0
- package/dist/context/batches.js.map +1 -0
- package/dist/context/build.d.ts +7 -2
- package/dist/context/build.d.ts.map +1 -1
- package/dist/context/build.js +84 -50
- package/dist/context/build.js.map +1 -1
- package/dist/context/node-file.d.ts.map +1 -1
- package/dist/context/node-file.js +41 -4
- package/dist/context/node-file.js.map +1 -1
- package/dist/engine.d.ts +2 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1 -0
- package/dist/engine.js.map +1 -1
- package/dist/graph/cards.d.ts.map +1 -1
- package/dist/graph/cards.js +9 -4
- package/dist/graph/cards.js.map +1 -1
- package/dist/graph/enrich.d.ts.map +1 -1
- package/dist/graph/enrich.js +8 -3
- package/dist/graph/enrich.js.map +1 -1
- package/dist/graph/generic.d.ts.map +1 -1
- package/dist/graph/generic.js +207 -6
- package/dist/graph/generic.js.map +1 -1
- package/dist/graph/lsp/client.d.ts +18 -0
- package/dist/graph/lsp/client.d.ts.map +1 -1
- package/dist/graph/lsp/client.js +40 -2
- package/dist/graph/lsp/client.js.map +1 -1
- package/dist/graph/lsp/enrich.d.ts.map +1 -1
- package/dist/graph/lsp/enrich.js +9 -1
- package/dist/graph/lsp/enrich.js.map +1 -1
- package/dist/graph/lsp/registry.d.ts +7 -0
- package/dist/graph/lsp/registry.d.ts.map +1 -1
- package/dist/graph/lsp/registry.js +27 -2
- package/dist/graph/lsp/registry.js.map +1 -1
- package/dist/graph/queries/gleam.scm +51 -0
- package/dist/graph/resolve.d.ts.map +1 -1
- package/dist/graph/resolve.js +65 -3
- package/dist/graph/resolve.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 +1 -1
- package/dist/graph/workspace-cli.js.map +1 -1
- package/dist/init-guard.d.ts +11 -0
- package/dist/init-guard.d.ts.map +1 -0
- package/dist/init-guard.js +21 -0
- package/dist/init-guard.js.map +1 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +2 -1
- package/dist/mcp/tools.js.map +1 -1
- package/dist/notes/branches.d.ts +7 -1
- package/dist/notes/branches.d.ts.map +1 -1
- package/dist/notes/branches.js +31 -3
- package/dist/notes/branches.js.map +1 -1
- package/dist/notes/cold-start.d.ts +12 -2
- package/dist/notes/cold-start.d.ts.map +1 -1
- package/dist/notes/cold-start.js +46 -5
- package/dist/notes/cold-start.js.map +1 -1
- package/dist/notes/credit.d.ts +37 -0
- package/dist/notes/credit.d.ts.map +1 -0
- package/dist/notes/credit.js +78 -0
- package/dist/notes/credit.js.map +1 -0
- package/dist/notes/find-notes.d.ts +17 -0
- package/dist/notes/find-notes.d.ts.map +1 -0
- package/dist/notes/find-notes.js +48 -0
- package/dist/notes/find-notes.js.map +1 -0
- package/dist/notes/flatten.d.ts +31 -0
- package/dist/notes/flatten.d.ts.map +1 -0
- package/dist/notes/flatten.js +73 -0
- package/dist/notes/flatten.js.map +1 -0
- package/dist/notes/git-facts.d.ts +7 -1
- package/dist/notes/git-facts.d.ts.map +1 -1
- package/dist/notes/git-facts.js +12 -2
- package/dist/notes/git-facts.js.map +1 -1
- package/dist/notes/home.d.ts +24 -6
- package/dist/notes/home.d.ts.map +1 -1
- package/dist/notes/home.js +82 -15
- package/dist/notes/home.js.map +1 -1
- package/dist/notes/learning-fields.d.ts +102 -0
- package/dist/notes/learning-fields.d.ts.map +1 -0
- package/dist/notes/learning-fields.js +214 -0
- package/dist/notes/learning-fields.js.map +1 -0
- package/dist/notes/memory-notes.d.ts +71 -0
- package/dist/notes/memory-notes.d.ts.map +1 -0
- package/dist/notes/memory-notes.js +239 -0
- package/dist/notes/memory-notes.js.map +1 -0
- package/dist/notes/note-gate.d.ts +42 -0
- package/dist/notes/note-gate.d.ts.map +1 -0
- package/dist/notes/note-gate.js +181 -0
- package/dist/notes/note-gate.js.map +1 -0
- package/dist/notes/notes.d.ts +29 -77
- package/dist/notes/notes.d.ts.map +1 -1
- package/dist/notes/notes.js +69 -228
- package/dist/notes/notes.js.map +1 -1
- package/dist/notes/pr-reviews.d.ts +35 -7
- package/dist/notes/pr-reviews.d.ts.map +1 -1
- package/dist/notes/pr-reviews.js +156 -46
- package/dist/notes/pr-reviews.js.map +1 -1
- package/dist/notes/secrets.d.ts +28 -0
- package/dist/notes/secrets.d.ts.map +1 -0
- package/dist/notes/secrets.js +362 -0
- package/dist/notes/secrets.js.map +1 -0
- package/dist/notes/session-cost.d.ts +2 -0
- package/dist/notes/session-cost.d.ts.map +1 -1
- package/dist/notes/session-cost.js +8 -3
- package/dist/notes/session-cost.js.map +1 -1
- package/dist/notes/targets.d.ts +37 -0
- package/dist/notes/targets.d.ts.map +1 -0
- package/dist/notes/targets.js +108 -0
- package/dist/notes/targets.js.map +1 -0
- package/dist/notes/team-check.d.ts +22 -0
- package/dist/notes/team-check.d.ts.map +1 -0
- package/dist/notes/team-check.js +130 -0
- package/dist/notes/team-check.js.map +1 -0
- package/dist/notes/trail-home.d.ts +3 -0
- package/dist/notes/trail-home.d.ts.map +1 -0
- package/dist/notes/trail-home.js +12 -0
- package/dist/notes/trail-home.js.map +1 -0
- package/dist/search/grep-cli.d.ts +5 -2
- package/dist/search/grep-cli.d.ts.map +1 -1
- package/dist/search/grep-cli.js +11 -4
- package/dist/search/grep-cli.js.map +1 -1
- package/dist/search/ignored-code.d.ts +9 -0
- package/dist/search/ignored-code.d.ts.map +1 -0
- package/dist/search/ignored-code.js +73 -0
- package/dist/search/ignored-code.js.map +1 -0
- package/dist/skills/answers-prompt.d.ts +13 -0
- package/dist/skills/answers-prompt.d.ts.map +1 -0
- package/dist/skills/answers-prompt.js +83 -0
- package/dist/skills/answers-prompt.js.map +1 -0
- package/dist/skills/never-save.d.ts +5 -0
- package/dist/skills/never-save.d.ts.map +1 -0
- package/dist/skills/never-save.js +158 -0
- package/dist/skills/never-save.js.map +1 -0
- package/dist/skills/params.d.ts +39 -0
- package/dist/skills/params.d.ts.map +1 -0
- package/dist/skills/params.js +275 -0
- package/dist/skills/params.js.map +1 -0
- package/dist/skills/ran-less.d.ts +5 -0
- package/dist/skills/ran-less.d.ts.map +1 -0
- package/dist/skills/ran-less.js +51 -0
- package/dist/skills/ran-less.js.map +1 -0
- package/dist/skills/risk-delta.d.ts +32 -0
- package/dist/skills/risk-delta.d.ts.map +1 -0
- package/dist/skills/risk-delta.js +56 -0
- package/dist/skills/risk-delta.js.map +1 -0
- package/dist/skills/risk.d.ts +73 -0
- package/dist/skills/risk.d.ts.map +1 -0
- package/dist/skills/risk.js +396 -0
- package/dist/skills/risk.js.map +1 -0
- package/dist/skills/types.d.ts +422 -0
- package/dist/skills/types.d.ts.map +1 -0
- package/dist/skills/types.js +2 -0
- package/dist/skills/types.js.map +1 -0
- package/dist/telemetry/contract.d.ts.map +1 -1
- package/dist/telemetry/contract.js +8 -6
- package/dist/telemetry/contract.js.map +1 -1
- package/dist/telemetry/notice.d.ts.map +1 -1
- package/dist/telemetry/notice.js +3 -1
- package/dist/telemetry/notice.js.map +1 -1
- package/dist/trace/check-reads.d.ts +36 -0
- package/dist/trace/check-reads.d.ts.map +1 -0
- package/dist/trace/check-reads.js +57 -0
- package/dist/trace/check-reads.js.map +1 -0
- package/dist/trace/host.d.ts +40 -0
- package/dist/trace/host.d.ts.map +1 -0
- package/dist/trace/host.js +84 -0
- package/dist/trace/host.js.map +1 -0
- package/dist/trace/redact.d.ts +8 -0
- package/dist/trace/redact.d.ts.map +1 -0
- package/dist/trace/redact.js +63 -0
- package/dist/trace/redact.js.map +1 -0
- package/dist/trace/seen-cache.d.ts +9 -0
- package/dist/trace/seen-cache.d.ts.map +1 -0
- package/dist/trace/seen-cache.js +46 -0
- package/dist/trace/seen-cache.js.map +1 -0
- package/dist/trace/shell-scan.d.ts +52 -0
- package/dist/trace/shell-scan.d.ts.map +1 -0
- package/dist/trace/shell-scan.js +318 -0
- package/dist/trace/shell-scan.js.map +1 -0
- package/dist/trace/shell-words.d.ts +91 -0
- package/dist/trace/shell-words.d.ts.map +1 -0
- package/dist/trace/shell-words.js +558 -0
- package/dist/trace/shell-words.js.map +1 -0
- package/dist/trace/shell.d.ts +91 -0
- package/dist/trace/shell.d.ts.map +1 -0
- package/dist/trace/shell.js +435 -0
- package/dist/trace/shell.js.map +1 -0
- package/dist/trace/state.d.ts +27 -0
- package/dist/trace/state.d.ts.map +1 -0
- package/dist/trace/state.js +98 -0
- package/dist/trace/state.js.map +1 -0
- package/dist/trace/tamper-code.d.ts +9 -0
- package/dist/trace/tamper-code.d.ts.map +1 -0
- package/dist/trace/tamper-code.js +174 -0
- package/dist/trace/tamper-code.js.map +1 -0
- package/dist/trace/tamper-git.d.ts +12 -0
- package/dist/trace/tamper-git.d.ts.map +1 -0
- package/dist/trace/tamper-git.js +77 -0
- package/dist/trace/tamper-git.js.map +1 -0
- package/dist/trace/types.d.ts +477 -0
- package/dist/trace/types.d.ts.map +1 -0
- package/dist/trace/types.js +12 -0
- package/dist/trace/types.js.map +1 -0
- package/dist/trace/walls-glob.d.ts +11 -0
- package/dist/trace/walls-glob.d.ts.map +1 -0
- package/dist/trace/walls-glob.js +264 -0
- package/dist/trace/walls-glob.js.map +1 -0
- package/dist/trace/walls-quoted.d.ts +25 -0
- package/dist/trace/walls-quoted.d.ts.map +1 -0
- package/dist/trace/walls-quoted.js +81 -0
- package/dist/trace/walls-quoted.js.map +1 -0
- package/dist/upkeep.d.ts +1 -1
- package/dist/upkeep.d.ts.map +1 -1
- package/dist/upkeep.js +4 -1
- package/dist/upkeep.js.map +1 -1
- package/dist/util/actions-folder.d.ts +2 -0
- package/dist/util/actions-folder.d.ts.map +1 -0
- package/dist/util/actions-folder.js +9 -0
- package/dist/util/actions-folder.js.map +1 -0
- package/dist/util/front-matter.d.ts +31 -4
- package/dist/util/front-matter.d.ts.map +1 -1
- package/dist/util/front-matter.js +95 -3
- package/dist/util/front-matter.js.map +1 -1
- package/dist/util/private-file.d.ts +14 -0
- package/dist/util/private-file.d.ts.map +1 -0
- package/dist/util/private-file.js +82 -0
- package/dist/util/private-file.js.map +1 -0
- package/dist/util/private-fs.d.ts +25 -0
- package/dist/util/private-fs.d.ts.map +1 -0
- package/dist/util/private-fs.js +253 -0
- package/dist/util/private-fs.js.map +1 -0
- package/dist/util/state.d.ts.map +1 -1
- package/dist/util/state.js +3 -0
- package/dist/util/state.js.map +1 -1
- package/dist/viewer/app.js +2 -2
- package/package.json +5 -5
- package/scripts/run-tests.mjs +14 -2
package/dist/notes/notes.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import type { NoteHit } from "./find-notes.js";
|
|
2
|
+
import { type LearningFields } from "./learning-fields.js";
|
|
3
|
+
export { findNotes, type NoteHit } from "./find-notes.js";
|
|
4
|
+
export { mentionedFiles, noteTargets, type NoteTarget } from "./targets.js";
|
|
5
|
+
export { commitLines, namesLabel, PR_SPEEDUP, setupCommitLine, TOKEN_CREDIT_FLOOR, tokensLabel, tokensWorthSaying } from "./credit.js";
|
|
1
6
|
export interface NoteCost {
|
|
2
7
|
minutes?: number;
|
|
3
8
|
tokens?: number;
|
|
4
9
|
}
|
|
5
|
-
|
|
10
|
+
/** A learning. Its id, kind, scope, status and the rest are in `LearningFields`. */
|
|
11
|
+
export interface Note extends LearningFields {
|
|
6
12
|
/** Where the note is: an absolute path on this machine. */
|
|
7
13
|
path: string;
|
|
8
14
|
title: string;
|
|
@@ -16,12 +22,14 @@ export interface Note {
|
|
|
16
22
|
body: string;
|
|
17
23
|
/** Written by someone other than whoever is asking. */
|
|
18
24
|
teammate?: boolean;
|
|
19
|
-
/** `repo`: in the repo's `.trail/learnings/`, for everyone. `personal`: in ~/.trail, this machine only. */
|
|
20
|
-
scope?: "repo" | "personal";
|
|
21
25
|
/** A rule or preference for every session here, not knowledge about one
|
|
22
26
|
* task: what the user kept having to tell Claude. The session-start hook
|
|
23
27
|
* lists these up front, since no single task would bring them up. */
|
|
24
28
|
standing?: boolean;
|
|
29
|
+
/** The file name of an earlier learning this one takes over from. The old
|
|
30
|
+
* file stays, as the record of what that session knew, but `listNotes`
|
|
31
|
+
* reads this one instead. */
|
|
32
|
+
replaces?: string;
|
|
25
33
|
/** Found on a teammate's pushed branch (`origin/kumar/jitter`) rather than in
|
|
26
34
|
* this checkout. `path` is then `<ref>:.trail/learnings/<name>`, readable
|
|
27
35
|
* with `git show`, and the whole body travels with the note. */
|
|
@@ -44,7 +52,11 @@ export declare const SECTIONS: readonly [{
|
|
|
44
52
|
readonly label: "watch out";
|
|
45
53
|
}];
|
|
46
54
|
export declare function renderNote(n: Omit<Note, "path">): string;
|
|
47
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* A note file, or null when it isn't one (no title in its frontmatter), or
|
|
57
|
+
* when its front matter is refused (anchors, aliases, a list for a title) or
|
|
58
|
+
* isn't valid YAML, which says so once on stderr.
|
|
59
|
+
*/
|
|
48
60
|
export declare function parseNote(text: string, path: string): Note | null;
|
|
49
61
|
/** The file name a note is known by, wherever it sits: one learning on a branch and in the tree is one learning. */
|
|
50
62
|
export declare function noteName(n: Note): string;
|
|
@@ -75,65 +87,32 @@ export interface WriteNoteInput {
|
|
|
75
87
|
cost?: NoteCost;
|
|
76
88
|
touches?: string[];
|
|
77
89
|
standing?: boolean;
|
|
90
|
+
/** The fields a new learning may be given; its id, status and scope come from where it is written. */
|
|
91
|
+
kind?: LearningFields["kind"];
|
|
92
|
+
replaces?: string;
|
|
93
|
+
src?: LearningFields["src"];
|
|
94
|
+
evidence?: string;
|
|
95
|
+
/** Personal only: defaults to this repo. */
|
|
96
|
+
applies?: string;
|
|
78
97
|
}
|
|
79
98
|
/**
|
|
80
99
|
* Write a new learning for the repo `dir` is in, and return it: into the
|
|
81
100
|
* repo's `.trail/learnings/` (made if needed), or with `scope: "personal"`,
|
|
82
101
|
* or outside git, into ~/.trail. Never overwrites: a second one with the same
|
|
83
|
-
* name on the same day gets `-2`, `-3`.
|
|
102
|
+
* name on the same day gets `-2`, `-3`. Its id is 8 hex digits from the file
|
|
103
|
+
* name it gets (`newLearningId`), written into the file.
|
|
84
104
|
*/
|
|
85
105
|
export declare function writeNote(dir: string, input: WriteNoteInput, scope?: LearningScope): Note;
|
|
86
106
|
/**
|
|
87
107
|
* The standing learnings for `dir`: the team's in the checkout and the
|
|
88
108
|
* person's own, never ones on a teammate's unmerged branch, which nobody has
|
|
89
|
-
* reviewed yet.
|
|
109
|
+
* reviewed yet, and only live ones (nothing retired or replaced).
|
|
90
110
|
*/
|
|
91
111
|
export declare function standingLearnings(dir: string): Note[];
|
|
92
112
|
/** Where a learning is, as the agent and the person read it: `.trail/learnings/x.md`, `~/.trail/…`, or `origin/b:.trail/…`. */
|
|
93
113
|
export declare function notePlace(n: Note): string;
|
|
94
|
-
export interface NoteTarget {
|
|
95
|
-
/** A folder in the repo the note goes to. */
|
|
96
|
-
dir: string;
|
|
97
|
-
/** What the session touched there, relative to the top of that checkout. */
|
|
98
|
-
touches: string[];
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Which repos a session's note belongs in, and what it touched in each.
|
|
102
|
-
*
|
|
103
|
-
* The repo the note was saved from gets the files it has uncommitted or
|
|
104
|
-
* committed since `since`. Every other repo the session edited (`edited`:
|
|
105
|
-
* absolute paths, from the agent's transcript) gets the note too, with its
|
|
106
|
-
* own files, so a session that changed a backend and the library it calls
|
|
107
|
-
* leaves a note in both. Saved from a folder that holds several repos, the
|
|
108
|
-
* note goes to each repo the session changed, or stays with the folder when
|
|
109
|
-
* it changed none. Two worktrees of one repo are one repo.
|
|
110
|
-
*/
|
|
111
|
-
export declare function noteTargets(dir: string, edited?: string[], since?: number): NoteTarget[];
|
|
112
|
-
/**
|
|
113
|
-
* Files of the checkout at `dir` that a note's text names by path. A session
|
|
114
|
-
* that only read code edits nothing, so without these its note would touch no
|
|
115
|
-
* file, and nothing that looks a note up by file (`trail check`, the ranked
|
|
116
|
-
* search) would ever find it.
|
|
117
|
-
*/
|
|
118
|
-
export declare function mentionedFiles(dir: string, body: string): string[];
|
|
119
|
-
export interface NoteHit {
|
|
120
|
-
note: Note;
|
|
121
|
-
score: number;
|
|
122
|
-
/** Files the note touches that this query's results also point into. */
|
|
123
|
-
shared: string[];
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* The notes worth showing next to a query's results: ones whose title or body
|
|
127
|
-
* share the query's words, or that touch a file the results point into. At
|
|
128
|
-
* most `limit`, best first. A note has to clear a bar — two of the query's
|
|
129
|
-
* words, or one in its title, or a shared file — so an unrelated note never
|
|
130
|
-
* rides along just because it exists.
|
|
131
|
-
*/
|
|
132
|
-
export declare function findNotes(notes: Note[], query: string, files?: string[], limit?: number): NoteHit[];
|
|
133
114
|
/** `2026-10-07` → `Oct 7`; anything else as it came. */
|
|
134
115
|
export declare function shortDate(date: string): string;
|
|
135
|
-
/** `~9.6k tokens`, `~600 tokens`. */
|
|
136
|
-
export declare function tokensLabel(n: number): string;
|
|
137
116
|
/** `took 12 min and ~9.6k tokens to work out`, or null when the note has no cost. */
|
|
138
117
|
export declare function costLabel(cost?: NoteCost): string | null;
|
|
139
118
|
/** What reading a note costs an agent, in tokens: its text at ~4 characters a token. */
|
|
@@ -143,7 +122,7 @@ export declare function readingTokens(n: Note): number;
|
|
|
143
122
|
* it costs. 0 when it carries no token cost, or reading it costs as much.
|
|
144
123
|
*/
|
|
145
124
|
export declare function savedByNote(n: Note): number;
|
|
146
|
-
/** `
|
|
125
|
+
/** `Maya's learning`, `a learning`, `Kumar's learning on kumar/jitter`. */
|
|
147
126
|
export declare function whoseLearning(n: Note): string;
|
|
148
127
|
/**
|
|
149
128
|
* The block `ask` prints above its results: whose learning each is, when,
|
|
@@ -151,39 +130,12 @@ export declare function whoseLearning(n: Note): string;
|
|
|
151
130
|
* where to read the rest. A learning on a teammate's unmerged branch says so,
|
|
152
131
|
* with the `git show` that prints it, since there's no file here to open. An agent
|
|
153
132
|
* relays the cost when it says what the turn saved, which is how the saving
|
|
154
|
-
* becomes countable.
|
|
133
|
+
* becomes countable. Every field is flattened to one line: a learning's text
|
|
134
|
+
* comes from whoever wrote it, and a line of its own could pass for trail's.
|
|
155
135
|
*/
|
|
156
136
|
export declare function formatNoteHits(hits: NoteHit[]): string[];
|
|
157
137
|
/** The line an agent closes its reply with after saving a learning just for the user. */
|
|
158
138
|
export declare const PERSONAL_SAVED_LINE = "\uD83D\uDCDD Saved just for you \u00B7 stays on this machine, out of the repo";
|
|
159
|
-
/** `Anay`, `Anay and Rushil`, `Anay, Rushil and 3 others`. */
|
|
160
|
-
export declare function namesLabel(names: string[]): string;
|
|
161
|
-
/** What starting from a teammate's learning is worth when its tokens are too few to say: time to a PR. */
|
|
162
|
-
export declare const PR_SPEEDUP = "3x faster";
|
|
163
|
-
/**
|
|
164
|
-
* Below this many tokens, what a learning saves is too small a number to
|
|
165
|
-
* impress, so trail credits it as a faster PR instead.
|
|
166
|
-
*/
|
|
167
|
-
export declare const TOKEN_CREDIT_FLOOR = 80000;
|
|
168
|
-
/** Whether a token count is big enough to be the credit. */
|
|
169
|
-
export declare function tokensWorthSaying(tokens: number | undefined): boolean;
|
|
170
|
-
/**
|
|
171
|
-
* The lines that close a reply after a team learning is saved, one for each
|
|
172
|
-
* way the turn can end, since only the agent knows whether it committed the
|
|
173
|
-
* learning by then. Uncommitted, it makes the case for committing: who else
|
|
174
|
-
* works on the same files, and the head start their agents would get, which
|
|
175
|
-
* is what the learning took to work out, or a faster PR when that's small.
|
|
176
|
-
*/
|
|
177
|
-
export declare function commitLines(note: Pick<Note, "cost">, teammates: string[], where?: "files" | "repo"): {
|
|
178
|
-
committed: string;
|
|
179
|
-
uncommitted: string;
|
|
180
|
-
};
|
|
181
|
-
/**
|
|
182
|
-
* The line asking for trail's shared setup to be committed, once: `.trail/`
|
|
183
|
-
* and the SessionStart entry in `.claude/settings.json` are what let a
|
|
184
|
-
* teammate's Claude read the learnings before they install anything.
|
|
185
|
-
*/
|
|
186
|
-
export declare function setupCommitLine(teammates: string[]): string;
|
|
187
139
|
/**
|
|
188
140
|
* What starting from these learnings is worth, for the agent's closing line:
|
|
189
141
|
* the tokens they saved, less what reading them costs, when that's worth
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notes.d.ts","sourceRoot":"","sources":["../../src/notes/notes.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"notes.d.ts","sourceRoot":"","sources":["../../src/notes/notes.ts"],"names":[],"mappings":"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"}
|
package/dist/notes/notes.js
CHANGED
|
@@ -17,13 +17,19 @@
|
|
|
17
17
|
* source files or prompts into it.
|
|
18
18
|
*/
|
|
19
19
|
import { readdirSync, readFileSync, existsSync, writeFileSync } from "node:fs";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import { matter } from "../util/front-matter.js";
|
|
20
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
21
|
+
import { matter, textList, warnSkipped } from "../util/front-matter.js";
|
|
23
22
|
import { readBranchCache } from "./branches.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
23
|
+
import { PR_SPEEDUP, tokensLabel, tokensWorthSaying } from "./credit.js";
|
|
24
|
+
import { flatLine } from "./flatten.js";
|
|
25
|
+
import { ensureRepoHome, notePlaces, shownPath } from "./home.js";
|
|
26
|
+
import { LEARNING_KEYS, learningFileName, liveLearnings, newLearningId, readLearningFields } from "./learning-fields.js";
|
|
26
27
|
import { ensureRepoTrail, LEARNINGS_DIR, learningsDir } from "./repo-trail.js";
|
|
28
|
+
import { TOUCH_CHARS, TOUCH_LIMIT } from "./targets.js";
|
|
29
|
+
// Where a learning goes, which to show for a query and how one is credited live in their own files.
|
|
30
|
+
export { findNotes } from "./find-notes.js";
|
|
31
|
+
export { mentionedFiles, noteTargets } from "./targets.js";
|
|
32
|
+
export { commitLines, namesLabel, PR_SPEEDUP, setupCommitLine, TOKEN_CREDIT_FLOOR, tokensLabel, tokensWorthSaying } from "./credit.js";
|
|
27
33
|
/** The three sections a note is written under. Notes may have others; these are the ones `ask` quotes. */
|
|
28
34
|
export const SECTIONS = [
|
|
29
35
|
{ key: "decided", heading: "Decided", label: "decided" },
|
|
@@ -50,6 +56,10 @@ export function renderNote(n) {
|
|
|
50
56
|
fm.push(`cost: { ${cost.join(", ")} }`);
|
|
51
57
|
if (n.standing)
|
|
52
58
|
fm.push("standing: true");
|
|
59
|
+
// Evidence is free text, always quoted so YAML never reads "12" back as a number.
|
|
60
|
+
for (const k of LEARNING_KEYS)
|
|
61
|
+
if (n[k])
|
|
62
|
+
fm.push(`${k}: ${k === "evidence" ? JSON.stringify(n[k]) : scalar(n[k])}`);
|
|
53
63
|
if (n.touches.length)
|
|
54
64
|
fm.push("touches:", ...n.touches.map((t) => ` - ${scalar(t)}`));
|
|
55
65
|
fm.push("---", "");
|
|
@@ -58,32 +68,47 @@ export function renderNote(n) {
|
|
|
58
68
|
function num(v) {
|
|
59
69
|
return typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
60
70
|
}
|
|
61
|
-
/**
|
|
71
|
+
/** Front matter keys that hold one value: a list or a map under one refuses the file. */
|
|
72
|
+
const NOTE_SCALARS = ["title", "author", "date", "branch"];
|
|
73
|
+
/**
|
|
74
|
+
* A note file, or null when it isn't one (no title in its frontmatter), or
|
|
75
|
+
* when its front matter is refused (anchors, aliases, a list for a title) or
|
|
76
|
+
* isn't valid YAML, which says so once on stderr.
|
|
77
|
+
*/
|
|
62
78
|
export function parseNote(text, path) {
|
|
63
79
|
let parsed;
|
|
64
80
|
try {
|
|
65
|
-
parsed = matter(text);
|
|
81
|
+
parsed = matter(text, { scalars: NOTE_SCALARS });
|
|
66
82
|
}
|
|
67
83
|
catch {
|
|
84
|
+
// The parser's own message quotes the file; the warning names it instead.
|
|
85
|
+
warnSkipped("the learning", path, "its front matter isn't valid YAML");
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (parsed.refused) {
|
|
89
|
+
warnSkipped("the learning", path, parsed.refused);
|
|
68
90
|
return null;
|
|
69
91
|
}
|
|
70
92
|
const d = parsed.data;
|
|
71
|
-
|
|
93
|
+
// Title, author and date reach what agents read: one line each (flatten.ts).
|
|
94
|
+
const title = typeof d.title === "string" ? flatLine(d.title) : "";
|
|
95
|
+
if (!title)
|
|
72
96
|
return null;
|
|
73
|
-
const date = d.date instanceof Date ? d.date.toISOString().slice(0, 10) :
|
|
97
|
+
const date = d.date instanceof Date ? d.date.toISOString().slice(0, 10) : flatLine(d.date, 40);
|
|
74
98
|
const cost = (d.cost ?? {});
|
|
75
99
|
const minutes = num(cost.minutes);
|
|
76
100
|
const tokens = num(cost.tokens);
|
|
77
101
|
return {
|
|
78
102
|
path,
|
|
79
|
-
title
|
|
80
|
-
author: typeof d.author === "string" ? d.author : "",
|
|
103
|
+
title,
|
|
104
|
+
author: typeof d.author === "string" ? flatLine(d.author, 80) : "",
|
|
81
105
|
date,
|
|
82
106
|
branch: typeof d.branch === "string" ? d.branch : undefined,
|
|
83
107
|
cost: minutes !== undefined || tokens !== undefined ? { minutes, tokens } : undefined,
|
|
84
|
-
touches:
|
|
108
|
+
touches: textList(d.touches, TOUCH_LIMIT, TOUCH_CHARS),
|
|
85
109
|
body: parsed.content.trim(),
|
|
86
110
|
...(d.standing === true ? { standing: true } : {}),
|
|
111
|
+
...readLearningFields(d, learningFileName(path)),
|
|
87
112
|
};
|
|
88
113
|
}
|
|
89
114
|
/** A touch saved relative to `checkout`, as a path relative to `here`. */
|
|
@@ -118,7 +143,7 @@ function readNotesIn(notesDir, checkout, here, scope) {
|
|
|
118
143
|
}
|
|
119
144
|
/** The file name a note is known by, wherever it sits: one learning on a branch and in the tree is one learning. */
|
|
120
145
|
export function noteName(n) {
|
|
121
|
-
return
|
|
146
|
+
return learningFileName(n.path);
|
|
122
147
|
}
|
|
123
148
|
/**
|
|
124
149
|
* Every learning that bears on the folder `dir`, newest first: the repo's
|
|
@@ -153,7 +178,7 @@ export function listNotes(dir, opts = {}) {
|
|
|
153
178
|
}
|
|
154
179
|
return out.sort((a, b) => (a.date === b.date ? b.path.localeCompare(a.path) : b.date.localeCompare(a.date)));
|
|
155
180
|
}
|
|
156
|
-
/** The learnings on teammates' pushed branches, from the branch cache. */
|
|
181
|
+
/** The learnings on teammates' pushed branches, from the branch cache. One that won't read is skipped, never the rest. */
|
|
157
182
|
function branchNotes(checkout, here) {
|
|
158
183
|
const cache = readBranchCache(checkout);
|
|
159
184
|
if (!cache)
|
|
@@ -161,9 +186,15 @@ function branchNotes(checkout, here) {
|
|
|
161
186
|
const out = [];
|
|
162
187
|
for (const b of cache.branches) {
|
|
163
188
|
for (const l of b.learnings) {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
189
|
+
const where = `${b.ref}:${LEARNINGS_DIR}/${l.name}`;
|
|
190
|
+
try {
|
|
191
|
+
const n = parseNote(l.text, where);
|
|
192
|
+
if (n)
|
|
193
|
+
out.push({ ...n, scope: "repo", ref: b.ref, touches: n.touches.map((t) => rebaseTouch(t, checkout, here)) });
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
warnSkipped("the learning", where, e instanceof Error ? e.message : String(e));
|
|
197
|
+
}
|
|
167
198
|
}
|
|
168
199
|
}
|
|
169
200
|
return out;
|
|
@@ -198,7 +229,8 @@ export function slug(s, words = 6) {
|
|
|
198
229
|
* Write a new learning for the repo `dir` is in, and return it: into the
|
|
199
230
|
* repo's `.trail/learnings/` (made if needed), or with `scope: "personal"`,
|
|
200
231
|
* or outside git, into ~/.trail. Never overwrites: a second one with the same
|
|
201
|
-
* name on the same day gets `-2`, `-3`.
|
|
232
|
+
* name on the same day gets `-2`, `-3`. Its id is 8 hex digits from the file
|
|
233
|
+
* name it gets (`newLearningId`), written into the file.
|
|
202
234
|
*/
|
|
203
235
|
export function writeNote(dir, input, scope = "repo") {
|
|
204
236
|
const { place } = ensureRepoHome(dir);
|
|
@@ -215,16 +247,17 @@ export function writeNote(dir, input, scope = "repo") {
|
|
|
215
247
|
let name = `${base}.md`;
|
|
216
248
|
for (let i = 2; existsSync(join(notes, name)); i++)
|
|
217
249
|
name = `${base}-${i}.md`;
|
|
250
|
+
// One line each, as every reader would show them (flatten.ts): what's written reads back the same.
|
|
218
251
|
const note = {
|
|
219
|
-
title: input.title
|
|
220
|
-
author: input.author,
|
|
252
|
+
title: flatLine(input.title),
|
|
253
|
+
author: flatLine(input.author, 80),
|
|
221
254
|
date: input.date,
|
|
222
255
|
branch: input.branch,
|
|
223
256
|
cost: input.cost,
|
|
224
257
|
touches: input.touches ?? [],
|
|
225
258
|
body: input.body,
|
|
226
|
-
scope: inRepo ? "repo" : "personal",
|
|
227
259
|
...(input.standing ? { standing: true } : {}),
|
|
260
|
+
...readLearningFields({ ...input, id: newLearningId(name), scope: inRepo ? "repo" : "personal", applies: inRepo ? undefined : (input.applies ?? place.key), status: "active" }, name),
|
|
228
261
|
};
|
|
229
262
|
writeFileSync(join(notes, name), renderNote(note));
|
|
230
263
|
return { ...note, path: join(notes, name) };
|
|
@@ -232,10 +265,10 @@ export function writeNote(dir, input, scope = "repo") {
|
|
|
232
265
|
/**
|
|
233
266
|
* The standing learnings for `dir`: the team's in the checkout and the
|
|
234
267
|
* person's own, never ones on a teammate's unmerged branch, which nobody has
|
|
235
|
-
* reviewed yet.
|
|
268
|
+
* reviewed yet, and only live ones (nothing retired or replaced).
|
|
236
269
|
*/
|
|
237
270
|
export function standingLearnings(dir) {
|
|
238
|
-
return listNotes(dir, { branches: false }).filter((n) => n.standing && !n.ref);
|
|
271
|
+
return liveLearnings(listNotes(dir, { branches: false })).filter((n) => n.standing && !n.ref);
|
|
239
272
|
}
|
|
240
273
|
/** Where a learning is, as the agent and the person read it: `.trail/learnings/x.md`, `~/.trail/…`, or `origin/b:.trail/…`. */
|
|
241
274
|
export function notePlace(n) {
|
|
@@ -246,122 +279,6 @@ export function notePlace(n) {
|
|
|
246
279
|
const at = path.lastIndexOf(`/${LEARNINGS_DIR}/`);
|
|
247
280
|
return at === -1 ? shownPath(n.path) : path.slice(at + 1);
|
|
248
281
|
}
|
|
249
|
-
/** Most files one note lists per repo. */
|
|
250
|
-
const TOUCH_LIMIT = 10;
|
|
251
|
-
/**
|
|
252
|
-
* Which repos a session's note belongs in, and what it touched in each.
|
|
253
|
-
*
|
|
254
|
-
* The repo the note was saved from gets the files it has uncommitted or
|
|
255
|
-
* committed since `since`. Every other repo the session edited (`edited`:
|
|
256
|
-
* absolute paths, from the agent's transcript) gets the note too, with its
|
|
257
|
-
* own files, so a session that changed a backend and the library it calls
|
|
258
|
-
* leaves a note in both. Saved from a folder that holds several repos, the
|
|
259
|
-
* note goes to each repo the session changed, or stays with the folder when
|
|
260
|
-
* it changed none. Two worktrees of one repo are one repo.
|
|
261
|
-
*/
|
|
262
|
-
export function noteTargets(dir, edited = [], since) {
|
|
263
|
-
const here = repoPlace(dir);
|
|
264
|
-
const byKey = new Map();
|
|
265
|
-
const add = (checkout, files) => {
|
|
266
|
-
const key = repoPlace(checkout).key;
|
|
267
|
-
const entry = byKey.get(key) ?? { dir: checkout, files: [] };
|
|
268
|
-
for (const f of files)
|
|
269
|
-
if (f && !ownPath(f) && !entry.files.includes(f))
|
|
270
|
-
entry.files.push(f);
|
|
271
|
-
byKey.set(key, entry);
|
|
272
|
-
};
|
|
273
|
-
if (here.git)
|
|
274
|
-
add(here.checkout, changedFiles(here.checkout, since));
|
|
275
|
-
else if (edited.length === 0)
|
|
276
|
-
for (const c of childCheckouts(here.checkout))
|
|
277
|
-
add(c, changedFiles(c, since));
|
|
278
|
-
// The agent's own files (memory, plans, scratch under ~/.claude) aren't what a note is about.
|
|
279
|
-
const agentFiles = join(homedir(), ".claude") + sep;
|
|
280
|
-
for (const f of edited) {
|
|
281
|
-
if (f.startsWith(agentFiles))
|
|
282
|
-
continue;
|
|
283
|
-
const top = checkoutRoot(dirname(f));
|
|
284
|
-
if (top)
|
|
285
|
-
add(top, [relative(top, f).split(sep).join("/")]);
|
|
286
|
-
else if (!here.git && !relative(here.checkout, f).startsWith(".."))
|
|
287
|
-
add(here.checkout, [relative(here.checkout, f).split(sep).join("/")]);
|
|
288
|
-
}
|
|
289
|
-
const out = [...byKey.entries()]
|
|
290
|
-
.filter(([key, e]) => e.files.length > 0 || key === here.key)
|
|
291
|
-
.map(([, e]) => ({ dir: e.dir, touches: e.files.slice(0, TOUCH_LIMIT) }));
|
|
292
|
-
// The repo it was saved from first; a folder of repos only when nothing else was touched.
|
|
293
|
-
out.sort((a, b) => Number(repoPlace(b.dir).key === here.key) - Number(repoPlace(a.dir).key === here.key));
|
|
294
|
-
const touched = out.filter((t) => t.touches.length > 0);
|
|
295
|
-
if (!here.git && touched.length > 0)
|
|
296
|
-
return touched;
|
|
297
|
-
return out.length ? out : [{ dir: here.checkout, touches: [] }];
|
|
298
|
-
}
|
|
299
|
-
/** A path-shaped word in a note: `client.go`, `src/a/b.ts:12-30`, `x.go#Fn`. */
|
|
300
|
-
const MENTION = /(?:^|[\s(`'"\[])((?:[\w.-]+\/)*[\w-][\w.-]*\.[A-Za-z][A-Za-z0-9]{0,5})(?=$|[\s:#)`'",.;\]])/gm;
|
|
301
|
-
/**
|
|
302
|
-
* Files of the checkout at `dir` that a note's text names by path. A session
|
|
303
|
-
* that only read code edits nothing, so without these its note would touch no
|
|
304
|
-
* file, and nothing that looks a note up by file (`trail check`, the ranked
|
|
305
|
-
* search) would ever find it.
|
|
306
|
-
*/
|
|
307
|
-
export function mentionedFiles(dir, body) {
|
|
308
|
-
const out = [];
|
|
309
|
-
for (const m of body.matchAll(MENTION)) {
|
|
310
|
-
const f = m[1].replace(/^\.\//, "");
|
|
311
|
-
if (out.includes(f) || ownPath(f) || f.startsWith(".."))
|
|
312
|
-
continue;
|
|
313
|
-
if (existsSync(join(dir, f)))
|
|
314
|
-
out.push(f);
|
|
315
|
-
if (out.length >= TOUCH_LIMIT)
|
|
316
|
-
break;
|
|
317
|
-
}
|
|
318
|
-
return out;
|
|
319
|
-
}
|
|
320
|
-
/* -------------------------------------------------------------------------- */
|
|
321
|
-
/* finding the notes that matter for a query */
|
|
322
|
-
/* -------------------------------------------------------------------------- */
|
|
323
|
-
const STOP = new Set("the and for with from that this what when where which into does how why are was were not but you your our out its it's can should would could about after before over under use using used fix find make".split(" "));
|
|
324
|
-
function terms(s) {
|
|
325
|
-
return [...new Set(s.toLowerCase().split(/[^a-z0-9_]+/).filter((t) => t.length >= 3 && !STOP.has(t)))];
|
|
326
|
-
}
|
|
327
|
-
/** The file part of a `file#Symbol` touch. */
|
|
328
|
-
function touchFile(t) {
|
|
329
|
-
return t.split("#")[0];
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
|
-
* The notes worth showing next to a query's results: ones whose title or body
|
|
333
|
-
* share the query's words, or that touch a file the results point into. At
|
|
334
|
-
* most `limit`, best first. A note has to clear a bar — two of the query's
|
|
335
|
-
* words, or one in its title, or a shared file — so an unrelated note never
|
|
336
|
-
* rides along just because it exists.
|
|
337
|
-
*/
|
|
338
|
-
export function findNotes(notes, query, files = [], limit = 2) {
|
|
339
|
-
const q = terms(query);
|
|
340
|
-
const hitFiles = new Set(files);
|
|
341
|
-
const out = [];
|
|
342
|
-
for (const note of notes) {
|
|
343
|
-
const title = new Set(terms(note.title));
|
|
344
|
-
const text = new Set(terms(`${note.body} ${note.touches.join(" ")}`));
|
|
345
|
-
let score = 0;
|
|
346
|
-
let matched = 0;
|
|
347
|
-
for (const t of q) {
|
|
348
|
-
if (title.has(t)) {
|
|
349
|
-
score += 3;
|
|
350
|
-
matched++;
|
|
351
|
-
}
|
|
352
|
-
else if (text.has(t)) {
|
|
353
|
-
score += 1;
|
|
354
|
-
matched++;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
const shared = [...new Set(note.touches.map(touchFile))].filter((f) => hitFiles.has(f));
|
|
358
|
-
score += shared.length * 4;
|
|
359
|
-
const clears = shared.length > 0 || matched >= 2 || [...title].some((t) => q.includes(t));
|
|
360
|
-
if (clears && score >= 3)
|
|
361
|
-
out.push({ note, score, shared });
|
|
362
|
-
}
|
|
363
|
-
return out.sort((a, b) => b.score - a.score || b.note.date.localeCompare(a.note.date)).slice(0, limit);
|
|
364
|
-
}
|
|
365
282
|
/* -------------------------------------------------------------------------- */
|
|
366
283
|
/* how a note reads inside a query's output */
|
|
367
284
|
/* -------------------------------------------------------------------------- */
|
|
@@ -373,12 +290,6 @@ export function shortDate(date) {
|
|
|
373
290
|
return date;
|
|
374
291
|
return `${MONTHS[Number(m[2]) - 1] ?? m[2]} ${Number(m[3])}`;
|
|
375
292
|
}
|
|
376
|
-
/** `~9.6k tokens`, `~600 tokens`. */
|
|
377
|
-
export function tokensLabel(n) {
|
|
378
|
-
if (n >= 1000)
|
|
379
|
-
return `~${(n / 1000).toFixed(n >= 10_000 ? 0 : 1).replace(/\.0$/, "")}k tokens`;
|
|
380
|
-
return `~${n} tokens`;
|
|
381
|
-
}
|
|
382
293
|
/** `took 12 min and ~9.6k tokens to work out`, or null when the note has no cost. */
|
|
383
294
|
export function costLabel(cost) {
|
|
384
295
|
if (!cost)
|
|
@@ -394,9 +305,6 @@ export function costLabel(cost) {
|
|
|
394
305
|
function shellQuote(s) {
|
|
395
306
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
396
307
|
}
|
|
397
|
-
function clip(s, n = 88) {
|
|
398
|
-
return s.length > n ? `${s.slice(0, n - 1).trimEnd()}…` : s;
|
|
399
|
-
}
|
|
400
308
|
/** What reading a note costs an agent, in tokens: its text at ~4 characters a token. */
|
|
401
309
|
export function readingTokens(n) {
|
|
402
310
|
return Math.ceil(renderNote(n).length / 4);
|
|
@@ -409,10 +317,11 @@ export function savedByNote(n) {
|
|
|
409
317
|
const took = n.cost?.tokens ?? 0;
|
|
410
318
|
return Math.max(0, took - readingTokens(n));
|
|
411
319
|
}
|
|
412
|
-
/** `
|
|
320
|
+
/** `Maya's learning`, `a learning`, `Kumar's learning on kumar/jitter`. */
|
|
413
321
|
export function whoseLearning(n) {
|
|
414
|
-
const
|
|
415
|
-
|
|
322
|
+
const author = flatLine(n.author, 80);
|
|
323
|
+
const who = author ? `${author}'s learning` : "a learning";
|
|
324
|
+
return n.ref ? `${who} on ${flatLine(n.ref.replace(/^origin\//, ""), 80)}` : who;
|
|
416
325
|
}
|
|
417
326
|
/**
|
|
418
327
|
* The block `ask` prints above its results: whose learning each is, when,
|
|
@@ -420,103 +329,35 @@ export function whoseLearning(n) {
|
|
|
420
329
|
* where to read the rest. A learning on a teammate's unmerged branch says so,
|
|
421
330
|
* with the `git show` that prints it, since there's no file here to open. An agent
|
|
422
331
|
* relays the cost when it says what the turn saved, which is how the saving
|
|
423
|
-
* becomes countable.
|
|
332
|
+
* becomes countable. Every field is flattened to one line: a learning's text
|
|
333
|
+
* comes from whoever wrote it, and a line of its own could pass for trail's.
|
|
424
334
|
*/
|
|
425
335
|
export function formatNoteHits(hits) {
|
|
426
336
|
const lines = [];
|
|
427
337
|
for (const { note } of hits) {
|
|
428
|
-
const
|
|
429
|
-
|
|
338
|
+
const author = flatLine(note.author, 80);
|
|
339
|
+
const who = `${author ? `${author}'s learning` : "a learning"}${note.teammate ? ", a teammate's" : ""}`;
|
|
340
|
+
lines.push(`from ${who} · ${flatLine(shortDate(note.date), 40)} · ${flatLine(note.title)}`);
|
|
430
341
|
const cost = costLabel(note.cost);
|
|
431
342
|
for (const s of SECTIONS) {
|
|
432
343
|
const lead = sectionLead(note.body, s.heading);
|
|
433
344
|
if (lead)
|
|
434
|
-
lines.push(` ${s.label.padEnd(11)} ${
|
|
345
|
+
lines.push(` ${s.label.padEnd(11)} ${flatLine(lead, 88)}`);
|
|
435
346
|
}
|
|
436
347
|
if (note.ref) {
|
|
437
348
|
// On a teammate's branch, not reviewed yet: where to read the rest, and that it's unmerged.
|
|
438
|
-
|
|
349
|
+
const ref = flatLine(note.ref.replace(/^origin\//, ""), 80);
|
|
350
|
+
lines.push(` on ${ref}, pushed and not merged yet · git show ${shellQuote(flatLine(note.path, 300))}${cost ? ` · ${cost}` : ""}`);
|
|
439
351
|
lines.push("");
|
|
440
352
|
continue;
|
|
441
353
|
}
|
|
442
|
-
lines.push(` ${notePlace(note)}${cost ? ` · ${cost}` : ""}`);
|
|
354
|
+
lines.push(` ${flatLine(notePlace(note), 300)}${cost ? ` · ${cost}` : ""}`);
|
|
443
355
|
lines.push("");
|
|
444
356
|
}
|
|
445
357
|
return lines;
|
|
446
358
|
}
|
|
447
359
|
/** The line an agent closes its reply with after saving a learning just for the user. */
|
|
448
360
|
export const PERSONAL_SAVED_LINE = "📝 Saved just for you · stays on this machine, out of the repo";
|
|
449
|
-
/** `Anay`, `Anay and Rushil`, `Anay, Rushil and 3 others`. */
|
|
450
|
-
export function namesLabel(names) {
|
|
451
|
-
if (names.length <= 2)
|
|
452
|
-
return names.join(" and ");
|
|
453
|
-
if (names.length === 3)
|
|
454
|
-
return `${names[0]}, ${names[1]} and ${names[2]}`;
|
|
455
|
-
return `${names[0]}, ${names[1]} and ${names.length - 2} others`;
|
|
456
|
-
}
|
|
457
|
-
/** `48k`, `3.8k`, `600`. */
|
|
458
|
-
function tokensShort(n) {
|
|
459
|
-
return tokensLabel(n).replace(/^~/, "").replace(" tokens", "");
|
|
460
|
-
}
|
|
461
|
-
/** What starting from a teammate's learning is worth when its tokens are too few to say: time to a PR. */
|
|
462
|
-
export const PR_SPEEDUP = "3x faster";
|
|
463
|
-
/**
|
|
464
|
-
* Below this many tokens, what a learning saves is too small a number to
|
|
465
|
-
* impress, so trail credits it as a faster PR instead.
|
|
466
|
-
*/
|
|
467
|
-
export const TOKEN_CREDIT_FLOOR = 80_000;
|
|
468
|
-
/** Whether a token count is big enough to be the credit. */
|
|
469
|
-
export function tokensWorthSaying(tokens) {
|
|
470
|
-
return (tokens ?? 0) >= TOKEN_CREDIT_FLOOR;
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* The lines that close a reply after a team learning is saved, one for each
|
|
474
|
-
* way the turn can end, since only the agent knows whether it committed the
|
|
475
|
-
* learning by then. Uncommitted, it makes the case for committing: who else
|
|
476
|
-
* works on the same files, and the head start their agents would get, which
|
|
477
|
-
* is what the learning took to work out, or a faster PR when that's small.
|
|
478
|
-
*/
|
|
479
|
-
export function commitLines(note, teammates, where = "files") {
|
|
480
|
-
const tokens = note.cost?.tokens;
|
|
481
|
-
const short = tokensWorthSaying(tokens) ? tokensShort(tokens) : null;
|
|
482
|
-
// "an 80k-token", "an 11k-token": said aloud, they start with a vowel.
|
|
483
|
-
const start = short ? `${/^8|^1[18]k$/.test(short) ? "an" : "a"} ${short}-token head start` : null;
|
|
484
|
-
const place = where === "files" ? "this part of the codebase" : "this repo";
|
|
485
|
-
if (!teammates.length) {
|
|
486
|
-
return start
|
|
487
|
-
? {
|
|
488
|
-
committed: `📝 Committed · your teammates' agents now get ${start}`,
|
|
489
|
-
uncommitted: `🌱 Commit what you learned and give your teammates' agents ${start}`,
|
|
490
|
-
}
|
|
491
|
-
: {
|
|
492
|
-
committed: `📝 Committed · your teammates' agents now get to a PR ${PR_SPEEDUP}`,
|
|
493
|
-
uncommitted: `🌱 Commit what you learned and your teammates' agents get to a PR ${PR_SPEEDUP}`,
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
const who = namesLabel(teammates);
|
|
497
|
-
const work = teammates.length === 1 ? "works" : "work";
|
|
498
|
-
return start
|
|
499
|
-
? {
|
|
500
|
-
committed: `📝 Committed · ${who} ${work} in ${place}, and their agents now get ${start}`,
|
|
501
|
-
uncommitted: `🌱 ${who} ${work} in ${place}. Commit what you learned and give their agents ${start}`,
|
|
502
|
-
}
|
|
503
|
-
: {
|
|
504
|
-
committed: `📝 Committed · ${who} ${work} in ${place}, and their agents now get to a PR ${PR_SPEEDUP}`,
|
|
505
|
-
uncommitted: `🌱 ${who} ${work} in ${place}. Commit what you learned and their agents get to a PR ${PR_SPEEDUP}`,
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* The line asking for trail's shared setup to be committed, once: `.trail/`
|
|
510
|
-
* and the SessionStart entry in `.claude/settings.json` are what let a
|
|
511
|
-
* teammate's Claude read the learnings before they install anything.
|
|
512
|
-
*/
|
|
513
|
-
export function setupCommitLine(teammates) {
|
|
514
|
-
const ask = "Commit .trail/ and .claude/settings.json once";
|
|
515
|
-
if (!teammates.length)
|
|
516
|
-
return `🌱 ${ask}, and your teammates' Claude starts from your learnings, even before they install trail`;
|
|
517
|
-
const work = teammates.length === 1 ? "works" : "work";
|
|
518
|
-
return `🌱 ${namesLabel(teammates)} ${work} in this repo. ${ask}, and their Claude starts from your learnings, even before they install trail`;
|
|
519
|
-
}
|
|
520
361
|
/**
|
|
521
362
|
* What starting from these learnings is worth, for the agent's closing line:
|
|
522
363
|
* the tokens they saved, less what reading them costs, when that's worth
|