@trailkeep/cli 0.3.1 → 0.4.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.1",
3
+ "version": "0.4.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
 
@@ -10,7 +10,7 @@ It's a single POSIX shell script that needs only `git`. It never changes your re
10
10
  npm install -g @trailkeep/cli && trailkeep install
11
11
  ```
12
12
 
13
- Or without npm:
13
+ Or without npm, or if your npm goes through a company mirror that doesn't carry the package (a 404 from a feed proxy):
14
14
 
15
15
  ```sh
16
16
  git clone https://github.com/msiric/trailkeep ~/.trailkeep && ~/.trailkeep/bin/trailkeep install
@@ -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,7 @@ 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`. It doesn't wait for anyone to decide the task "spans sessions". Turn it off with `TRAILKEEP_HINT=0`. |
45
46
  | 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
47
  | Context gets compacted | The effort file is re-injected fresh. |
47
48
  | 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 +77,7 @@ The format is one event per line, tab-separated, one file per month: `id time ty
76
77
 
77
78
  ```sh
78
79
  trailkeep new # start an effort for this branch (or ask the agent to)
80
+ trailkeep note "..." # record a decision or dead end for this branch (--repo: whole repo, --all: every repo)
79
81
  trailkeep show # exactly what agents get here at session start
80
82
  trailkeep list # active efforts across all repos, with their last progress line
81
83
  trailkeep close # archive the effort; lists decisions worth promoting
@@ -103,7 +105,7 @@ Files it edits are backed up to `~/.agents/backups/` first.
103
105
  - `~/.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
106
  - `~/.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
107
  - 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.
108
+ - 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
109
  - A copy of the tool goes to `~/.agents/trailkeep/`, and `trailkeep` is linked into `~/.local/bin`.
108
110
 
109
111
  `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 +133,7 @@ Live-session behavior can't be tested offline: hooks running, the Claude import
131
133
 
132
134
  - `TRAILKEEP_HOME` (default `~/.agents`)
133
135
  - `TRAILKEEP_MAX_CHARS` (default 9000, max 9500)
134
- - `TRAILKEEP_HINT=0` (no hint on branches without an effort file)
136
+ - `TRAILKEEP_HINT=0` (no capture rule at session start)
135
137
  - `TRAILKEEP_EFFORT` (use a named effort instead of the branch)
136
138
 
137
139
  It also honours `CLAUDE_CONFIG_DIR`, `CODEX_HOME` and `COPILOT_HOME`. It works on Linux and macOS; on Windows, use WSL.
@@ -148,6 +150,8 @@ The workflow bumps the version in all three places, runs the tests, and only the
148
150
 
149
151
  From a clone, `sh scripts/release patch` (or `X.Y.Z`) does the bump, tests, commit and tag locally and pushes; the tag starts the same workflow, which checks the versions, tests again and publishes. Either way, no machine but GitHub's talks to npm.
150
152
 
151
- npm authentication works one of two ways:
153
+ npm authentication:
152
154
  - **Trusted publishing (preferred):** on npmjs.com, open the package's Settings, then Trusted Publisher, then GitHub Actions. Enter owner `msiric`, repository `trailkeep`, workflow `release.yml`. No secret is needed. npm only offers this for packages that already exist.
153
- - **An `NPM_TOKEN` repository secret,** for the first release only: a granular token with read-and-write access to packages and a short expiry. Delete it once trusted publishing is set up.
155
+ - **An `NPM_TOKEN` repository secret** was used for the very first publish only. The workflow no longer reads it; trusted publishing is set up for `@trailkeep/cli`.
156
+
157
+ If a publish fails after the release commit and tag were pushed, run the workflow again with that exact version (`-f version=X.Y.Z`); it publishes the existing tag without bumping.
package/bin/trailkeep CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  set -u
7
7
 
