@macintacos/caret 1.0.2 → 1.1.1

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.
Files changed (104) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +19 -9
  3. package/bin/caret +1 -1
  4. package/bin/caret-launcher +48 -23
  5. package/commands/doctor.md +140 -0
  6. package/commands/plan.md +25 -0
  7. package/dist/cli.js +2863 -1413
  8. package/opencode/caret.plugin.ts +6 -5
  9. package/opencode/commands/doctor.md +121 -0
  10. package/opencode/commands/plan.md +24 -0
  11. package/package.json +1 -1
  12. package/ui/dist/assets/{angular-html-CNIx69MF.js → angular-html-BECXXPyr.js} +1 -1
  13. package/ui/dist/assets/{angular-ts-B6ob15kD.js → angular-ts-CJKAKY9F.js} +1 -1
  14. package/ui/dist/assets/{apl-BzQg5wKE.js → apl-CCcfDDqZ.js} +1 -1
  15. package/ui/dist/assets/{astro-DQ6PiTON.js → astro-DKCnAnVR.js} +1 -1
  16. package/ui/dist/assets/{blade-DdGMUzbN.js → blade-DQ_JdT2N.js} +1 -1
  17. package/ui/dist/assets/{c-DhrAH2di.js → c-D3TwszVj.js} +1 -1
  18. package/ui/dist/assets/{chapel-BIJH6n90.js → chapel-C-2sPcCX.js} +1 -1
  19. package/ui/dist/assets/{cobol-CR-dFJ1T.js → cobol-BPmo_4f_.js} +1 -1
  20. package/ui/dist/assets/{coffee-DWT27RO5.js → coffee-DMTsNDVt.js} +1 -1
  21. package/ui/dist/assets/{cpp-CuONTEvg.js → cpp-BJr6_Y8R.js} +1 -1
  22. package/ui/dist/assets/{crystal-Bvc0l-OM.js → crystal-pIWESUkD.js} +1 -1
  23. package/ui/dist/assets/{css-BYejQHVV.js → css-DRd9Q4Li.js} +1 -1
  24. package/ui/dist/assets/{dist-COogZ9jg.js → dist-BFQBMwmz.js} +1 -1
  25. package/ui/dist/assets/{dist-BgZhLp84.js → dist-BGfsV7n2.js} +1 -1
  26. package/ui/dist/assets/{dist-Cuerw0_4.js → dist-BJ7SLVNC.js} +1 -1
  27. package/ui/dist/assets/{dist-D-lJ6tIg.js → dist-BNr0c85A.js} +1 -1
  28. package/ui/dist/assets/{dist-C-FIXuZc.js → dist-C6t67BjE.js} +1 -1
  29. package/ui/dist/assets/{dist-Bn_ExXCc.js → dist-CD25BkNS.js} +1 -1
  30. package/ui/dist/assets/{dist-BbAsmjOe.js → dist-CU5aMlNo.js} +1 -1
  31. package/ui/dist/assets/{dist-CCPLuvXB.js → dist-DoJTqy3z.js} +1 -1
  32. package/ui/dist/assets/{dist-RKZBj-sl.js → dist-RKKeH1zs.js} +1 -1
  33. package/ui/dist/assets/{dist-DV7t-p4N.js → dist-aKlRxpD_.js} +1 -1
  34. package/ui/dist/assets/{edge-B_-8Eolr.js → edge-DeNk4F2G.js} +1 -1
  35. package/ui/dist/assets/{elixir-CAhwbI74.js → elixir-DHc-DOO8.js} +1 -1
  36. package/ui/dist/assets/{elm-yg-VSwEl.js → elm-CJRqEcvz.js} +1 -1
  37. package/ui/dist/assets/{erb-CIMOpWGO.js → erb-BEWGTHDA.js} +1 -1
  38. package/ui/dist/assets/{git-rebase-DI22snkc.js → git-rebase-DzlcWhg9.js} +1 -1
  39. package/ui/dist/assets/{glimmer-js-CZHZNAs-.js → glimmer-js-B-aWCRIZ.js} +1 -1
  40. package/ui/dist/assets/{glimmer-ts-C2rq-JGq.js → glimmer-ts-DcjmPyQn.js} +1 -1
  41. package/ui/dist/assets/{glsl-ByXN6hIN.js → glsl-C9FOT-Uv.js} +1 -1
  42. package/ui/dist/assets/{graphql-DiiJwLqR.js → graphql-BgKSoRp0.js} +1 -1
  43. package/ui/dist/assets/{hack-hQSSm1D1.js → hack-BOedpUUE.js} +1 -1
  44. package/ui/dist/assets/{haml-cf9hcOgw.js → haml-Cpngw1vd.js} +1 -1
  45. package/ui/dist/assets/{handlebars-B3tg892p.js → handlebars-CpW1IyBg.js} +1 -1
  46. package/ui/dist/assets/{html-CkIr2qlC.js → html-CV142D8s.js} +1 -1
  47. package/ui/dist/assets/{html-derivative-U1EOyk5J.js → html-derivative-Df7h1Nuw.js} +1 -1
  48. package/ui/dist/assets/{http-ek0-hDT8.js → http-Cneddv9N.js} +1 -1
  49. package/ui/dist/assets/{hurl-DbN52Bpk.js → hurl-D6TS55N6.js} +1 -1
  50. package/ui/dist/assets/{index-uBaTjG1U.js → index-Ci9yCNtV.js} +248 -214
  51. package/ui/dist/assets/index-t6i4na1R.css +1 -0
  52. package/ui/dist/assets/{java-DXEu0FGs.js → java-BicD3zQH.js} +1 -1
  53. package/ui/dist/assets/{javascript-Bi95ibpN.js → javascript-l6vKHWBV.js} +1 -1
  54. package/ui/dist/assets/{jinja-BcKnkqjY.js → jinja-BJF2iot2.js} +1 -1
  55. package/ui/dist/assets/{jison-CFaHQ730.js → jison-DiXPFJAT.js} +1 -1
  56. package/ui/dist/assets/{json--ulVurkL.js → json-BDRuI4UV.js} +1 -1
  57. package/ui/dist/assets/{jsx-ZonywBBD.js → jsx-BvfdkIOy.js} +1 -1
  58. package/ui/dist/assets/{julia-Bnt3bGqT.js → julia-bXAhreI5.js} +1 -1
  59. package/ui/dist/assets/{just-CBpil8Tz.js → just-BOxCfXDg.js} +1 -1
  60. package/ui/dist/assets/{latex-D-qwKUqq.js → latex-B9OiTBLF.js} +1 -1
  61. package/ui/dist/assets/{liquid-Bu8cI_b1.js → liquid-DuLKDeWE.js} +1 -1
  62. package/ui/dist/assets/{lua-DO_5Mymc.js → lua-Dbh9FQm_.js} +1 -1
  63. package/ui/dist/assets/{marko-DJfs6dcc.js → marko--tjEBgDM.js} +1 -1
  64. package/ui/dist/assets/{mdc-CnkU1WJT.js → mdc-ByNJpl3m.js} +1 -1
  65. package/ui/dist/assets/{nginx-B5LxXEA3.js → nginx-D4wsmFK9.js} +1 -1
  66. package/ui/dist/assets/{nim-DLTorTuD.js → nim-Cy8RxisP.js} +1 -1
  67. package/ui/dist/assets/{org-DnNvsXCU.js → org-CAm7o61d.js} +1 -1
  68. package/ui/dist/assets/{perl-Q5Q2rA31.js → perl-BAvaFGKe.js} +1 -1
  69. package/ui/dist/assets/{php-S65hjGBt.js → php-DpAf3w7u.js} +1 -1
  70. package/ui/dist/assets/{pug-k-MB8PEo.js → pug-Dm7otyJU.js} +1 -1
  71. package/ui/dist/assets/{qml-Bf9cLRPv.js → qml-D4rWQQKC.js} +1 -1
  72. package/ui/dist/assets/{r-Cr3hDQkZ.js → r-DMKUX8qf.js} +1 -1
  73. package/ui/dist/assets/{razor-BO5-wpmp.js → razor-BIUJkuf_.js} +1 -1
  74. package/ui/dist/assets/{regexp-ffQ9gsih.js → regexp-D8W_pVHj.js} +1 -1
  75. package/ui/dist/assets/{rst-B4AQC5xj.js → rst-aFr0DP5H.js} +1 -1
  76. package/ui/dist/assets/{ruby-Dr8kfvLX.js → ruby-BzH6kUKr.js} +1 -1
  77. package/ui/dist/assets/{sas-Kuo2pLeL.js → sas-CDk9DyCV.js} +1 -1
  78. package/ui/dist/assets/{scss-Ci63BQ8y.js → scss-DABbvU5W.js} +1 -1
  79. package/ui/dist/assets/{shellscript-DBh4gnVx.js → shellscript-B0ObP3K-.js} +1 -1
  80. package/ui/dist/assets/{shellsession-L5KFvHI3.js → shellsession-2sv3Jq0j.js} +1 -1
  81. package/ui/dist/assets/{soy-D6F5ilyd.js → soy-irSRV4oq.js} +1 -1
  82. package/ui/dist/assets/{sql-xwRblxjL.js → sql-DJCHoKvP.js} +1 -1
  83. package/ui/dist/assets/{stata-l8sBIGPT.js → stata-a8YnzbIc.js} +1 -1
  84. package/ui/dist/assets/{surrealql-DL2sOSzB.js → surrealql-DdXPOg8s.js} +1 -1
  85. package/ui/dist/assets/{svelte-Dp5d2Ja3.js → svelte-ClAKUEZR.js} +1 -1
  86. package/ui/dist/assets/{templ-B3LfgeSM.js → templ-NUCvvUBA.js} +1 -1
  87. package/ui/dist/assets/{tex-CfLQjgEV.js → tex-DwZeSAfC.js} +1 -1
  88. package/ui/dist/assets/{ts-tags-BCfi6Eh_.js → ts-tags-CFJEHYY_.js} +1 -1
  89. package/ui/dist/assets/{tsx-BXhT7ita.js → tsx-B934yX2T.js} +1 -1
  90. package/ui/dist/assets/{twig-t6tdMpRg.js → twig-NFkkOj3w.js} +1 -1
  91. package/ui/dist/assets/{typescript-CFIlDlxn.js → typescript-7bmMC_xU.js} +1 -1
  92. package/ui/dist/assets/{typst-CVQ9ief2.js → typst-BQEjp9tn.js} +1 -1
  93. package/ui/dist/assets/{vue-D92U7PL6.js → vue-JqD9ieOF.js} +1 -1
  94. package/ui/dist/assets/{vue-html-B_-4d7cG.js → vue-html-CT4XQRX1.js} +1 -1
  95. package/ui/dist/assets/{vue-vine-LaG8znAe.js → vue-vine-DIR27Dmn.js} +1 -1
  96. package/ui/dist/assets/{xml-DgAE0jfV.js → xml-DhsMbw-1.js} +1 -1
  97. package/ui/dist/assets/{xsl-DkD8aIzS.js → xsl-BvqsVUg9.js} +1 -1
  98. package/ui/dist/assets/{yaml-CEG92fN8.js → yaml-C8eWjK_Q.js} +1 -1
  99. package/ui/dist/index.html +2 -2
  100. package/commands/debug.md +0 -138
  101. package/commands/discovery.md +0 -35
  102. package/opencode/commands/debug.md +0 -59
  103. package/opencode/commands/discovery.md +0 -31
  104. package/ui/dist/assets/index-CAl4rX7_.css +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caret",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
