@trailkeep/cli 0.3.2 → 0.5.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/.claude-plugin/plugin.json +1 -1
- package/README.md +9 -6
- package/bin/trailkeep +114 -9
- package/package.json +1 -1
- package/skills/effort/SKILL.md +2 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trailkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Injects your private repo notes and the current branch's effort file at session start, plus the effort skill.",
|
|
5
5
|
"author": { "name": "Mario Širić" },
|
|
6
6
|
"repository": "https://github.com/msiric/trailkeep",
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Context for coding agents that works the same in **Claude Code**, **Codex CLI** and **GitHub Copilot CLI**. It keeps three kinds of knowledge apart: what's always true about you, what's always true about a repo, and what matters only for the branch you're on. It also keeps an append-only log of why things were decided.
|
|
4
4
|
|
|
5
|
-
It's a single POSIX shell script that needs only `git`. It never changes your repos' tracked files. In a repo with no notes, effort or log, it adds
|
|
5
|
+
It's a single POSIX shell script that needs only `git`. It never changes your repos' tracked files. In a repo with no notes, effort or log, it adds one short capture rule to a session (about 110 tokens), and nothing at all with `TRAILKEEP_HINT=0`.
|
|
6
6
|
|
|
7
7
|
## Install (one command, once per machine)
|
|
8
8
|
|
|
@@ -30,8 +30,9 @@ Later, `trailkeep update` gets the latest version the same way you installed it
|
|
|
30
30
|
|---|---|---|---|
|
|
31
31
|
| **Personal**: you, in every repo | `~/.agents/AGENTS.md` | you | every session, by all three agents |
|
|
32
32
|
| **Repo**: true for every task here | the repo's own `AGENTS.md` (committed) | you and your team | natively by all three agents |
|
|
33
|
-
| **
|
|
34
|
-
| **
|
|
33
|
+
| **Personal notes**: preferences the agent picked up | `~/.agents/NOTES.md` | the agent (`trailkeep note --all`) | at session start in every repo, by a hook |
|
|
34
|
+
| **Private repo notes**: things you can't commit | `~/.agents/repos/<repo>/NOTES.md` | you, or the agent (`trailkeep note --repo`) | at session start, by a hook |
|
|
35
|
+
| **Effort**: this branch's goal, plan, progress, decisions | `.trailkeep/<branch>.md` in the worktree, ignored by git | the agent (`trailkeep note` adds a decision, creating the file if needed) | at session start and after compaction, by a hook |
|
|
35
36
|
| **Decision log**: why things happened, append-only | `~/.agents/repos/<repo>/log/YYYY-MM.tsv` | the session-end hook | never loaded; agents search it |
|
|
36
37
|
|
|
37
38
|
`<repo>` comes from the origin URL, so every clone and worktree of a repo shares the same notes and log.
|
|
@@ -41,7 +42,8 @@ Later, `trailkeep update` gets the latest version the same way you installed it
|
|
|
41
42
|
| When | What happens automatically |
|
|
42
43
|
|---|---|
|
|
43
44
|
| Any session starts in any repo | Notes and the branch's effort file are injected. A one-line pointer mentions the decision log if it has entries. Leftovers from a session that didn't exit cleanly are logged first. |
|
|
44
|
-
|
|
|
45
|
+
| Any session starts in a repo | A short capture rule tells the agent to record decisions, user rules and dead ends the moment they come up, with `trailkeep note`, and to pick the scope: this branch, the whole repo (`--repo`) or every repo (`--all`). It doesn't wait for anyone to decide the task "spans sessions". Turn it off with `TRAILKEEP_HINT=0`. |
|
|
46
|
+
| You're on a branch with no notes yet | The last few decisions recorded on other branches are shown, each labelled with its branch, so repo-wide rules saved on one branch aren't lost on the next. Once the branch has its own notes, only those are shown. |
|
|
45
47
|
| The agent works | It keeps the effort file current: plan ticks, a progress line per session, decisions and dead ends. That's the effort skill's job. |
|
|
46
48
|
| Context gets compacted | The effort file is re-injected fresh. |
|
|
47
49
|
| A session ends | New Decisions and Progress lines from every effort in the worktree go into the decision log, stamped with the commit, the branch's changed files, branch and session id. It takes under 0.1 s, well inside Codex's 3-second limit for this hook. Anything a killed session missed is picked up at the next start, in the background. |
|
|
@@ -76,6 +78,7 @@ The format is one event per line, tab-separated, one file per month: `id time ty
|
|
|
76
78
|
|
|
77
79
|
```sh
|
|
78
80
|
trailkeep new # start an effort for this branch (or ask the agent to)
|
|
81
|
+
trailkeep note "..." # record a decision or dead end for this branch (--repo: whole repo, --all: every repo)
|
|
79
82
|
trailkeep show # exactly what agents get here at session start
|
|
80
83
|
trailkeep list # active efforts across all repos, with their last progress line
|
|
81
84
|
trailkeep close # archive the effort; lists decisions worth promoting
|
|
@@ -103,7 +106,7 @@ Files it edits are backed up to `~/.agents/backups/` first.
|
|
|
103
106
|
- `~/.claude/CLAUDE.md` gets a 3-line block importing `~/.agents/AGENTS.md`; your own lines stay. HTML comments are stripped by Claude, so the markers cost nothing.
|
|
104
107
|
- `~/.codex/AGENTS.md` and `~/.copilot/copilot-instructions.md` become symlinks to `~/.agents/AGENTS.md`. Existing files with content are left alone, with a warning.
|
|
105
108
|
- Hooks go in through a Claude Code plugin (installed from the clone), `~/.codex/hooks.json` and `~/.copilot/hooks/trailkeep.json`. Hook files it didn't create are never edited; it prints what to add instead.
|
|
106
|
-
- The `effort` skill is linked into `~/.agents/skills/`, which Codex and Copilot read. Claude Code gets it from the plugin
|
|
109
|
+
- The `effort` skill is linked into `~/.agents/skills/`, which Codex and Copilot read. Claude Code gets it from the plugin; its description costs about 75 tokens per session (measured, see `bench/results/stage0-injection.md`).
|
|
107
110
|
- A copy of the tool goes to `~/.agents/trailkeep/`, and `trailkeep` is linked into `~/.local/bin`.
|
|
108
111
|
|
|
109
112
|
`trailkeep uninstall` removes all of that. It leaves each agent a plain copy of your personal instructions, and keeps `~/.agents` and any `.trailkeep/` folders.
|
|
@@ -131,7 +134,7 @@ Live-session behavior can't be tested offline: hooks running, the Claude import
|
|
|
131
134
|
|
|
132
135
|
- `TRAILKEEP_HOME` (default `~/.agents`)
|
|
133
136
|
- `TRAILKEEP_MAX_CHARS` (default 9000, max 9500)
|
|
134
|
-
- `TRAILKEEP_HINT=0` (no
|
|
137
|
+
- `TRAILKEEP_HINT=0` (no capture rule at session start)
|
|
135
138
|
- `TRAILKEEP_EFFORT` (use a named effort instead of the branch)
|
|
136
139
|
|
|
137
140
|
It also honours `CLAUDE_CONFIG_DIR`, `CODEX_HOME` and `COPILOT_HOME`. It works on Linux and macOS; on Windows, use WSL.
|
package/bin/trailkeep
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
set -u
|
|
7
7
|
|
|
8
|
-
VERSION="0.
|
|
8
|
+
VERSION="0.5.0"
|
|
9
9
|
PKG="@trailkeep/cli"
|
|
10
10
|
MARKER="managed by trailkeep"
|
|
11
11
|
|
|
@@ -28,6 +28,7 @@ ROOT=$(cd "$(dirname "$SELF")/.." && pwd -P)
|
|
|
28
28
|
|
|
29
29
|
TRAILKEEP_HOME=${TRAILKEEP_HOME:-$HOME/.agents}
|
|
30
30
|
PERSONAL=$TRAILKEEP_HOME/AGENTS.md
|
|
31
|
+
GLOBAL_NOTES=$TRAILKEEP_HOME/NOTES.md # `note --all`: agent-written, injected everywhere
|
|
31
32
|
REPOS=$TRAILKEEP_HOME/repos
|
|
32
33
|
SKILLS_DIR=$HOME/.agents/skills # read by Codex and Copilot
|
|
33
34
|
BIN_DIR=${TRAILKEEP_BIN_DIR:-$HOME/.local/bin}
|
|
@@ -226,6 +227,27 @@ default_branch() {
|
|
|
226
227
|
return 1
|
|
227
228
|
}
|
|
228
229
|
|
|
230
|
+
# One standing instruction, so capture doesn't depend on the agent deciding a
|
|
231
|
+
# task "spans sessions" (it can't know). Measured cost: about 110 tokens.
|
|
232
|
+
capture_rule() {
|
|
233
|
+
printf '%s\n' 'trailkeep: when something comes up that a later session must not lose (a decision and its reason, a rule or preference the user states, an approach that failed and why), record it right away with its reason. Pick the scope: `trailkeep note --repo "..."` for a rule about this codebase that holds on every branch, `trailkeep note "..."` for something specific to this branch'"'"'s task, `trailkeep note --all "..."` for a personal preference in every repo. Notes are shown at the start of the next session.'
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Decisions recorded on other branches, newest first: effort files in this
|
|
237
|
+
# worktree (their latest lines may not be in the log yet), then the log.
|
|
238
|
+
other_decisions() { # CURRENT_BRANCH KEY -> up to 5 lines "- (branch) text"
|
|
239
|
+
{
|
|
240
|
+
for f in $(ls -t "$(effort_dir)"/*.md 2>/dev/null); do
|
|
241
|
+
br=$(effort_branch "$f"); [ -n "$br" ] && [ "$br" != "$1" ] || continue
|
|
242
|
+
section "$f" "Decisions and dead ends" |
|
|
243
|
+
awk -v br="$br" -v tab="$TAB" 'NF { l = $0; sub(/^[[:space:]]*[-*][[:space:]]*/, "", l); a[++n] = br tab l } END { for (i = n; i > 0; i--) print a[i] }'
|
|
244
|
+
done
|
|
245
|
+
log_events "$2" | awk -F "$TAB" -v OFS="$TAB" -v b="$1" '($3 == "decision" || $3 == "dead-end") && $4 != b { print $4, $7 }'
|
|
246
|
+
} | awk -F "$TAB" '!seen[$2]++ && $2 !~ /^\[[ xX]\]/ {
|
|
247
|
+
t = $2; if (length(t) > 240) t = substr(t, 1, 237) "..."
|
|
248
|
+
printf "- (%s) %s\n", $1, t; if (++c >= 5) exit }'
|
|
249
|
+
}
|
|
250
|
+
|
|
229
251
|
build_context() {
|
|
230
252
|
in_repo || return 0
|
|
231
253
|
KEY=$(repo_key) || return 0
|
|
@@ -238,18 +260,34 @@ build_context() {
|
|
|
238
260
|
[ -n "$effort" ] && [ -s "$effort" ] && has_effort=1
|
|
239
261
|
|
|
240
262
|
budget=$MAX_CHARS
|
|
263
|
+
if [ -s "$GLOBAL_NOTES" ]; then
|
|
264
|
+
piece "trailkeep: your preferences for every repo; apply them unless the user says otherwise" "$GLOBAL_NOTES" "$((budget / 4))"
|
|
265
|
+
budget=$((budget - $(chars "$GLOBAL_NOTES")))
|
|
266
|
+
[ "$budget" -lt $((MAX_CHARS / 2)) ] && budget=$((MAX_CHARS / 2))
|
|
267
|
+
printf '\n'
|
|
268
|
+
fi
|
|
241
269
|
if [ "$has_notes" -eq 1 ]; then
|
|
242
|
-
piece "trailkeep:
|
|
270
|
+
piece "trailkeep: rules and notes for this repository; follow them unless the user says otherwise" "$notes" "$((budget / 3))"
|
|
243
271
|
budget=$((budget - $(chars "$notes")))
|
|
244
272
|
[ "$budget" -lt $((MAX_CHARS / 2)) ] && budget=$((MAX_CHARS / 2))
|
|
245
273
|
printf '\n'
|
|
246
274
|
fi
|
|
247
|
-
if [ "$has_effort" -eq 1 ]; then
|
|
248
|
-
|
|
275
|
+
if [ "$has_effort" -eq 1 ] && ! grep -q '^## Plan' "$effort"; then
|
|
276
|
+
piece "trailkeep: decisions recorded for branch $b; follow them unless the user says otherwise" "$effort" "$budget"
|
|
277
|
+
printf '\n'
|
|
278
|
+
elif [ "$has_effort" -eq 1 ]; then
|
|
279
|
+
printf 'trailkeep: this branch has an effort file. It is the source of truth for the current task. Keep it current as you work: tick Plan items and add one dated line under Progress before you stop.\n'
|
|
249
280
|
piece "trailkeep: effort file for branch $b" "$effort" "$budget"
|
|
250
281
|
printf '\n'
|
|
251
|
-
|
|
252
|
-
|
|
282
|
+
fi
|
|
283
|
+
if [ "$has_effort" -eq 0 ] && [ -n "$effort" ]; then
|
|
284
|
+
od=$(other_decisions "$b" "$KEY" 2>/dev/null)
|
|
285
|
+
if [ -n "$od" ]; then
|
|
286
|
+
printf 'trailkeep: branch %s has no notes yet. Recent decisions from other branches of this repo, newest first. Rules about the codebase as a whole probably hold here too; ones tied to another branch'"'"'s task may not:\n%s\n\n' "$b" "$od"
|
|
287
|
+
fi
|
|
288
|
+
fi
|
|
289
|
+
if [ "${TRAILKEEP_HINT:-1}" != 0 ] && [ -n "$effort" ]; then
|
|
290
|
+
capture_rule
|
|
253
291
|
fi
|
|
254
292
|
n=0
|
|
255
293
|
for f in "$(log_dir "$KEY")"/*.tsv; do [ -f "$f" ] && n=$((n + $(lines "$f"))); done
|
|
@@ -391,7 +429,8 @@ harvest_file() { # FILE BRANCH SESSION TRANSCRIPT
|
|
|
391
429
|
l = $0; sub(/^[[:space:]]*[-*][[:space:]]*/, "", l); gsub(/[\t\r]/, " ", l); sub(/[[:space:]]+$/, "", l)
|
|
392
430
|
if (l == "" || l ~ /^\[[ xX]\]/ || (l in seen)) next
|
|
393
431
|
seen[l] = 1; t = sec
|
|
394
|
-
|
|
432
|
+
d = l; sub(/^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]:?[[:space:]]*/, "", d)
|
|
433
|
+
if (d ~ /^([Dd]ead[ -]end|[Tt]ried |[Rr]ejected|[Rr]everted)/) t = "dead-end"
|
|
395
434
|
print t tab l
|
|
396
435
|
}' "$1")
|
|
397
436
|
[ -n "$new" ] || return 0
|
|
@@ -558,18 +597,80 @@ EOF
|
|
|
558
597
|
cmd_new() {
|
|
559
598
|
need_repo; need_branch
|
|
560
599
|
f=$(effort_path "$KEY" "$BRANCH")
|
|
561
|
-
if [ -e "$f" ]; then
|
|
600
|
+
if [ -e "$f" ] && grep -q '^## Plan' "$f"; then
|
|
562
601
|
say "Effort already exists: $f"
|
|
563
602
|
return 0
|
|
564
603
|
fi
|
|
565
604
|
kind=branch; [ -n "${TRAILKEEP_EFFORT:-}" ] && kind=named
|
|
566
605
|
ensure_effort_dir
|
|
567
|
-
|
|
606
|
+
if [ -e "$f" ]; then
|
|
607
|
+
# a notes-only file from `trailkeep note`: add the task sections, keep the notes
|
|
608
|
+
kept=$(section "$f" "Decisions and dead ends" | awk 'NF')
|
|
609
|
+
effort_template "$BRANCH" "$KEY" "$kind" > "$f" || die "could not write $f"
|
|
610
|
+
[ -n "$kept" ] && printf '%s\n' "$kept" | while IFS= read -r l; do append_to_section "$f" "Decisions and dead ends" "$l"; done
|
|
611
|
+
else
|
|
612
|
+
effort_template "$BRANCH" "$KEY" "$kind" > "$f" || die "could not write $f"
|
|
613
|
+
fi
|
|
568
614
|
remember_repo
|
|
569
615
|
say "Created $f"
|
|
570
616
|
say "Fill in Goal, Done when and Plan (or ask your agent to, with the effort skill)."
|
|
571
617
|
}
|
|
572
618
|
|
|
619
|
+
# Append LINE at the end of a "## HEADING" section (adds the section if missing).
|
|
620
|
+
append_to_section() { # FILE HEADING LINE
|
|
621
|
+
tmp=$(mktemp) || return 1
|
|
622
|
+
awk -v h="## $2" -v add="$3" '
|
|
623
|
+
function flush( i) { for (i = 1; i <= n; i++) print buf[i]; print add; print ""; on = 0; done = 1; n = 0 }
|
|
624
|
+
$0 == h { print; print ""; on = 1; n = 0; blank = 0; next }
|
|
625
|
+
on && /^## / { flush() }
|
|
626
|
+
on { if (NF) { if (n) for (i = 0; i < blank; i++) buf[++n] = ""; blank = 0; buf[++n] = $0 } else blank++; next }
|
|
627
|
+
{ print }
|
|
628
|
+
END { if (on) flush(); else if (!done) { print ""; print h; print ""; print add } }' "$1" > "$tmp" &&
|
|
629
|
+
cat "$tmp" > "$1"
|
|
630
|
+
st=$?; rm -f "$tmp"; return $st
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
cmd_note() {
|
|
634
|
+
scope=branch
|
|
635
|
+
case ${1:-} in
|
|
636
|
+
--repo) scope=repo; shift ;;
|
|
637
|
+
--all) scope=all; shift ;;
|
|
638
|
+
-h|--help|help|"") die 'usage: trailkeep note [--repo|--all] "what and why"' ;;
|
|
639
|
+
esac
|
|
640
|
+
[ $# -ge 1 ] || die 'usage: trailkeep note [--repo|--all] "what and why"'
|
|
641
|
+
# drop a date the agent wrote itself; the note gets today's date below
|
|
642
|
+
text=$(printf '%s' "$*" | one_line | sed 's/^[-* ]*[0-9]\{4\}-[0-9][0-9]-[0-9][0-9]:\{0,1\}[[:space:]]*//')
|
|
643
|
+
[ -n "$text" ] || die "note: empty text"
|
|
644
|
+
case $scope in
|
|
645
|
+
branch)
|
|
646
|
+
need_repo; need_branch
|
|
647
|
+
f=$(effort_path "$KEY" "$BRANCH")
|
|
648
|
+
if [ ! -e "$f" ]; then
|
|
649
|
+
# Notes only: no empty Goal/Plan to inject; `trailkeep new` adds them.
|
|
650
|
+
kind=branch; [ -n "${TRAILKEEP_EFFORT:-}" ] && kind=named
|
|
651
|
+
ensure_effort_dir
|
|
652
|
+
printf '# %s\n\nBranch: %s · Repo: %s · Kind: %s · Created: %s\n\n## Decisions and dead ends\n\n' \
|
|
653
|
+
"$BRANCH" "$BRANCH" "$KEY" "$kind" "$(today)" > "$f" || die "could not write $f"
|
|
654
|
+
remember_repo
|
|
655
|
+
fi
|
|
656
|
+
append_to_section "$f" "Decisions and dead ends" "- $(today): $text" || die "could not write $f"
|
|
657
|
+
say "Noted for branch $BRANCH ($f)." ;;
|
|
658
|
+
repo)
|
|
659
|
+
need_repo
|
|
660
|
+
f=$(notes_path "$KEY")
|
|
661
|
+
{ mkdir -p "$(dirname "$f")" && { [ -e "$f" ] || printf '# Private notes: %s\n\n' "$KEY" > "$f"; } &&
|
|
662
|
+
printf -- '- %s: %s\n' "$(today)" "$text" >> "$f"; } 2>/dev/null ||
|
|
663
|
+
die "could not write $f (a sandbox may block it; use a branch note instead)"
|
|
664
|
+
remember_repo
|
|
665
|
+
say "Noted for this repo ($(tilde "$f"))." ;;
|
|
666
|
+
all)
|
|
667
|
+
# Not ~/.agents/AGENTS.md: that file is yours, and agents only propose changes to it.
|
|
668
|
+
{ mkdir -p "$(dirname "$GLOBAL_NOTES")" && printf -- '- %s: %s\n' "$(today)" "$text" >> "$GLOBAL_NOTES"; } 2>/dev/null ||
|
|
669
|
+
die "could not write $GLOBAL_NOTES (a sandbox may block it; ask the user to add it)"
|
|
670
|
+
say "Noted for every repo ($(tilde "$GLOBAL_NOTES"))." ;;
|
|
671
|
+
esac
|
|
672
|
+
}
|
|
673
|
+
|
|
573
674
|
cmd_path() {
|
|
574
675
|
need_repo
|
|
575
676
|
case ${1:-} in
|
|
@@ -1412,6 +1513,9 @@ Setup
|
|
|
1412
1513
|
|
|
1413
1514
|
Per repo / branch (run inside a git repo)
|
|
1414
1515
|
new create the effort file for the current branch
|
|
1516
|
+
note [--repo|--all] TEXT
|
|
1517
|
+
record a decision, rule or dead end: for this branch (default),
|
|
1518
|
+
the whole repo, or every repo
|
|
1415
1519
|
show print what agents get at session start here
|
|
1416
1520
|
edit open the effort file in \$EDITOR
|
|
1417
1521
|
path [--notes|--repo|--dir]
|
|
@@ -1439,6 +1543,7 @@ case $cmd in
|
|
|
1439
1543
|
doctor) cmd_doctor "$@" ;;
|
|
1440
1544
|
verify) cmd_verify "$@" ;;
|
|
1441
1545
|
new) cmd_new "$@" ;;
|
|
1546
|
+
note) cmd_note "$@" ;;
|
|
1442
1547
|
show) cmd_show "$@" ;;
|
|
1443
1548
|
edit) cmd_edit "$@" ;;
|
|
1444
1549
|
path) cmd_path "$@" ;;
|
package/package.json
CHANGED
package/skills/effort/SKILL.md
CHANGED
|
@@ -25,7 +25,8 @@ directly with your file tools.
|
|
|
25
25
|
- Add one line under **Progress** per session: `YYYY-MM-DD: what changed, what's next`.
|
|
26
26
|
- Record decisions with their reason under **Decisions and dead ends**, one
|
|
27
27
|
line each. Start a line with `Dead end:` for an approach that failed, and say
|
|
28
|
-
why, so no later session retries it.
|
|
28
|
+
why, so no later session retries it. `trailkeep note "<what and why>"` does
|
|
29
|
+
this in one step, and works before an effort file exists.
|
|
29
30
|
- Keep the file under about 150 lines: fold old Progress lines into one summary line.
|
|
30
31
|
|
|
31
32
|
Every new Decisions and Progress line is copied to the repo's append-only
|