8
- VERSION="0.3.1"
8
+ VERSION="0.4.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,12 @@ 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: `trailkeep note "<what and why>"`. It is kept for this branch and shown at the start of the next session. Use `trailkeep note --repo` for a fact about the whole repo, or `--all` for a personal preference that applies in every repo.'
234
+ }
235
+
229
236
  build_context() {
230
237
  in_repo || return 0
231
238
  KEY=$(repo_key) || return 0
@@ -238,18 +245,28 @@ build_context() {
238
245
  [ -n "$effort" ] && [ -s "$effort" ] && has_effort=1
239
246
 
240
247
  budget=$MAX_CHARS
248
+ if [ -s "$GLOBAL_NOTES" ]; then
249
+ piece "trailkeep: your notes for every repo" "$GLOBAL_NOTES" "$((budget / 4))"
250
+ budget=$((budget - $(chars "$GLOBAL_NOTES")))
251
+ [ "$budget" -lt $((MAX_CHARS / 2)) ] && budget=$((MAX_CHARS / 2))
252
+ printf '\n'
253
+ fi
241
254
  if [ "$has_notes" -eq 1 ]; then
242
255
  piece "trailkeep: private notes for this repository" "$notes" "$((budget / 3))"
243
256
  budget=$((budget - $(chars "$notes")))
244
257
  [ "$budget" -lt $((MAX_CHARS / 2)) ] && budget=$((MAX_CHARS / 2))
245
258
  printf '\n'
246
259
  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'
260
+ if [ "$has_effort" -eq 1 ] && ! grep -q '^## Plan' "$effort"; then
261
+ piece "trailkeep: notes for branch $b" "$effort" "$budget"
262
+ printf '\n'
263
+ elif [ "$has_effort" -eq 1 ]; then
264
+ 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
265
  piece "trailkeep: effort file for branch $b" "$effort" "$budget"
250
266
  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"
267
+ fi
268
+ if [ "${TRAILKEEP_HINT:-1}" != 0 ] && [ -n "$effort" ]; then
269
+ capture_rule
253
270
  fi
254
271
  n=0
255
272
  for f in "$(log_dir "$KEY")"/*.tsv; do [ -f "$f" ] && n=$((n + $(lines "$f"))); done
@@ -391,7 +408,8 @@ harvest_file() { # FILE BRANCH SESSION TRANSCRIPT
391
408
  l = $0; sub(/^[[:space:]]*[-*][[:space:]]*/, "", l); gsub(/[\t\r]/, " ", l); sub(/[[:space:]]+$/, "", l)
392
409
  if (l == "" || l ~ /^\[[ xX]\]/ || (l in seen)) next
393
410
  seen[l] = 1; t = sec
394
- if (l ~ /^([Dd]ead[ -]end|[Tt]ried |[Rr]ejected|[Rr]everted)/) t = "dead-end"
411
+ d = l; sub(/^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]:?[[:space:]]*/, "", d)
412
+ if (d ~ /^([Dd]ead[ -]end|[Tt]ried |[Rr]ejected|[Rr]everted)/) t = "dead-end"
395
413
  print t tab l
396
414
  }' "$1")
397
415
  [ -n "$new" ] || return 0
@@ -558,18 +576,79 @@ EOF
558
576
  cmd_new() {
559
577
  need_repo; need_branch
560
578
  f=$(effort_path "$KEY" "$BRANCH")
561
- if [ -e "$f" ]; then
579
+ if [ -e "$f" ] && grep -q '^## Plan' "$f"; then
562
580
  say "Effort already exists: $f"
563
581
  return 0
564
582
  fi
565
583
  kind=branch; [ -n "${TRAILKEEP_EFFORT:-}" ] && kind=named
566
584
  ensure_effort_dir
567
- effort_template "$BRANCH" "$KEY" "$kind" > "$f" || die "could not write $f"
585
+ if [ -e "$f" ]; then
586
+ # a notes-only file from `trailkeep note`: add the task sections, keep the notes
587
+ kept=$(section "$f" "Decisions and dead ends" | awk 'NF')
588
+ effort_template "$BRANCH" "$KEY" "$kind" > "$f" || die "could not write $f"
589
+ [ -n "$kept" ] && printf '%s\n' "$kept" | while IFS= read -r l; do append_to_section "$f" "Decisions and dead ends" "$l"; done
590
+ else
591
+ effort_template "$BRANCH" "$KEY" "$kind" > "$f" || die "could not write $f"
592
+ fi
568
593
  remember_repo
569
594
  say "Created $f"
570
595
  say "Fill in Goal, Done when and Plan (or ask your agent to, with the effort skill)."
571
596
  }
572
597
 