4
4
  "description": "Review, annotate, and approve Claude Code plans in a local web UI.",
5
5
  "author": { "name": "macintacos" },
6
6
  "homepage": "https://github.com/macintacos/caret",
package/README.md CHANGED
@@ -35,6 +35,8 @@ After installing:
35
35
  1. **Restart the agent.** OpenCode installs the plugin package on its next start.
36
36
  2. **Run `/caret:demo`.** It presents a short demo plan that points at files in the repo
37
37
  you run it from, so you can exercise the whole flow before a real one arrives.
38
+ 3. **Run `/caret:plan <what you want done>`** to plan a real change and review it in
39
+ `caret` before the agent implements it.
38
40
 
39
41
  ### Running `caret` yourself
40
42
 
@@ -58,8 +60,12 @@ bunx --no-cache @macintacos/caret@latest install --refresh # update
58
60
  bunx --no-cache @macintacos/caret@latest install --uninstall # remove
59
61
  ```
60
62
 
63
+ `--refresh` restarts the caret service on the newest caret installed, never older than the
64
+ one you ran; caret keeps a copy of it under `~/.local/state/caret/roots/`.
65
+
61
66
  `caret`'s UI is designed to check for updates, at most once a day. When a newer `caret` is
62
- out, the review UI says so once: a toast on load, a mark on the settings button, and a
67
+ out, the review UI says so once: a toast on load that opens **What's new** (the skipped
68
+ release notes or trunk commits), a mark on the settings button, and a
63
69
  **Settings → Updates** pane naming the version and the exact command to take it.
64
70
 
65
71
  Turn the check off from that same pane, or by hand in
@@ -87,10 +93,12 @@ your browser instead of the terminal prompt. There you:
87
93
  and re-present.
88
94
 
89
95
  > [!TIP]
90
- > You don't have to wait to be intercepted: `caret` gives both agents a plan-review tool
91
- > they can call directly — `review_plan` in Claude Code (from the plugin's MCP server) and
92
- > `caret_review_plan` in OpenCode — so a skill of your own can route its plan through the
93
- > same review UI. The tool is for plans only; see
96
+ > You don't have to wait to be intercepted: `/caret:plan <what you want done>` asks the
97
+ > agent for a plan and routes it straight to the review UI. In OpenCode it runs on the
98
+ > `plan` agent, so switch to `build` to implement once you approve. A skill of your own
99
+ > can do the same with the plan-review tool `caret` gives both agents — `review_plan` in
100
+ > Claude Code (from the plugin's MCP server) and `caret_review_plan` in OpenCode. The tool
101
+ > is for plans only; see
94
102
  > [`doc/ARCHITECTURE.md`](doc/ARCHITECTURE.md#calling-the-review-tool-from-your-own-skill).
95
103
 
96
104
  ## Configuration
@@ -114,10 +122,12 @@ Two more live at the repo root:
114
122
 
115
123
  ## Diagnostics
116
124
 
117
- - `/caret:discovery` prints a one-shot, read-only diagnostics snapshot of your install —
118
- always redacted, and it never contains plan, prompt, or feedback bodies, nor any log
119
- contents.
120
- - `/caret:debug` reviews the current session's plans and recent errors.
125
+ - `/caret:doctor` runs `caret doctor`: a one-shot, read-only check of your install —
126
+ what's wrong, how to fix it, and the state it read that from. Always redacted, and it
127
+ never contains plan, prompt, or feedback bodies, nor any log message, error or stack
128
+ text.
129
+ - `caret doctor --bundle` archives the raw logs and review records when a maintainer needs
130
+ them. That archive is **not** redacted, so caret asks before writing it.
121
131
 
122
132
  ## License
123
133
 
package/bin/caret CHANGED
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # caret plugin entrypoint shim (EXC-643). This is the ONE file Claude Code's
4
4
  # hooks invoke — `${CLAUDE_PLUGIN_ROOT}/bin/caret <subcommand>` (see
5
- # hooks/hooks.json and commands/discovery.md). It is committed and shipped in
5
+ # hooks/hooks.json and commands/doctor.md). It is committed and shipped in
6
6
  # every install; it resolves the actual caret runtime from whatever the install
7
7
  # method placed beside it:
8
8
  #
@@ -19,6 +19,9 @@
19
19
  # service the launchd label WITHOUT `.plist` on macOS; the full unit name
20
20
  # WITH `.service` on Linux. Only macOS appends a suffix below.
21
21
  #
22
+ # $XDG_STATE_HOME/caret/roots/<version>/ holds caret install's own copies, scanned
23
+ # beside the agents' caches.
24
+ #
22
25
  # Each record must end with a newline: `read -r` reports EOF on a file without
23
26
  # one, and the read is what decides the record exists.
24
27
 
@@ -28,6 +31,8 @@ state="${XDG_STATE_HOME:-$HOME/.local/state}/caret"
28
31
  records="$state/launcher"
29
32
  claude_roots="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/plugins/cache/caret/caret"
30
33
  opencode_roots="${XDG_CACHE_HOME:-$HOME/.cache}/opencode/packages/@macintacos"
34
+ # Keep in sync with ownedRootsDir() (src/config/paths.ts).
35
+ owned_roots="$state/roots"
31
36
 
32
37
  # The units name no log destination, so nothing opens this before caret does:
33
38
  # openDaemonStderr's 0700/0600 guarantee (src/daemon/lifecycle.ts) happens here for a
@@ -60,50 +65,70 @@ read_record() {
60
65
  [ -f "$1" ] && read -r value <"$1" && [ -n "$value" ] && printf '%s' "$value"
61
66
  }
62
67
 
63
- # Highest of the `<version><TAB><path>` lines on stdin. A numeric field sort
64
- # rather than `sort -V`, which BSD sort does not carry everywhere. Ceiling: on an
65
- # equal numeric prefix the whole-line tiebreak puts a prerelease ABOVE the stable
66
- # release, so 0.15.0-rc.1 would beat 0.15.0. caret publishes no prereleases.
68
+ # Highest of the `<version><TAB><rest>` lines on stdin, printing <rest>. A numeric
69
+ # field sort rather than `sort -V`, which BSD sort does not carry everywhere. On an
70
+ # equal numeric prefix sort compares the whole line, and two things ride on that:
71
+ # candidates() puts its rank right after the version, so an agent's root (1) beats an
72
+ # owned copy (0) — keep the sort unstable (no -s); and a prerelease sorts ABOVE its
73
+ # release (0.15.0-rc.1 beats 0.15.0), a ceiling since caret publishes no prereleases.
74
+ # Keep in sync with pickLauncherRoot (src/commands/install/launcher.ts).
67
75
  highest() { sort -t. -k1,1n -k2,2n -k3,3n | tail -1 | cut -f2-; }
68
76
 
69
- # A candidate is a caret when it ships an executable bin/caret — the one file
70
- # every install method carries, tested the way it is used, since a non-executable
71
- # one would exec-fail at 126 and the supervisor would respawn into it forever.
77
+ # bin/caret-native embeds its UI at compile time; the bundle serves ui/dist, so a
78
+ # half-installed one would answer every UI request with ENOENT. A non-executable
79
+ # bin/caret would exec-fail at 126 and the supervisor would respawn into it forever.
80
+ # Keep in sync with isRunnableRoot (src/daemon/lifecycle.ts).
81
+ root_runnable() {
82
+ [ -x "$1/bin/caret" ] || return 1
83
+ [ -x "$1/bin/caret-native" ] || [ -f "$1/ui/dist/index.html" ]
84
+ }
85
+
72
86
  # Version comes from package.json, never the directory name: OpenCode names its
73
87
  # dir after the verbatim `plugin` specifier. The key is anchored so a minified
74
88
  # manifest yields nothing rather than a later key's value — a miss falls into the
75
89
  # branch that names the directory, a wrong version picks the wrong caret.
76
90
  candidate_version() {
77
- [ -x "$1/bin/caret" ] || return 1
91
+ root_runnable "$1" || return 1
78
92
  sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$1/package.json" 2>/dev/null | head -1
79
93
  }
80
94
 
81
- # Every dir either cache offers, runnable caret or not — the difference between
82
- # a broken install (terminal) and an empty cache (eviction). The OpenCode entry
83
- # is a two-line npm project, not a caret root; the root is one level down.
95
+ # Every dir either agent's cache or caret's own copies offer, runnable caret or
96
+ # not — the difference between a broken install (terminal) and an empty cache
97
+ # (eviction). The OpenCode entry is a two-line npm project, not a caret root; the
98
+ # root is one level down. `*` skips dot-prefixed dirs, so a half-copied owned root
99
+ # is never a candidate. Keep in sync with launcherCandidateDirs
100
+ # (src/commands/install/launcher.ts).
84
101
  candidate_dirs() {
85
102
  local dir
86
- for dir in "$claude_roots"/* "$opencode_roots"/caret*/node_modules/@macintacos/caret; do
103
+ for dir in "$claude_roots"/* "$opencode_roots"/caret*/node_modules/@macintacos/caret \
104
+ "$owned_roots"/*; do
87
105
  if [ -d "$dir" ]; then printf '%s\n' "$dir"; fi
88
106
  done
89
107
  }
90
108
 
109
+ # `<version><TAB><rank><TAB><dir>`: rank 0 for caret's own copy, 1 for an agent's.
91
110
  candidates() {
92
- local dir version
111
+ local dir version rank
93
112
  candidate_dirs | while read -r dir; do
94
113
  version="$(candidate_version "$dir" || true)"
95
- if [ -n "$version" ]; then printf '%s\t%s\n' "$version" "$dir"; fi
114
+ rank=1
115
+ case "$dir" in "$owned_roots"/*) rank=0 ;; esac
116
+ if [ -n "$version" ]; then printf '%s\t%s\t%s\n' "$version" "$rank" "$dir"; fi
96
117
  done
97
118
  }
98
119
 
99
120
  resolve_root() {
100
121
  local pin
101
122
  pin="$(read_record "$records/pinned-root" || true)"
102
- if [ -n "$pin" ] && [ -x "$pin/bin/caret" ]; then
123
+ if [ -n "$pin" ] && root_runnable "$pin"; then
103
124
  printf '%s' "$pin"
104
125
  return 0
105
126
  fi
106
- candidates | highest
127
+ # The rank sorts an agent's root above an owned copy of the same version: a hook
128
+ # reuses a supervised daemon only when its build matches. highest() leaves
129
+ # candidates()' rank in front of the dir. Keep in sync with pickLauncherRoot
130
+ # (src/commands/install/launcher.ts).
131
+ candidates | highest | cut -f2-
107
132
  }
108
133
 
109
134
  # The newest mise-managed bun, for a machine whose mise shims are not on the
@@ -153,8 +178,8 @@ stop_agent() {
153
178
  # tears its service down and deletes itself. Returns non-zero without touching
154
179
  # anything when there is no `service` record — nothing here installed a
155
180
  # supervisor, so this launcher cannot know what still names it, and deleting
156
- # itself would leave whatever does respawning on ENOENT. Deleting only bin/ and
157
- # the records leaves review state for a reinstall.
181
+ # itself would leave whatever does respawning on ENOENT. Deleting bin/, the
182
+ # records and caret's own roots leaves review state for a reinstall.
158
183
  evict() {
159
184
  local service unit
160
185
  service="$(read_record "$records/service" || true)"
@@ -166,17 +191,17 @@ evict() {
166
191
  systemctl --user disable --now "$service" || true
167
192
  unit="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/$service"
168
193
  fi
169
- printf 'caret-launcher: no caret installed anywhere; removing %s and %s.\n' \
170
- "$unit" "$state/bin" >&2
194
+ printf 'caret-launcher: no caret installed anywhere; removing %s and %s/{bin,launcher,roots}.\n' \
195
+ "$unit" "$state" >&2
171
196
  rm -f "$unit"
172
- rm -rf "${state:?}/bin" "${state:?}/launcher"
197
+ rm -rf "${state:?}/bin" "${state:?}/launcher" "${state:?}/roots"
173
198
  exit 0
174
199
  }
175
200
 
176
201
  # Only under a supervisor: a hand-run launcher keeps its diagnostics on the terminal.
177
202
  if [ "${CARET_SUPERVISED:-}" = 1 ]; then open_daemon_log; fi
178
203
 
179
- # Only a total miss re-probes. A populated version dir with no runnable bin/caret
204
+ # Only a total miss re-probes. A populated version dir with no runnable caret
180
205
  # is far more likely a bad install than an extraction caught mid-flight, and that
181
206
  # path is recoverable with `caret install`; an empty cache is the one whose branch
182
207
  # deletes things, so it is the one worth waiting on.
@@ -190,7 +215,7 @@ done
190
215
  if [ -z "$root" ]; then
191
216
  found="$(candidate_dirs)"
192
217
  if [ -z "$found" ]; then evict || true; fi
193
- printf 'caret-launcher: no runnable bin/caret under any of:\n%s\n' \
218
+ printf 'caret-launcher: no runnable caret (an executable bin/caret and a UI build) under any of:\n%s\n' \
194
219
  "${found:-(no install directories at all)}" >&2
195
220
  printf 'caret-launcher: run: caret install — to restore the service.\n' >&2
196
221
  stop_agent
@@ -0,0 +1,140 @@
1
+ ---
2
+ description: Diagnose this caret install — run caret doctor, relay each failing check's remedy, and present the report ready to share
3
+ ---
4
+
5
+ caret's `doctor` subcommand renders a verdict over a one-shot, read-only snapshot of the
6
+ local install: a `checks:` block naming what is wrong and how to fix it, then the state it
7
+ read that from — running caret processes, daemon identity, lock/port state, effective
8
+ settings, review counts, the agent adapter's install-state probe (plugin version, enabled
9
+ state, and whether a manual hook sits in the agent's user settings), log sizes and error
10
+ counts, recent failures grouped by review with their codes, install/runtime info, and
11
+ system basics. The report is **always redacted** (home paths become `~`, usernames in
12
+ foreign home paths are censored) and never contains plan, prompt, or feedback bodies, nor
13
+ any log message, error or stack text — it exists to be shared.
14
+
15
+ ## 1. Run it
16
+
17
+ First ask the user (via `AskUserQuestion`) which format they want:
18
+
19
+ - **JSON** — for feeding to tooling.
20
+ - **Human-readable** — for scanning in the conversation.
21
+
22
+ Then run the matching invocation:
23
+
24
+ ```bash
25
+ "${CLAUDE_PLUGIN_ROOT}/bin/caret" doctor --json # JSON
26
+ "${CLAUDE_PLUGIN_ROOT}/bin/caret" doctor # human-readable
27
+ ```
28
+
29
+ The exit code is the verdict: `0` every check passed, `1` at least one failed, `2` no
30
+ report — doctor could not produce one, or a `--bundle` run had no terminal to ask consent
31
+ at and stopped before collecting anything. On `2`, present the exit code and stderr
32
+ instead — and if the binary itself is missing (a source checkout that has never been
33
+ built), say so and point at `mise run build` (or `mise run build --install`).
34
+
35
+ ## 2. Relay each failing check
36
+
37
+ A failing check already carries the remedy that closes it; relay it rather than inventing
38
+ one. What each id means:
39
+
40
+ | id | what failed | relay |
41
+ | --- | --- | --- |
42
+ | `daemon-reachable` | a caret service is recorded but its supervisor is not keeping it alive (not loaded, turned off, or parked) or the daemon did not answer; or the effective port answers as something other than caret | the check's remedy verbatim, then offer to read any log it names |
43
+ | `daemon-version` | the running daemon is older than npm's latest | the check's remedy verbatim |
44
+ | `daemon-lock` | the lock names a dead pid, or a port caret is not configured to bind | the check's remedy verbatim |
45
+ | `agent-install` | the agent the check names does not have caret enabled | `caret install` |
46
+ | `log-errors` | a live log wrote an NDJSON error record in the last 24h; the detail names which log and when it last erred | step 4 |
47
+ | `opencode-caret-version` | OpenCode would load a caret behind the published one | the check's remedy verbatim |
48
+
49
+ An `unknown` check is not a failure: it names its own `reason` — no network, an unreadable
50
+ config, or a report section that never collected, which also shows as `<name> error: …`
51
+ (`"error": "…"` under `--json`) and leaves the check with no detail. A doctor run offline
52
+ reaching one is the normal case; present it as-is rather than treating it as a failure of
53
+ this command. A daemon that is simply not running is not one of these — it reports
54
+ `reachable false` and *passes*, because an on-demand daemon idle-exits by design.
55
+
56
+ ## 3. Present the report
57
+
58
+ End your output with the command's stdout **verbatim** in a fenced code block —
59
+ ` ```json ` for the JSON format, plain ` ``` ` for the human-readable one — so the user
60
+ can copy it out in one piece. Add nothing inside the block and do not summarize away
61
+ sections; the report is already redacted and complete.
62
+
63
+ ## 4. Debug a failing `log-errors`
64
+
65
+ When `log-errors` failed and the `/systematic-debugging` skill is available, invoke it to
66
+ drive root-cause investigation from the logs the check named. Otherwise read the last
67
+ error records yourself and reason from the failing step, code, msg, cause, and stack.
68
+
69
+ Read the report's `failures` first. Each group is one review's error records from both
70
+ logs with their codes (a session's, when the failure came before a review id existed), so
71
+ a hook-side record sits beside any daemon-side record from that review's own requests. A
72
+ daemon-wide failure (a crash, a bind failure, a failed update check) carries no id and
73
+ lands in `ungrouped`: read it alongside the group and correlate the two by `time`. Pull a
74
+ group's full records from both logs (`.sessionId` for a session group):
75
+
76
+ ```bash
77
+ dir="${XDG_STATE_HOME:-$HOME/.local/state}/caret"
78
+ grep -h '^{' "$dir/logs/caret.log" "$dir/logs/daemon.log" | jq -c 'select(.reviewId == "<id>")'
79
+ ```
80
+
81
+ For records outside any group or the report's window, fall back to the last error records
82
+ of each log:
83
+
84
+ ```bash
85
+ grep '^{' "$dir/logs/caret.log" | jq -s '[.[] | select(.level >= 50)] | .[-5:]'
86
+ grep '^{' "$dir/logs/daemon.log" | jq -s '[.[] | select(.level >= 50)] | .[-5:]'
87
+ tail -n 40 "$dir/logs/daemon-stderr.log"
88
+ ```
89
+
90
+ `log-errors` counts only NDJSON error records, so a crash that reached `daemon-stderr.log`
91
+ alone leaves it passing — tail that file even when the check is green. It fails only on
92
+ records from the last 24h, so a log that has settled passes while still reporting when it
93
+ last erred; work from that time rather than from the count. If a failure predates the
94
+ current live file, `gunzip -c` the segment you need from `logs/archive/` and rerun the
95
+ recipe against the result.
96
+
97
+ ## 5. Review this session's plans
98
+
99
+ The report counts reviews in aggregate; it does not say what happened in *this* session.
100
+ When the user wants that timeline, reconstruct it from the records — identify the session
101
+ by the most recently updated review whose `cwd` matches, then list every review in it,
102
+ oldest first:
103
+
104
+ ```bash
105
+ dir="${XDG_STATE_HOME:-$HOME/.local/state}/caret"
106
+ sid=$(jq -rs --arg cwd "$PWD" '[.[] | select(.cwd == $cwd)] | sort_by(.updatedAt) | last | .sessionId // empty' "$dir"/reviews/*.json)
107
+ jq -s --arg sid "$sid" '[.[] | select(.sessionId == $sid)] | sort_by(.createdAt) | .[] | {id, title, status, versions: (.versions | length), decidedAt: .decision.decidedAt, feedback: .decision.feedback}' "$dir"/reviews/*.json
108
+ ```
109
+
110
+ That field whitelist is deliberate: `versions[].plan` and `generalCommentDraft` hold full
111
+ plan and draft bodies — never select or echo them.
112
+
113
+ If `sid` comes back empty, no review matches the working directory. Say so, then fall back
114
+ to the most recently updated session across all reviews and re-run the listing with it:
115
+
116
+ ```bash
117
+ sid=$(jq -rs 'sort_by(.updatedAt) | last | .sessionId // empty' "$dir"/reviews/*.json)
118
+ ```
119
+
120
+ If `$dir/reviews` is missing or empty the glob won't match and these commands error (shell
121
+ or jq, depending on the shell) — that is "no reviews recorded", not a failure.
122
+
123
+ Present the result grouped by status, each review with its id, title, and version count
124
+ (the number of plan revisions):
125
+
126
+ - **pending** — awaiting a decision in the browser.
127
+ - **rejected** — changes requested; awaiting a revised plan. Include a short excerpt of
128
+ the decision `feedback`.
129
+ - **approved** — plan accepted; terminal success.
130
+ - **expired** — abandoned by its hook (timeout) or superseded by a resubmitted plan;
131
+ terminal, never reviewed.
132
+
133
+ ## 6. Sharing the raw logs
134
+
135
+ If the report is not enough and the user wants to hand the raw material to a maintainer,
136
+ `caret doctor --bundle` writes a zip of the live logs and review records to caret's state
137
+ dir. Tell them plainly before suggesting it: that archive is **not** redacted — it holds
138
+ their logs and full plan bodies — so it is written only after they confirm, and it should
139
+ move over a channel they trust. caret asks at a terminal and your run has none, so carry
140
+ their answer with `--yes` — never before they have given it.
@@ -0,0 +1,25 @@
1
+ ---
2
+ description: Plan a change and route the plan through caret's review UI before implementing it
3
+ argument-hint: [what you want done]
4
+ ---
5
+
6
+ Plan this, then implement it once caret approves the plan:
7
+
8
+ $ARGUMENTS
9
+
10
+ If that is empty, plan whatever the user asked for earlier in this session.
11
+
12
+ Call `EnterPlanMode` first: `ExitPlanMode` is refused outside plan mode. Research what the
13
+ change needs — read the relevant code, trace the real flow — then write the plan to the
14
+ file your plan-mode instructions name. Call `ExitPlanMode` with that same text as its
15
+ `plan` argument — caret reviews the argument, so a call without it reaches the review UI
16
+ empty.
17
+
18
+ This triggers caret's `PermissionRequest`/`ExitPlanMode` hook, which opens the plan in a
19
+ local browser UI for inline review. When the user approves or requests changes there, the
20
+ decision flows back to you:
21
+
22
+ - **Approved** → implement the plan.
23
+ - **Changes requested** → the feedback arrives as the denial reason. Re-read the plan file
24
+ (caret rewrites it in its canonical shape, so the feedback's line numbers match it),
25
+ revise it, and call `ExitPlanMode` again with the revised text as its `plan` argument.