@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trailkeep",
3
- "version": "0.3.2",
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 at most one hint line to a session, and none at all with `TRAILKEEP_HINT=0`.
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
- | **Private repo notes**: things you can't commit | `~/.agents/repos/<repo>/NOTES.md` | you | at session start, by a hook |
34
- | **Effort**: this branch's goal, plan, progress, decisions | `.trailkeep/<branch>.md` in the worktree, ignored by git | the agent | at session start and after compaction, by a hook |
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
- | You're on a feature branch with no effort file | One line tells the agent it can run `trailkeep new` for multi-session work. Turn it off with `TRAILKEEP_HINT=0`. |
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, at about 60 always-on tokens.
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 hint on branches without an effort file)
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.3.2"
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: private notes for this repository" "$notes" "$((budget / 3))"
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
- 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, add one dated line under Progress before you stop, and record decisions and dead ends (start a line with "Dead end:" for approaches that failed).\n'
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
- elif [ "${TRAILKEEP_HINT:-1}" != 0 ] && [ -n "$effort" ] && db=$(default_branch) && [ "$b" != "$db" ]; then
252
- printf 'trailkeep: no effort file for branch %s. If this task spans more than one session, run `trailkeep new` and keep it current.\n' "$b"
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
- if (l ~ /^([Dd]ead[ -]end|[Tt]ried |[Rr]ejected|[Rr]everted)/) t = "dead-end"
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
- effort_template "$BRANCH" "$KEY" "$kind" > "$f" || die "could not write $f"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trailkeep/cli",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "description": "Shared instructions, per-branch task context and a decision log for Claude Code, Codex CLI and GitHub Copilot CLI. POSIX sh + git.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -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