598
+ # Append LINE at the end of a "## HEADING" section (adds the section if missing).
599
+ append_to_section() { # FILE HEADING LINE
600
+ tmp=$(mktemp) || return 1
601
+ awk -v h="## $2" -v add="$3" '
602
+ function flush( i) { for (i = 1; i <= n; i++) print buf[i]; print add; print ""; on = 0; done = 1; n = 0 }
603
+ $0 == h { print; print ""; on = 1; n = 0; blank = 0; next }
604
+ on && /^## / { flush() }
605
+ on { if (NF) { if (n) for (i = 0; i < blank; i++) buf[++n] = ""; blank = 0; buf[++n] = $0 } else blank++; next }
606
+ { print }
607
+ END { if (on) flush(); else if (!done) { print ""; print h; print ""; print add } }' "$1" > "$tmp" &&
608
+ cat "$tmp" > "$1"
609
+ st=$?; rm -f "$tmp"; return $st
610
+ }
611
+
612
+ cmd_note() {
613
+ scope=branch
614
+ case ${1:-} in
615
+ --repo) scope=repo; shift ;;
616
+ --all) scope=all; shift ;;
617
+ -h|--help|help|"") die 'usage: trailkeep note [--repo|--all] "what and why"' ;;
618
+ esac
619
+ [ $# -ge 1 ] || die 'usage: trailkeep note [--repo|--all] "what and why"'
620
+ text=$(printf '%s' "$*" | one_line)
621
+ [ -n "$text" ] || die "note: empty text"
622
+ case $scope in
623
+ branch)
624
+ need_repo; need_branch
625
+ f=$(effort_path "$KEY" "$BRANCH")
626
+ if [ ! -e "$f" ]; then
627
+ # Notes only: no empty Goal/Plan to inject; `trailkeep new` adds them.
628
+ kind=branch; [ -n "${TRAILKEEP_EFFORT:-}" ] && kind=named
629
+ ensure_effort_dir
630
+ printf '# %s\n\nBranch: %s · Repo: %s · Kind: %s · Created: %s\n\n## Decisions and dead ends\n\n' \
631
+ "$BRANCH" "$BRANCH" "$KEY" "$kind" "$(today)" > "$f" || die "could not write $f"
632
+ remember_repo
633
+ fi
634
+ append_to_section "$f" "Decisions and dead ends" "- $(today): $text" || die "could not write $f"
635
+ say "Noted for branch $BRANCH ($f)." ;;
636
+ repo)
637
+ need_repo
638
+ f=$(notes_path "$KEY")
639
+ { mkdir -p "$(dirname "$f")" && { [ -e "$f" ] || printf '# Private notes: %s\n\n' "$KEY" > "$f"; } &&
640
+ printf -- '- %s: %s\n' "$(today)" "$text" >> "$f"; } 2>/dev/null ||
641
+ die "could not write $f (a sandbox may block it; use a branch note instead)"
642
+ remember_repo
643
+ say "Noted for this repo ($(tilde "$f"))." ;;
644
+ all)
645
+ # Not ~/.agents/AGENTS.md: that file is yours, and agents only propose changes to it.
646
+ { mkdir -p "$(dirname "$GLOBAL_NOTES")" && printf -- '- %s: %s\n' "$(today)" "$text" >> "$GLOBAL_NOTES"; } 2>/dev/null ||
647
+ die "could not write $GLOBAL_NOTES (a sandbox may block it; ask the user to add it)"
648
+ say "Noted for every repo ($(tilde "$GLOBAL_NOTES"))." ;;
649
+ esac
650
+ }
651
+
573
652
  cmd_path() {
574
653
  need_repo
575
654
  case ${1:-} in
@@ -1412,6 +1491,9 @@ Setup
1412
1491
 
1413
1492
  Per repo / branch (run inside a git repo)
1414
1493
  new create the effort file for the current branch
1494
+ note [--repo|--all] TEXT
1495
+ record a decision, rule or dead end: for this branch (default),
1496
+ the whole repo, or every repo
1415
1497
  show print what agents get at session start here
1416
1498
  edit open the effort file in \$EDITOR
1417
1499
  path [--notes|--repo|--dir]
@@ -1439,6 +1521,7 @@ case $cmd in
1439
1521
  doctor) cmd_doctor "$@" ;;
1440
1522
  verify) cmd_verify "$@" ;;
1441
1523
  new) cmd_new "$@" ;;
1524
+ note) cmd_note "$@" ;;
1442
1525
  show) cmd_show "$@" ;;
1443
1526
  edit) cmd_edit "$@" ;;
1444
1527
  path) cmd_path "$@" ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trailkeep/cli",
3
- "version": "0.3.1",
3
+ "version": "0.4.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