@macintacos/caret 1.8.0 → 1.9.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 +34 -7
- package/bin/caret-launcher +15 -10
- package/{opencode → bridge}/review-bridge.ts +153 -12
- package/commands/doctor.md +4 -0
- package/dist/cli.js +23974 -23344
- package/opencode/caret.core.ts +26 -113
- package/opencode/caret.plugin.ts +11 -5
- package/opencode/caret.plugin.v2.ts +12 -4
- package/opencode/caret.tui.ts +6 -18
- package/opencode/commands/demo.md +1 -1
- package/opencode/commands/doctor.md +4 -0
- package/package.json +18 -3
- package/pi/caret.ts +145 -0
- package/pi/demo.ts +79 -0
- package/pi/doctor.ts +149 -0
- package/pi/follow-up.ts +19 -0
- package/pi/gate.ts +49 -0
- package/pi/notice.ts +127 -0
- package/pi/plan.ts +182 -0
- package/pi/render.ts +51 -0
- package/pi/state.ts +74 -0
- package/pi/tool.ts +150 -0
- package/ui/dist/assets/{angular-html-BXFKQGXA.js → angular-html-DTAmLsI1.js} +1 -1
- package/ui/dist/assets/{angular-ts-BteBaZbv.js → angular-ts-CnmIqVSv.js} +1 -1
- package/ui/dist/assets/{apl-Boa3o4y5.js → apl-BRrI8o6u.js} +1 -1
- package/ui/dist/assets/{astro-BeBp_x10.js → astro-ws1SFlqZ.js} +1 -1
- package/ui/dist/assets/{blade-BsWYzT9U.js → blade-BAfome-F.js} +1 -1
- package/ui/dist/assets/{c-CiG9bK25.js → c-Cb8iBkqY.js} +1 -1
- package/ui/dist/assets/{chapel-7fNKKqpl.js → chapel-CwNxsGmt.js} +1 -1
- package/ui/dist/assets/{cobol-B1HBnyA6.js → cobol-C6senUsW.js} +1 -1
- package/ui/dist/assets/{coffee-BR4Q34UF.js → coffee-BNmOwO3K.js} +1 -1
- package/ui/dist/assets/{cpp-C3wngvr5.js → cpp-BBfuAVc8.js} +1 -1
- package/ui/dist/assets/{crystal-DiIiTPBe.js → crystal-BIenSyu_.js} +1 -1
- package/ui/dist/assets/{css-BX3gLifs.js → css-BM7X2qQt.js} +1 -1
- package/ui/dist/assets/{dist-anXz8tLl.js → dist-8Qj1RsyX.js} +1 -1
- package/ui/dist/assets/{dist-yYI7sEVm.js → dist-BBj5r4JZ.js} +1 -1
- package/ui/dist/assets/{dist-r7n7YX8s.js → dist-B_l6M6p7.js} +1 -1
- package/ui/dist/assets/{dist-DaLa-fRf.js → dist-Bsjq6LzO.js} +1 -1
- package/ui/dist/assets/{dist-D7HvdIC5.js → dist-Bxb_HUlD.js} +1 -1
- package/ui/dist/assets/{dist-BoVp9dqK.js → dist-C7V4XWMN.js} +1 -1
- package/ui/dist/assets/{dist-SYZxBIS_.js → dist-Cq8hWpOn.js} +1 -1
- package/ui/dist/assets/{dist-C79XSdY8.js → dist-fytLbOcF.js} +1 -1
- package/ui/dist/assets/{dist-50vAtYCv.js → dist-zbP7SV6c.js} +1 -1
- package/ui/dist/assets/{dist-jDKYW61p.js → dist-zezPBQmO.js} +1 -1
- package/ui/dist/assets/{edge-D6eM3HKg.js → edge-BlclzlRw.js} +1 -1
- package/ui/dist/assets/{elixir-BiXwbjRd.js → elixir-BYumuPVp.js} +1 -1
- package/ui/dist/assets/{elm-uPrh5C1H.js → elm-DNBykypS.js} +1 -1
- package/ui/dist/assets/{erb-C01CNhsq.js → erb-Cg-GLw-3.js} +1 -1
- package/ui/dist/assets/{git-rebase-BctEis_g.js → git-rebase-DcwetDCn.js} +1 -1
- package/ui/dist/assets/{glimmer-js-gguWPKCT.js → glimmer-js-DwUQB02R.js} +1 -1
- package/ui/dist/assets/{glimmer-ts-ByonKsDM.js → glimmer-ts-DNxDwzad.js} +1 -1
- package/ui/dist/assets/{glsl-Buh4zHgg.js → glsl-CTQvGLl-.js} +1 -1
- package/ui/dist/assets/{graphql-DtmB7miI.js → graphql-6N6VWnCL.js} +1 -1
- package/ui/dist/assets/{hack-qTVQw4Zz.js → hack-BIWJ2t_-.js} +1 -1
- package/ui/dist/assets/{haml-Bt2RhQj7.js → haml-CTaug-6I.js} +1 -1
- package/ui/dist/assets/{handlebars-QJ7q0AEL.js → handlebars-EciX6Xtm.js} +1 -1
- package/ui/dist/assets/{html-Db-D3rXr.js → html-5ZVMwxqg.js} +1 -1
- package/ui/dist/assets/{html-derivative-DX0FUICr.js → html-derivative-Bo7PACv6.js} +1 -1
- package/ui/dist/assets/{http-CF8QPtf7.js → http-D14aglwt.js} +1 -1
- package/ui/dist/assets/{hurl-GyiNlLSJ.js → hurl-DGa-Lc1_.js} +1 -1
- package/ui/dist/assets/index-BocyxbG2.css +1 -0
- package/ui/dist/assets/{index-CfQ31NDJ.js → index-Do48mTUl.js} +226 -225
- package/ui/dist/assets/{java-7GW5fJMj.js → java-BUpF6706.js} +1 -1
- package/ui/dist/assets/{javascript-DLle3-Wd.js → javascript-CU7AY0Mj.js} +1 -1
- package/ui/dist/assets/{jinja-DyZ1srCs.js → jinja-OREkRLMJ.js} +1 -1
- package/ui/dist/assets/{jison-B5gggSJ6.js → jison-Zf3gmndI.js} +1 -1
- package/ui/dist/assets/{json-CQEsMxfE.js → json-C8E_SzMr.js} +1 -1
- package/ui/dist/assets/{jsx-BP9hEOr_.js → jsx-CHMjfgSc.js} +1 -1
- package/ui/dist/assets/{julia-CIVTqrrA.js → julia-DnO66Wng.js} +1 -1
- package/ui/dist/assets/{just-D4NdkezD.js → just-BUkZWNEk.js} +1 -1
- package/ui/dist/assets/{latex-CFviGdV1.js → latex-eFDA2ndR.js} +1 -1
- package/ui/dist/assets/{liquid-Dtx11Zuh.js → liquid-BqLuL6hU.js} +1 -1
- package/ui/dist/assets/{lua-kGMOY_yZ.js → lua-CKNxXI-W.js} +1 -1
- package/ui/dist/assets/{marko-OjkhOEN7.js → marko-CM7Qyeyg.js} +1 -1
- package/ui/dist/assets/{mdc-wyhjzzvx.js → mdc-DQnmyYku.js} +1 -1
- package/ui/dist/assets/{nginx-C6dVmlUS.js → nginx-B7PJAw6W.js} +1 -1
- package/ui/dist/assets/{nim-BX63w2ay.js → nim-CmtAHkVL.js} +1 -1
- package/ui/dist/assets/{org-DZgxDttS.js → org-DXNJ0F8G.js} +1 -1
- package/ui/dist/assets/{perl-JXbylPLY.js → perl-DCIWxaEc.js} +1 -1
- package/ui/dist/assets/{php-5EbhLfI_.js → php-Dc66Nx9S.js} +1 -1
- package/ui/dist/assets/{pug-C6Iv2U2H.js → pug-C_S0MlW-.js} +1 -1
- package/ui/dist/assets/{qml-CqGs5UjC.js → qml-VqOrS5Wl.js} +1 -1
- package/ui/dist/assets/{r-BKwdj1Ob.js → r-Dl8VhfSg.js} +1 -1
- package/ui/dist/assets/{razor-CAmmpIRM.js → razor-BEpGMKEn.js} +1 -1
- package/ui/dist/assets/{regexp-D-P27PhL.js → regexp-X9VkDePO.js} +1 -1
- package/ui/dist/assets/{rst-BtBzILBo.js → rst-CQz_65m4.js} +1 -1
- package/ui/dist/assets/{ruby-BFChMOIO.js → ruby-DcUs-Hjd.js} +1 -1
- package/ui/dist/assets/{sas-BHer69PY.js → sas-0Aya4BIJ.js} +1 -1
- package/ui/dist/assets/{scss-B5xzD0OV.js → scss-BiYXJFG-.js} +1 -1
- package/ui/dist/assets/{shellscript-Dzit1Ttv.js → shellscript-D_GiG2j2.js} +1 -1
- package/ui/dist/assets/{shellsession-CMvz-BMu.js → shellsession-CJaG9Dka.js} +1 -1
- package/ui/dist/assets/{soy-D7ynJc6J.js → soy-qeuP0wdW.js} +1 -1
- package/ui/dist/assets/{sql-bRew6PYM.js → sql-DjtVrjTT.js} +1 -1
- package/ui/dist/assets/{stata-22_vRW2L.js → stata-CTtxT0Lq.js} +1 -1
- package/ui/dist/assets/{surrealql-C34aKiyl.js → surrealql-CLiRfZ3-.js} +1 -1
- package/ui/dist/assets/{svelte-Dto-0MeJ.js → svelte-DojibMCX.js} +1 -1
- package/ui/dist/assets/{templ-DtacKQ1n.js → templ-BccZaOLv.js} +1 -1
- package/ui/dist/assets/{tex-CS5XiOvi.js → tex-DvrCGTlw.js} +1 -1
- package/ui/dist/assets/{ts-tags-BdzU5K8A.js → ts-tags-Dv3BB8TA.js} +1 -1
- package/ui/dist/assets/{tsx-Butg554t.js → tsx-DogZPQbY.js} +1 -1
- package/ui/dist/assets/{twig-CZKpRnni.js → twig-DY6_W6um.js} +1 -1
- package/ui/dist/assets/{typescript-CZA7NJZI.js → typescript-DwP-qhUH.js} +1 -1
- package/ui/dist/assets/{typst-DIVncoNE.js → typst-DW555l0v.js} +1 -1
- package/ui/dist/assets/{vue-Bn3_aUS9.js → vue-OZGLIqo2.js} +1 -1
- package/ui/dist/assets/{vue-html-B7r3yzLD.js → vue-html-CjmVpLjs.js} +1 -1
- package/ui/dist/assets/{vue-vine-B3JnxtKQ.js → vue-vine-Cye_YqnL.js} +1 -1
- package/ui/dist/assets/{xml-DKhH88k1.js → xml-eBWMgcbX.js} +1 -1
- package/ui/dist/assets/{xsl-boEEflBZ.js → xsl-BdSZmEF6.js} +1 -1
- package/ui/dist/assets/{yaml-B2pr6j9H.js → yaml-CbCy6UZM.js} +1 -1
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-P_fbn8LU.css +0 -1
package/README.md
CHANGED
|
@@ -35,7 +35,8 @@ Assuming that you installed it as a long-lived service (the default), you can na
|
|
|
35
35
|
|
|
36
36
|
After installing:
|
|
37
37
|
|
|
38
|
-
1. **Restart the agent.** OpenCode installs the plugin package on its next start
|
|
38
|
+
1. **Restart the agent.** OpenCode installs the plugin package on its next start; in pi,
|
|
39
|
+
run `/reload` or restart it.
|
|
39
40
|
2. **Run `/caret:demo`.** It presents a short demo plan that points at files in the repo
|
|
40
41
|
you run it from, so you can exercise the whole flow before a real one arrives.
|
|
41
42
|
3. **Run `/caret:plan <what you want done>`** to plan a real change and review it in
|
|
@@ -83,9 +84,15 @@ check = false
|
|
|
83
84
|
loads caret but can't review a plan file the agent wrote, so the agent sends the plan text
|
|
84
85
|
instead. See [the OpenCode adapter](doc/ARCHITECTURE.md#the-opencode-adapter) for the
|
|
85
86
|
by-hand equivalents, for pinning a version in OpenCode's plugin array (`plugin` on v1,
|
|
86
|
-
`plugins` on v2)
|
|
87
|
+
`plugins` on v2); [`caret install`](doc/ARCHITECTURE.md#installing-with-caret-install)
|
|
88
|
+
covers what each agent's install touches;
|
|
87
89
|
[the Claude Code adapter](doc/ARCHITECTURE.md#the-claude-code-adapter) covers the hooks
|
|
88
|
-
`caret` registers there.
|
|
90
|
+
`caret` registers there. `caret install` also installs into pi 1.5.0 or later when `pi` is
|
|
91
|
+
on your PATH; run `/reload` in pi or restart it to pick caret up. `caret install` and
|
|
92
|
+
`caret doctor` manage only pi's user-scope packages and ignore a project-scope install
|
|
93
|
+
(`pi install -l`). OpenCode and pi both load caret from its npm package, which runs on
|
|
94
|
+
bun, so `bun` must be on the PATH they run with. See
|
|
95
|
+
[the pi adapter](doc/ARCHITECTURE.md#the-pi-adapter) for the by-hand equivalents.
|
|
89
96
|
|
|
90
97
|
## Using `caret`
|
|
91
98
|
|
|
@@ -103,13 +110,32 @@ your browser instead of the terminal prompt. There you:
|
|
|
103
110
|
> You don't have to wait to be intercepted: `/caret:plan <what you want done>` asks the
|
|
104
111
|
> agent for a plan and routes it straight to the review UI. In OpenCode it runs on the
|
|
105
112
|
> `plan` agent, so switch to `build` to implement once you approve. A skill of your own
|
|
106
|
-
> can do the same with the plan-review tool `caret` gives
|
|
107
|
-
> Claude Code (from the plugin's MCP server) and `caret_review_plan` in OpenCode
|
|
108
|
-
> is for plans only; see
|
|
113
|
+
> can do the same with the plan-review tool `caret` gives each agent — `review_plan` in
|
|
114
|
+
> Claude Code (from the plugin's MCP server) and `caret_review_plan` in OpenCode and pi.
|
|
115
|
+
> The tool is for plans only; see
|
|
109
116
|
> [`doc/ARCHITECTURE.md`](doc/ARCHITECTURE.md#calling-the-review-tool-from-your-own-skill).
|
|
110
117
|
> In OpenCode v2, `/caret` or `<leader>p` (`ctrl+x p` by default) opens the plan waiting
|
|
111
118
|
> in the session; rebind the key as `caret.open` under OpenCode's `keybinds`.
|
|
112
119
|
|
|
120
|
+
### Using `caret` in pi
|
|
121
|
+
|
|
122
|
+
pi has no plan mode of its own, so `caret` brings a small one:
|
|
123
|
+
|
|
124
|
+
1. `/caret:plan <what you want done>` turns on planning mode. The agent researches, writes
|
|
125
|
+
its plan into pi's plans folder (`~/.pi/agent/plans` by default), and sends it to
|
|
126
|
+
`caret`. Until you approve, `caret` blocks the agent's `edit` and `write` tools outside
|
|
127
|
+
that folder; shell commands are only discouraged. While planning, `/caret:plan` with no
|
|
128
|
+
task turns planning off. Outside planning, a bare `/caret:plan` plans what you asked
|
|
129
|
+
for earlier in the session.
|
|
130
|
+
2. While a review waits, a notice sits above the editor and the footer reads
|
|
131
|
+
`^ plan waiting`. `/caret` or `ctrl+alt+o` opens the plan in your browser. In
|
|
132
|
+
Terminal.app or iTerm2, set Option to act as Meta (Esc+) for the shortcut to reach pi.
|
|
133
|
+
3. Approve, and the agent implements. Request changes, and it revises and resubmits. Esc
|
|
134
|
+
in pi cancels a waiting review, and the agent is told not to implement the plan.
|
|
135
|
+
|
|
136
|
+
`/caret:demo` presents a demo plan, and `/caret:doctor` runs `caret doctor` and shows its
|
|
137
|
+
report in the session (see [Diagnostics](#diagnostics)).
|
|
138
|
+
|
|
113
139
|
## Configuration
|
|
114
140
|
|
|
115
141
|
`caret` runs with sensible defaults and needs no configuration. To tune it — the daemon
|
|
@@ -130,7 +156,8 @@ to the reference page that answers your question.
|
|
|
130
156
|
Two more live at the repo root:
|
|
131
157
|
|
|
132
158
|
- [CONTRIBUTING.md](CONTRIBUTING.md) — develop `caret` locally: setup, the `mise`
|
|
133
|
-
workflow, and where tests live.
|
|
159
|
+
workflow, and where tests live. PRs run the full eight-check gate in CI;
|
|
160
|
+
`mise run preflight` is the fast local one.
|
|
134
161
|
- [AGENTS.md](AGENTS.md) — for coding agents: routes a change to the rules-of-the-road
|
|
135
162
|
that govern it.
|
|
136
163
|
|
package/bin/caret-launcher
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
#
|
|
3
3
|
# The stable launcher a launchd plist or systemd unit names (EXC-1160). Those
|
|
4
4
|
# units must name an absolute executable path, but caret's runtime lives at a
|
|
5
|
-
# versioned path that changes on every upgrade — and
|
|
6
|
-
#
|
|
7
|
-
# `caret install`. So the unit names this file, which caret owns at
|
|
5
|
+
# versioned path that changes on every upgrade — and Claude Code, OpenCode,
|
|
6
|
+
# and pi (`pi update`) each replace their copy on their own schedule, without
|
|
7
|
+
# running `caret install`. So the unit names this file, which caret owns at
|
|
8
8
|
# $XDG_STATE_HOME/caret/bin/caret, and resolution happens here at exec time.
|
|
9
9
|
#
|
|
10
10
|
# Bash rather than TypeScript for the same reason bin/caret is: it has to locate
|
|
@@ -33,6 +33,9 @@ claude_roots="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/plugins/cache/caret/caret"
|
|
|
33
33
|
opencode_cache="${XDG_CACHE_HOME:-$HOME/.cache}/opencode"
|
|
34
34
|
opencode_v1_roots="$opencode_cache/packages/@macintacos"
|
|
35
35
|
opencode_v2_roots="$opencode_cache/npm/@macintacos"
|
|
36
|
+
# pi's default agent dir only: the supervisor never records PI_CODING_AGENT_DIR.
|
|
37
|
+
# Keep in sync with launcherCandidateDirs (src/commands/install/launcher.ts).
|
|
38
|
+
pi_root="$HOME/.pi/agent/npm/node_modules/@macintacos/caret"
|
|
36
39
|
# Keep in sync with ownedRootsDir() (src/config/paths.ts).
|
|
37
40
|
owned_roots="$state/roots"
|
|
38
41
|
|
|
@@ -107,13 +110,14 @@ live_generation() {
|
|
|
107
110
|
if [ -n "$newest" ]; then printf '%s' "$1/$newest"; fi
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
# Every dir
|
|
111
|
-
# not — the difference between a broken install (terminal)
|
|
112
|
-
# (eviction). OpenCode's cache dir is named exactly `caret`
|
|
113
|
-
# holds a two-line npm project with the root one level
|
|
114
|
-
# generations, of which only the live one is offered.
|
|
115
|
-
# so a half-copied owned root is never a
|
|
116
|
-
#
|
|
113
|
+
# Every dir an agent's cache, pi's npm folder, or caret's own copies offer,
|
|
114
|
+
# runnable caret or not — the difference between a broken install (terminal)
|
|
115
|
+
# and an empty cache (eviction). OpenCode's cache dir is named exactly `caret`
|
|
116
|
+
# or `caret@<spec>`: v1's holds a two-line npm project with the root one level
|
|
117
|
+
# down; v2's holds numbered generations, of which only the live one is offered.
|
|
118
|
+
# `*` skips dot-prefixed dirs, so a half-copied owned root is never a
|
|
119
|
+
# candidate. Keep in sync with launcherCandidateDirs
|
|
120
|
+
# (src/commands/install/launcher.ts) and opencodeRootPaths
|
|
117
121
|
# (src/adapters/opencode/paths.ts); both suites pin it through
|
|
118
122
|
# test/core/commands/install/fixtures/launcher-caches.txt.
|
|
119
123
|
candidate_dirs() {
|
|
@@ -129,6 +133,7 @@ candidate_dirs() {
|
|
|
129
133
|
dir="$live/node_modules/@macintacos/caret"
|
|
130
134
|
if [ -n "$live" ] && [ -d "$dir" ]; then printf '%s\n' "$dir"; fi
|
|
131
135
|
done
|
|
136
|
+
if [ -d "$pi_root" ]; then printf '%s\n' "$pi_root"; fi
|
|
132
137
|
for dir in "$owned_roots"/*; do
|
|
133
138
|
if [ -d "$dir" ]; then printf '%s\n' "$dir"; fi
|
|
134
139
|
done
|
|
@@ -1,16 +1,121 @@
|
|
|
1
1
|
// The bridge from an agent's plan-review tool to `caret review`, shared by the OpenCode
|
|
2
|
-
// plugin
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// and
|
|
2
|
+
// plugin, `caret mcp` and pi's extension: resolve the plan and the caret binary, build
|
|
3
|
+
// caret's review envelope, warm the daemon, spawn the review, and turn the flat decision
|
|
4
|
+
// it prints into the tool's result text; it also holds the detached opener both TUIs use
|
|
5
|
+
// for `band-link --open`. It ships as unbundled source the OpenCode plugin and pi's
|
|
6
|
+
// extension reach by relative import, and the CLI bundles it, so it imports node builtins
|
|
7
|
+
// only: nothing from src/, and no plugin SDK. It also holds PLAN_TITLE_INSTRUCTION, since
|
|
8
|
+
// this is the one module the hosts, `caret mcp` and `caret steer` can all import.
|
|
8
9
|
|
|
9
10
|
import { spawn } from "node:child_process";
|
|
11
|
+
import { readFileSync, statSync } from "node:fs";
|
|
12
|
+
import { constants } from "node:os";
|
|
13
|
+
import { resolve } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
10
15
|
|
|
16
|
+
/** `failsafe` is set only on a deny caret made itself because the review could not run,
|
|
17
|
+
* never on a reviewer's. */
|
|
11
18
|
export type CaretDecision =
|
|
12
19
|
| { behavior: "allow"; feedback?: string; acceptMode?: string }
|
|
13
|
-
| { behavior: "deny"; feedback: string };
|
|
20
|
+
| { behavior: "deny"; feedback: string; failsafe?: true };
|
|
21
|
+
|
|
22
|
+
/** The plan-review tool the agent calls. */
|
|
23
|
+
export const REVIEW_TOOL = "caret_review_plan";
|
|
24
|
+
|
|
25
|
+
// The review tool's model-facing text, shared so the hosts' registrations stay
|
|
26
|
+
// word-for-word identical (plugin-v2.test.ts and test/pi/tool.test.ts pin it per host).
|
|
27
|
+
export const REVIEW_TOOL_DESCRIPTION =
|
|
28
|
+
"Submit the current plan to caret for human review in a local browser UI. For plans only: caret presents what it receives as a plan, so do not use it for other documents or questions. Pass exactly one of `path` (preferred: a markdown file you write once and revise with edits) or `plan` (the plan inline). Blocks until the user approves or requests changes. On a change request, follow the result's instructions and call this tool again. Do not implement the plan until a call returns an approval.";
|
|
29
|
+
|
|
30
|
+
export const PLAN_ARG_DESCRIPTION =
|
|
31
|
+
"The complete plan, as markdown, to present for human review. The inline alternative to `path`.";
|
|
32
|
+
|
|
33
|
+
/** The caret binary a host spawns. The `overrideVar` env override wins; else
|
|
34
|
+
* `../bin/caret` beside the calling module's `importMetaUrl`. */
|
|
35
|
+
export function resolveCaretBin(opts: {
|
|
36
|
+
env: Record<string, string | undefined>;
|
|
37
|
+
importMetaUrl: string;
|
|
38
|
+
overrideVar: string;
|
|
39
|
+
}): string {
|
|
40
|
+
const override = opts.env[opts.overrideVar]?.trim();
|
|
41
|
+
if (override) return override;
|
|
42
|
+
return fileURLToPath(new URL("../bin/caret", opts.importMetaUrl));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `err`'s message, or `err` stringified when it is not an Error. A twin of
|
|
46
|
+
* `errorMessage` in src/lib/types.ts, which this module cannot import. */
|
|
47
|
+
export function errorMessage(err: unknown): string {
|
|
48
|
+
return err instanceof Error ? err.message : String(err);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Semver triple `[major, minor, patch]`, or null when `v` is not `X.Y.Z` (an
|
|
52
|
+
* optional leading `v` is stripped; trailing prerelease/build metadata ignored). */
|
|
53
|
+
export function parseVersionTriple(v: string): [number, number, number] | null {
|
|
54
|
+
const m = v
|
|
55
|
+
.trim()
|
|
56
|
+
.replace(/^v/, "")
|
|
57
|
+
.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
58
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** True when `latest` is a strictly higher semver than `current`. Inline (no semver
|
|
62
|
+
* dep) so the deployed plugin stays self-contained; an unparseable version on
|
|
63
|
+
* either side compares false, so the check never nags on something it can't read. */
|
|
64
|
+
export function isNewer(latest: string, current: string): boolean {
|
|
65
|
+
const a = parseVersionTriple(latest);
|
|
66
|
+
const b = parseVersionTriple(current);
|
|
67
|
+
if (!a || !b) return false;
|
|
68
|
+
const [a0, a1, a2] = a;
|
|
69
|
+
const [b0, b1, b2] = b;
|
|
70
|
+
if (a0 !== b0) return a0 > b0;
|
|
71
|
+
if (a1 !== b1) return a1 > b1;
|
|
72
|
+
return a2 > b2;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type PlanSource = { plan: string; planFilePath?: string };
|
|
76
|
+
|
|
77
|
+
/** The plan the review tool submits, from exactly one of its `plan` / `path` args (an
|
|
78
|
+
* empty string counts as absent). A `path` resolves against the session directory and
|
|
79
|
+
* must be a readable `.md` file — mirroring the core's `isPlanFile`
|
|
80
|
+
* (src/plan/canonical-file.ts), repeated here because this module cannot import src/.
|
|
81
|
+
* `readFile` returns the text, or undefined when absPath is not a readable regular
|
|
82
|
+
* file. */
|
|
83
|
+
export function resolvePlanSource(
|
|
84
|
+
args: { plan?: string; path?: string },
|
|
85
|
+
directory: string,
|
|
86
|
+
readFile: (absPath: string) => string | undefined,
|
|
87
|
+
): PlanSource | { error: string } {
|
|
88
|
+
const plan = args.plan || undefined;
|
|
89
|
+
const path = args.path || undefined;
|
|
90
|
+
const notExactlyOne = {
|
|
91
|
+
error: `caret: ${REVIEW_TOOL} takes exactly one of \`plan\` or \`path\`. Pass the plan file as \`path\`, or the plan inline as \`plan\`.`,
|
|
92
|
+
};
|
|
93
|
+
if (path === undefined) return plan === undefined ? notExactlyOne : { plan };
|
|
94
|
+
if (plan !== undefined) return notExactlyOne;
|
|
95
|
+
const planFilePath = resolve(directory, path);
|
|
96
|
+
if (!planFilePath.endsWith(".md")) {
|
|
97
|
+
return {
|
|
98
|
+
error: `caret: ${REVIEW_TOOL} needs a markdown (.md) file as \`path\`, not ${planFilePath}. Write the plan to a .md file, or pass it inline as \`plan\`.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const text = readFile(planFilePath);
|
|
102
|
+
if (text === undefined) {
|
|
103
|
+
return {
|
|
104
|
+
error: `caret: ${REVIEW_TOOL} could not read ${planFilePath} as a regular file. Write the plan there first, or pass it inline as \`plan\`.`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return { plan: text, planFilePath };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The file's text, or undefined when `absPath` is not a readable regular file; never
|
|
111
|
+
* throws. */
|
|
112
|
+
export function readRegularFile(absPath: string): string | undefined {
|
|
113
|
+
try {
|
|
114
|
+
return statSync(absPath).isFile() ? readFileSync(absPath, "utf-8") : undefined;
|
|
115
|
+
} catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
14
119
|
|
|
15
120
|
/** Asks the model to open its plan on a `#` heading, which deriveTitle
|
|
16
121
|
* (src/review/threading.ts) prefers as the review title — keep the two in sync. Sent by
|
|
@@ -54,6 +159,7 @@ export function parseDecision(stdout: string): CaretDecision {
|
|
|
54
159
|
behavior?: unknown;
|
|
55
160
|
feedback?: unknown;
|
|
56
161
|
acceptMode?: unknown;
|
|
162
|
+
failsafe?: unknown;
|
|
57
163
|
};
|
|
58
164
|
if (parsed.behavior === "allow") {
|
|
59
165
|
// Reviewer notes (EXC-791) ride the allow; surface them to the agent below.
|
|
@@ -69,6 +175,7 @@ export function parseDecision(stdout: string): CaretDecision {
|
|
|
69
175
|
return {
|
|
70
176
|
behavior: "deny",
|
|
71
177
|
feedback: typeof parsed.feedback === "string" ? parsed.feedback : "Plan changes requested.",
|
|
178
|
+
...(parsed.failsafe === true && { failsafe: true }),
|
|
72
179
|
};
|
|
73
180
|
}
|
|
74
181
|
return failsafeDeny("caret: unrecognized review decision — denying to fail safe.");
|
|
@@ -78,7 +185,7 @@ export function parseDecision(stdout: string): CaretDecision {
|
|
|
78
185
|
}
|
|
79
186
|
|
|
80
187
|
function failsafeDeny(feedback: string): CaretDecision {
|
|
81
|
-
return { behavior: "deny", feedback };
|
|
188
|
+
return { behavior: "deny", feedback, failsafe: true };
|
|
82
189
|
}
|
|
83
190
|
|
|
84
191
|
/** Tool result returned to the agent on approval. Optional reviewer notes
|
|
@@ -166,7 +273,8 @@ export function parseReviewUrl(stderr: string): string | undefined {
|
|
|
166
273
|
* the child's stderr chunks as they arrive, so the caller can surface the review URL
|
|
167
274
|
* the core prints there while the review is still pending. An aborted `signal` must
|
|
168
275
|
* terminate the child and settle (reject) promptly: `caret mcp` refuses a new call until
|
|
169
|
-
* the aborted one settles.
|
|
276
|
+
* the aborted one settles. A child killed by a signal exits 128 + the signal number, as
|
|
277
|
+
* in a shell, so it never reads as success. */
|
|
170
278
|
export type SpawnRunner = (
|
|
171
279
|
command: string[],
|
|
172
280
|
env: Record<string, string | undefined>,
|
|
@@ -223,8 +331,7 @@ export async function runReviewViaCaret(
|
|
|
223
331
|
);
|
|
224
332
|
return parseDecision(stdout);
|
|
225
333
|
} catch (e) {
|
|
226
|
-
|
|
227
|
-
return failsafeDeny(`caret: review failed to run (${message}) — denying to fail safe.`);
|
|
334
|
+
return failsafeDeny(`caret: review failed to run (${errorMessage(e)}) — denying to fail safe.`);
|
|
228
335
|
}
|
|
229
336
|
}
|
|
230
337
|
|
|
@@ -248,10 +355,44 @@ export const nodeSpawnRunner: SpawnRunner = (command, env, stdin, onStderr, sign
|
|
|
248
355
|
});
|
|
249
356
|
child.stderr.on("data", (chunk) => onStderr?.(chunk.toString()));
|
|
250
357
|
child.on("error", reject);
|
|
251
|
-
child.on("close", (code) =>
|
|
358
|
+
child.on("close", (code, killedBy) =>
|
|
359
|
+
resolve({ stdout, exitCode: code ?? 128 + (killedBy ? constants.signals[killedBy] : 0) }),
|
|
360
|
+
);
|
|
252
361
|
// A child killed before reading stdin EPIPEs the write; unhandled, that crashes the
|
|
253
362
|
// host. The 'error'/'close' handlers above already settle.
|
|
254
363
|
child.stdin.on("error", () => {});
|
|
255
364
|
child.stdin.write(stdin);
|
|
256
365
|
child.stdin.end();
|
|
257
366
|
});
|
|
367
|
+
|
|
368
|
+
/** Spawns `bin args` detached and unwaited; `onError` hears a child that fails to start
|
|
369
|
+
* or exits non-zero. */
|
|
370
|
+
export type DetachedSpawn = (bin: string, args: string[], onError: (err: Error) => void) => void;
|
|
371
|
+
|
|
372
|
+
/** A DetachedSpawn over node's `spawn` whose children get `env` laid over the inherited
|
|
373
|
+
* environment. */
|
|
374
|
+
export const detachedSpawnWith =
|
|
375
|
+
(env?: Record<string, string>): DetachedSpawn =>
|
|
376
|
+
(bin, args, onError) => {
|
|
377
|
+
// spawn reports ENOENT asynchronously; unhandled, that 'error' takes the host down.
|
|
378
|
+
const child = spawn(bin, args, {
|
|
379
|
+
stdio: "ignore",
|
|
380
|
+
detached: true,
|
|
381
|
+
env: env && { ...process.env, ...env },
|
|
382
|
+
});
|
|
383
|
+
child.on("error", onError);
|
|
384
|
+
// band-link exits 0 on its own failures, so non-zero means caret never ran.
|
|
385
|
+
child.on("exit", (code) => {
|
|
386
|
+
if (code) onError(new Error(`exited with status ${code}`));
|
|
387
|
+
});
|
|
388
|
+
child.unref();
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
export const nodeDetachedSpawn = detachedSpawnWith();
|
|
392
|
+
|
|
393
|
+
/** Warms the caret daemon: `caret prewarm`, detached and best-effort, so it never holds
|
|
394
|
+
* up the turn. CARET_AGENT rides along because this spawn is what stands the daemon up
|
|
395
|
+
* and the daemon picks its adapter from that env. */
|
|
396
|
+
export function warmCaret(bin: string, agent: string): void {
|
|
397
|
+
detachedSpawnWith({ CARET_AGENT: agent })(bin, ["prewarm"], () => {});
|
|
398
|
+
}
|
package/commands/doctor.md
CHANGED
|
@@ -44,7 +44,11 @@ one. What each id means:
|
|
|
44
44
|
| `daemon-lock` | the lock names a dead pid, or a port caret is not configured to bind | the check's remedy verbatim |
|
|
45
45
|
| `agent-install` | the agent the check names does not have caret enabled | `caret install` |
|
|
46
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-host` | an `opencode` on PATH is below its major's floor (1.3.4 for v1, 2.0.29 for v2), or caret sits in a key or config file that OpenCode doesn't load | the check's remedy verbatim |
|
|
47
48
|
| `opencode-caret-version` | OpenCode would load a caret behind the published one | the check's remedy verbatim |
|
|
49
|
+
| `pi-host` | `pi` can't run, is older than 1.5.0, or is the pre-rename `@mariozechner/pi-coding-agent` | the check's remedy verbatim |
|
|
50
|
+
| `pi-caret-entry` | pi lists caret more than once, filters out its extension, or lists it without its package installed | the check's remedy verbatim |
|
|
51
|
+
| `pi-caret-version` | pi would load a caret behind the published one | the check's remedy verbatim |
|
|
48
52
|
|
|
49
53
|
An `unknown` check is not a failure: it names its own `reason` — no network, an unreadable
|
|
50
54
|
config, or a report section that never collected, which also shows as an `error` line
|