@macintacos/caret 1.0.2 → 1.1.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.
Files changed (103) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +16 -9
  3. package/bin/caret +1 -1
  4. package/commands/doctor.md +139 -0
  5. package/commands/plan.md +25 -0
  6. package/dist/cli.js +1845 -821
  7. package/opencode/caret.plugin.ts +6 -5
  8. package/opencode/commands/doctor.md +120 -0
  9. package/opencode/commands/plan.md +24 -0
  10. package/package.json +1 -1
  11. package/ui/dist/assets/{angular-html-CNIx69MF.js → angular-html-BECXXPyr.js} +1 -1
  12. package/ui/dist/assets/{angular-ts-B6ob15kD.js → angular-ts-CJKAKY9F.js} +1 -1
  13. package/ui/dist/assets/{apl-BzQg5wKE.js → apl-CCcfDDqZ.js} +1 -1
  14. package/ui/dist/assets/{astro-DQ6PiTON.js → astro-DKCnAnVR.js} +1 -1
  15. package/ui/dist/assets/{blade-DdGMUzbN.js → blade-DQ_JdT2N.js} +1 -1
  16. package/ui/dist/assets/{c-DhrAH2di.js → c-D3TwszVj.js} +1 -1
  17. package/ui/dist/assets/{chapel-BIJH6n90.js → chapel-C-2sPcCX.js} +1 -1
  18. package/ui/dist/assets/{cobol-CR-dFJ1T.js → cobol-BPmo_4f_.js} +1 -1
  19. package/ui/dist/assets/{coffee-DWT27RO5.js → coffee-DMTsNDVt.js} +1 -1
  20. package/ui/dist/assets/{cpp-CuONTEvg.js → cpp-BJr6_Y8R.js} +1 -1
  21. package/ui/dist/assets/{crystal-Bvc0l-OM.js → crystal-pIWESUkD.js} +1 -1
  22. package/ui/dist/assets/{css-BYejQHVV.js → css-DRd9Q4Li.js} +1 -1
  23. package/ui/dist/assets/{dist-COogZ9jg.js → dist-BFQBMwmz.js} +1 -1
  24. package/ui/dist/assets/{dist-BgZhLp84.js → dist-BGfsV7n2.js} +1 -1
  25. package/ui/dist/assets/{dist-Cuerw0_4.js → dist-BJ7SLVNC.js} +1 -1
  26. package/ui/dist/assets/{dist-D-lJ6tIg.js → dist-BNr0c85A.js} +1 -1
  27. package/ui/dist/assets/{dist-C-FIXuZc.js → dist-C6t67BjE.js} +1 -1
  28. package/ui/dist/assets/{dist-Bn_ExXCc.js → dist-CD25BkNS.js} +1 -1
  29. package/ui/dist/assets/{dist-BbAsmjOe.js → dist-CU5aMlNo.js} +1 -1
  30. package/ui/dist/assets/{dist-CCPLuvXB.js → dist-DoJTqy3z.js} +1 -1
  31. package/ui/dist/assets/{dist-RKZBj-sl.js → dist-RKKeH1zs.js} +1 -1
  32. package/ui/dist/assets/{dist-DV7t-p4N.js → dist-aKlRxpD_.js} +1 -1
  33. package/ui/dist/assets/{edge-B_-8Eolr.js → edge-DeNk4F2G.js} +1 -1
  34. package/ui/dist/assets/{elixir-CAhwbI74.js → elixir-DHc-DOO8.js} +1 -1
  35. package/ui/dist/assets/{elm-yg-VSwEl.js → elm-CJRqEcvz.js} +1 -1
  36. package/ui/dist/assets/{erb-CIMOpWGO.js → erb-BEWGTHDA.js} +1 -1
  37. package/ui/dist/assets/{git-rebase-DI22snkc.js → git-rebase-DzlcWhg9.js} +1 -1
  38. package/ui/dist/assets/{glimmer-js-CZHZNAs-.js → glimmer-js-B-aWCRIZ.js} +1 -1
  39. package/ui/dist/assets/{glimmer-ts-C2rq-JGq.js → glimmer-ts-DcjmPyQn.js} +1 -1
  40. package/ui/dist/assets/{glsl-ByXN6hIN.js → glsl-C9FOT-Uv.js} +1 -1
  41. package/ui/dist/assets/{graphql-DiiJwLqR.js → graphql-BgKSoRp0.js} +1 -1
  42. package/ui/dist/assets/{hack-hQSSm1D1.js → hack-BOedpUUE.js} +1 -1
  43. package/ui/dist/assets/{haml-cf9hcOgw.js → haml-Cpngw1vd.js} +1 -1
  44. package/ui/dist/assets/{handlebars-B3tg892p.js → handlebars-CpW1IyBg.js} +1 -1
  45. package/ui/dist/assets/{html-CkIr2qlC.js → html-CV142D8s.js} +1 -1
  46. package/ui/dist/assets/{html-derivative-U1EOyk5J.js → html-derivative-Df7h1Nuw.js} +1 -1
  47. package/ui/dist/assets/{http-ek0-hDT8.js → http-Cneddv9N.js} +1 -1
  48. package/ui/dist/assets/{hurl-DbN52Bpk.js → hurl-D6TS55N6.js} +1 -1
  49. package/ui/dist/assets/{index-uBaTjG1U.js → index-Ci9yCNtV.js} +248 -214
  50. package/ui/dist/assets/index-t6i4na1R.css +1 -0
  51. package/ui/dist/assets/{java-DXEu0FGs.js → java-BicD3zQH.js} +1 -1
  52. package/ui/dist/assets/{javascript-Bi95ibpN.js → javascript-l6vKHWBV.js} +1 -1
  53. package/ui/dist/assets/{jinja-BcKnkqjY.js → jinja-BJF2iot2.js} +1 -1
  54. package/ui/dist/assets/{jison-CFaHQ730.js → jison-DiXPFJAT.js} +1 -1
  55. package/ui/dist/assets/{json--ulVurkL.js → json-BDRuI4UV.js} +1 -1
  56. package/ui/dist/assets/{jsx-ZonywBBD.js → jsx-BvfdkIOy.js} +1 -1
  57. package/ui/dist/assets/{julia-Bnt3bGqT.js → julia-bXAhreI5.js} +1 -1
  58. package/ui/dist/assets/{just-CBpil8Tz.js → just-BOxCfXDg.js} +1 -1
  59. package/ui/dist/assets/{latex-D-qwKUqq.js → latex-B9OiTBLF.js} +1 -1
  60. package/ui/dist/assets/{liquid-Bu8cI_b1.js → liquid-DuLKDeWE.js} +1 -1
  61. package/ui/dist/assets/{lua-DO_5Mymc.js → lua-Dbh9FQm_.js} +1 -1
  62. package/ui/dist/assets/{marko-DJfs6dcc.js → marko--tjEBgDM.js} +1 -1
  63. package/ui/dist/assets/{mdc-CnkU1WJT.js → mdc-ByNJpl3m.js} +1 -1
  64. package/ui/dist/assets/{nginx-B5LxXEA3.js → nginx-D4wsmFK9.js} +1 -1
  65. package/ui/dist/assets/{nim-DLTorTuD.js → nim-Cy8RxisP.js} +1 -1
  66. package/ui/dist/assets/{org-DnNvsXCU.js → org-CAm7o61d.js} +1 -1
  67. package/ui/dist/assets/{perl-Q5Q2rA31.js → perl-BAvaFGKe.js} +1 -1
  68. package/ui/dist/assets/{php-S65hjGBt.js → php-DpAf3w7u.js} +1 -1
  69. package/ui/dist/assets/{pug-k-MB8PEo.js → pug-Dm7otyJU.js} +1 -1
  70. package/ui/dist/assets/{qml-Bf9cLRPv.js → qml-D4rWQQKC.js} +1 -1
  71. package/ui/dist/assets/{r-Cr3hDQkZ.js → r-DMKUX8qf.js} +1 -1
  72. package/ui/dist/assets/{razor-BO5-wpmp.js → razor-BIUJkuf_.js} +1 -1
  73. package/ui/dist/assets/{regexp-ffQ9gsih.js → regexp-D8W_pVHj.js} +1 -1
  74. package/ui/dist/assets/{rst-B4AQC5xj.js → rst-aFr0DP5H.js} +1 -1
  75. package/ui/dist/assets/{ruby-Dr8kfvLX.js → ruby-BzH6kUKr.js} +1 -1
  76. package/ui/dist/assets/{sas-Kuo2pLeL.js → sas-CDk9DyCV.js} +1 -1
  77. package/ui/dist/assets/{scss-Ci63BQ8y.js → scss-DABbvU5W.js} +1 -1
  78. package/ui/dist/assets/{shellscript-DBh4gnVx.js → shellscript-B0ObP3K-.js} +1 -1
  79. package/ui/dist/assets/{shellsession-L5KFvHI3.js → shellsession-2sv3Jq0j.js} +1 -1
  80. package/ui/dist/assets/{soy-D6F5ilyd.js → soy-irSRV4oq.js} +1 -1
  81. package/ui/dist/assets/{sql-xwRblxjL.js → sql-DJCHoKvP.js} +1 -1
  82. package/ui/dist/assets/{stata-l8sBIGPT.js → stata-a8YnzbIc.js} +1 -1
  83. package/ui/dist/assets/{surrealql-DL2sOSzB.js → surrealql-DdXPOg8s.js} +1 -1
  84. package/ui/dist/assets/{svelte-Dp5d2Ja3.js → svelte-ClAKUEZR.js} +1 -1
  85. package/ui/dist/assets/{templ-B3LfgeSM.js → templ-NUCvvUBA.js} +1 -1
  86. package/ui/dist/assets/{tex-CfLQjgEV.js → tex-DwZeSAfC.js} +1 -1
  87. package/ui/dist/assets/{ts-tags-BCfi6Eh_.js → ts-tags-CFJEHYY_.js} +1 -1
  88. package/ui/dist/assets/{tsx-BXhT7ita.js → tsx-B934yX2T.js} +1 -1
  89. package/ui/dist/assets/{twig-t6tdMpRg.js → twig-NFkkOj3w.js} +1 -1
  90. package/ui/dist/assets/{typescript-CFIlDlxn.js → typescript-7bmMC_xU.js} +1 -1
  91. package/ui/dist/assets/{typst-CVQ9ief2.js → typst-BQEjp9tn.js} +1 -1
  92. package/ui/dist/assets/{vue-D92U7PL6.js → vue-JqD9ieOF.js} +1 -1
  93. package/ui/dist/assets/{vue-html-B_-4d7cG.js → vue-html-CT4XQRX1.js} +1 -1
  94. package/ui/dist/assets/{vue-vine-LaG8znAe.js → vue-vine-DIR27Dmn.js} +1 -1
  95. package/ui/dist/assets/{xml-DgAE0jfV.js → xml-DhsMbw-1.js} +1 -1
  96. package/ui/dist/assets/{xsl-DkD8aIzS.js → xsl-BvqsVUg9.js} +1 -1
  97. package/ui/dist/assets/{yaml-CEG92fN8.js → yaml-C8eWjK_Q.js} +1 -1
  98. package/ui/dist/index.html +2 -2
  99. package/commands/debug.md +0 -138
  100. package/commands/discovery.md +0 -35
  101. package/opencode/commands/debug.md +0 -59
  102. package/opencode/commands/discovery.md +0 -31
  103. package/ui/dist/assets/index-CAl4rX7_.css +0 -1
package/dist/cli.js CHANGED
@@ -419,6 +419,7 @@ var init_claude = __esm(() => {
419
419
  claudeAdapter = {
420
420
  id: "claude",
421
421
  approveVariants: APPROVE_VARIANTS,
422
+ restartHint: "Then run /reload-plugins or restart Claude Code.",
422
423
  parseHookInput(stdin) {
423
424
  const hook = parseHookStdin(stdin);
424
425
  return {
@@ -2983,212 +2984,6 @@ function isNewer(latest, current) {
2983
2984
  return a2 > b2;
2984
2985
  }
2985
2986
 
2986
- // src/adapters/opencode/upgrade.ts
2987
- import { existsSync as existsSync2, readFileSync as readFileSync3, rmSync } from "fs";
2988
- import { join as join6 } from "path";
2989
- function upgradeVerdict(input) {
2990
- const { entry, cached, published } = input;
2991
- if (published === null) {
2992
- return { kind: "unknown", reason: "could not reach npm for the published caret version" };
2993
- }
2994
- if (parseVersionTriple(published) === null) {
2995
- return { kind: "unknown", reason: `npm reported an unreadable version (${published})` };
2996
- }
2997
- if (entry === null)
2998
- return { kind: "fresh" };
2999
- const pinned = splitPluginSpecifier(entry).version;
3000
- if (pinned !== null && parseVersionTriple(pinned) !== null) {
3001
- return isNewer(published, pinned) ? { kind: "stale-pin", entry, pinned, published } : { kind: "current", version: pinned };
3002
- }
3003
- if (cached === null)
3004
- return { kind: "fresh" };
3005
- if (parseVersionTriple(cached) === null) {
3006
- return { kind: "unknown", reason: `OpenCode cached an unreadable version (${cached})` };
3007
- }
3008
- return isNewer(published, cached) ? { kind: "stale-cache", cached, published } : { kind: "current", version: cached };
3009
- }
3010
- function readCachedCaretVersion(dirs = existingOpencodeCachePackageDirs()) {
3011
- for (const d of dirs) {
3012
- try {
3013
- const deps = JSON.parse(readFileSync3(join6(d, "package.json"), "utf-8")).dependencies;
3014
- const v = deps?.[CARET_PACKAGE];
3015
- if (typeof v === "string" && v.length > 0)
3016
- return v;
3017
- } catch {}
3018
- }
3019
- return null;
3020
- }
3021
- function clearCachedCaret(dirs = existingOpencodeCachePackageDirs()) {
3022
- const cleared = [];
3023
- for (const d of dirs) {
3024
- if (!existsSync2(d))
3025
- continue;
3026
- rmSync(d, { recursive: true, force: true });
3027
- cleared.push(d);
3028
- }
3029
- return cleared;
3030
- }
3031
- var init_upgrade = __esm(() => {
3032
- init_config_plugin();
3033
- init_paths2();
3034
- });
3035
-
3036
- // src/adapters/opencode/install.ts
3037
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
3038
- import { join as join7 } from "path";
3039
- function readOpencodeInstallState() {
3040
- const dir = opencodeConfigDir();
3041
- if (!existsSync3(dir)) {
3042
- return { pluginVersion: "unknown", pluginEnabled: "unknown", hookInUserSettings: "unknown" };
3043
- }
3044
- const version = readCachedCaretVersion(existingOpencodeCachePackageDirs()) ?? "unknown";
3045
- return {
3046
- pluginVersion: version,
3047
- pluginEnabled: version !== "unknown",
3048
- hookInUserSettings: readCaretInPluginArray(dir)
3049
- };
3050
- }
3051
- function readCaretInPluginArray(dir) {
3052
- let sawConfig = false;
3053
- for (const name of CONFIG_FILENAMES) {
3054
- const path = join7(dir, name);
3055
- if (!existsSync3(path))
3056
- continue;
3057
- let cfg;
3058
- try {
3059
- cfg = parse3(readFileSync4(path, "utf-8"));
3060
- } catch {
3061
- continue;
3062
- }
3063
- if (cfg === undefined || cfg === null)
3064
- continue;
3065
- sawConfig = true;
3066
- const arr = cfg.plugin;
3067
- if (Array.isArray(arr) && arr.some((e) => typeof e === "string" && e.includes("caret"))) {
3068
- return true;
3069
- }
3070
- }
3071
- return sawConfig ? false : "unknown";
3072
- }
3073
- var init_install3 = __esm(() => {
3074
- init_main();
3075
- init_paths2();
3076
- init_upgrade();
3077
- });
3078
-
3079
- // src/adapters/opencode/skills.ts
3080
- import { readdir as readdir2 } from "fs/promises";
3081
- import { sep as sep2 } from "path";
3082
- async function commandNamesUnder(dir) {
3083
- let entries;
3084
- try {
3085
- entries = await readdir2(dir, { recursive: true });
3086
- } catch {
3087
- return [];
3088
- }
3089
- return entries.filter((rel) => rel.endsWith(".md")).map((rel) => rel.slice(0, -".md".length).split(sep2).join("/"));
3090
- }
3091
- async function readOpencodeCommands() {
3092
- const perDir = await Promise.all(commandDirs(opencodeConfigDir()).map(commandNamesUnder));
3093
- const names = new Set(perDir.flat());
3094
- return [...names].sort().map((name) => ({ name, origin: "command" }));
3095
- }
3096
- async function readOpencodeCommandDescription(name) {
3097
- const relative = `${name}.md`;
3098
- for (const dir of commandDirs(opencodeConfigDir())) {
3099
- const abs = await containedFile(dir, relative);
3100
- if (abs !== null)
3101
- return descriptionOf(abs);
3102
- }
3103
- return null;
3104
- }
3105
- var init_skills2 = __esm(() => {
3106
- init_paths2();
3107
- init_skill_doc();
3108
- });
3109
-
3110
- // src/adapters/opencode/index.ts
3111
- var opencodeAdapter;
3112
- var init_opencode = __esm(() => {
3113
- init_approve3();
3114
- init_feedback3();
3115
- init_install3();
3116
- init_skills2();
3117
- opencodeAdapter = {
3118
- id: "opencode",
3119
- approveVariants: APPROVE_VARIANTS3,
3120
- parseHookInput(stdin) {
3121
- const hook = parseHookStdin(stdin);
3122
- return {
3123
- sessionId: hook.session_id,
3124
- cwd: hook.cwd,
3125
- plan: hook.tool_input?.plan,
3126
- planFilePath: hook.tool_input?.planFilePath
3127
- };
3128
- },
3129
- emitDecision(decision) {
3130
- return JSON.stringify(toWireDecision(decision));
3131
- },
3132
- fatalDenyLine,
3133
- readInstallState() {
3134
- return readOpencodeInstallState();
3135
- },
3136
- listSkills() {
3137
- return readOpencodeCommands();
3138
- },
3139
- readSkillDescription(_cwd, skill) {
3140
- return readOpencodeCommandDescription(skill.name);
3141
- }
3142
- };
3143
- });
3144
-
3145
- // src/adapters/index.ts
3146
- function agentIds() {
3147
- return Object.keys(REGISTRY);
3148
- }
3149
- function selectAdapter(id) {
3150
- const raw = (id ?? process.env.CARET_AGENT ?? "").trim();
3151
- const key = raw === "" ? DEFAULT_AGENT : raw;
3152
- const adapter = REGISTRY[key];
3153
- if (!adapter) {
3154
- throw new Error(`unknown agent adapter: "${key}" (known: ${agentIds().join(", ")})`);
3155
- }
3156
- return adapter;
3157
- }
3158
- function fatalDeny(reason) {
3159
- let adapter;
3160
- try {
3161
- adapter = selectAdapter();
3162
- } catch {}
3163
- if (adapter) {
3164
- try {
3165
- return adapter.emitDecision({ behavior: "deny", feedback: reason, decidedAt: Date.now() });
3166
- } catch {}
3167
- try {
3168
- return adapter.fatalDenyLine(reason);
3169
- } catch {}
3170
- }
3171
- return permissionRequestDenyLine(reason);
3172
- }
3173
- var DEFAULT_AGENT = "claude", CLAUDE_MCP_AGENT = "claude-mcp", REGISTRY;
3174
- var init_adapters = __esm(() => {
3175
- init_claude();
3176
- init_codex();
3177
- init_opencode();
3178
- REGISTRY = {
3179
- claude: claudeAdapter,
3180
- codex: codexAdapter,
3181
- opencode: opencodeAdapter,
3182
- [CLAUDE_MCP_AGENT]: {
3183
- ...claudeAdapter,
3184
- id: CLAUDE_MCP_AGENT,
3185
- parseHookInput: opencodeAdapter.parseHookInput,
3186
- emitDecision: opencodeAdapter.emitDecision,
3187
- fatalDenyLine: opencodeAdapter.fatalDenyLine
3188
- }
3189
- };
3190
- });
3191
-
3192
2987
  // node_modules/.bun/zod@4.5.4/node_modules/zod/v4/core/util.js
3193
2988
  function getEnumValues(entries) {
3194
2989
  const numericValues = Object.values(entries).filter((v) => typeof v === "number");
@@ -4105,7 +3900,7 @@ var cuid, cuid2, ulid, xid, ksuid, nanoid, duration, guid, uuid = (version) => {
4105
3900
  }, email, _emoji = `^[\\p{Extended_Pictographic}\\p{Emoji_Component}]+$`, ipv4, ipv6, cidrv4, cidrv6, base64, base64url, httpProtocol, e164, dateSource = `(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))`, date, string = (params) => {
4106
3901
  const regex = params ? `[\\s\\S]{${params?.minimum ?? 0},${params?.maximum ?? ""}}` : `[\\s\\S]*`;
4107
3902
  return new RegExp(`^${regex}$`);
4108
- }, integer, number, boolean, _null, lowercase, uppercase;
3903
+ }, bigint, integer, number, boolean, _null, lowercase, uppercase;
4109
3904
  var init_regexes = __esm(() => {
4110
3905
  cuid = /^[cC][0-9a-z]{6,}$/;
4111
3906
  cuid2 = /^[0-9a-z]+$/;
@@ -4125,6 +3920,7 @@ var init_regexes = __esm(() => {
4125
3920
  httpProtocol = /^https?$/;
4126
3921
  e164 = /^\+[1-9]\d{6,14}$/;
4127
3922
  date = /* @__PURE__ */ anchor(dateSource);
3923
+ bigint = /^-?\d+n?$/;
4128
3924
  integer = /^-?\d+$/;
4129
3925
  number = /^-?\d+(?:\.\d+)?$/;
4130
3926
  boolean = /^(?:true|false)$/i;
@@ -4955,7 +4751,7 @@ function handleRefineResult(result, payload, input, inst) {
4955
4751
  payload.issues.push(issue(_iss));
4956
4752
  }
4957
4753
  }
4958
- var $ZodType, toStandardResult = (r) => r.success ? { value: r.data } : { issues: r.error?.issues }, $ZodString, $ZodStringFormat, $ZodGUID, $ZodUUID, $ZodEmail, URL_BAD_FORMAT = 1, URL_UNPARSEABLE = 2, asciiTabOrNewline, $ZodURL, $ZodEmoji, $ZodNanoID, $ZodCUID, $ZodCUID2, $ZodULID, $ZodXID, $ZodKSUID, $ZodISODateTime, $ZodISODate, $ZodISOTime, $ZodISODuration, $ZodIPv4, ipv6Alphabet, $ZodIPv6, $ZodCIDRv4, $ZodCIDRv6, $ZodBase64, $ZodBase64URL, $ZodE164, $ZodJWT, $ZodNumber, $ZodNumberFormat, $ZodBoolean, $ZodNull, $ZodUnknown, $ZodNever, $ZodArray, NO_SYMBOL_KEYS, propShapes, $ZodObject, $ZodObjectJIT, $ZodUnion, $ZodDiscriminatedUnion, $ZodIntersection, $ZodRecord, $ZodEnum, $ZodLiteral, $ZodTransform, $ZodOptional, $ZodExactOptional, $ZodNullable, $ZodDefault, $ZodPrefault, $ZodNonOptional, $ZodCatch, $ZodPipe, $ZodPreprocess, $ZodReadonly, $ZodCustom;
4754
+ var $ZodType, toStandardResult = (r) => r.success ? { value: r.data } : { issues: r.error?.issues }, $ZodString, $ZodStringFormat, $ZodGUID, $ZodUUID, $ZodEmail, URL_BAD_FORMAT = 1, URL_UNPARSEABLE = 2, asciiTabOrNewline, $ZodURL, $ZodEmoji, $ZodNanoID, $ZodCUID, $ZodCUID2, $ZodULID, $ZodXID, $ZodKSUID, $ZodISODateTime, $ZodISODate, $ZodISOTime, $ZodISODuration, $ZodIPv4, ipv6Alphabet, $ZodIPv6, $ZodCIDRv4, $ZodCIDRv6, $ZodBase64, $ZodBase64URL, $ZodE164, $ZodJWT, $ZodNumber, $ZodNumberFormat, $ZodBoolean, $ZodBigInt, $ZodNull, $ZodUnknown, $ZodNever, $ZodDate, $ZodArray, NO_SYMBOL_KEYS, propShapes, $ZodObject, $ZodObjectJIT, $ZodUnion, $ZodDiscriminatedUnion, $ZodIntersection, $ZodRecord, $ZodEnum, $ZodLiteral, $ZodTransform, $ZodOptional, $ZodExactOptional, $ZodNullable, $ZodDefault, $ZodPrefault, $ZodNonOptional, $ZodCatch, $ZodPipe, $ZodPreprocess, $ZodReadonly, $ZodCustom;
4959
4755
  var init_schemas = __esm(() => {
4960
4756
  init_checks();
4961
4757
  init_core();
@@ -5376,6 +5172,25 @@ var init_schemas = __esm(() => {
5376
5172
  return payload;
5377
5173
  };
5378
5174
  });
5175
+ $ZodBigInt = /* @__PURE__ */ $constructor("$ZodBigInt", (inst, def) => {
5176
+ $ZodType.init(inst, def);
5177
+ inst._zod.pattern = bigint;
5178
+ inst._zod.parse = (payload, _ctx) => {
5179
+ if (def.coerce)
5180
+ try {
5181
+ payload.value = BigInt(payload.value);
5182
+ } catch (_) {}
5183
+ if (typeof payload.value === "bigint")
5184
+ return payload;
5185
+ payload.issues.push({
5186
+ expected: "bigint",
5187
+ code: "invalid_type",
5188
+ input: payload.value,
5189
+ inst
5190
+ });
5191
+ return payload;
5192
+ };
5193
+ });
5379
5194
  $ZodNull = /* @__PURE__ */ $constructor("$ZodNull", (inst, def) => {
5380
5195
  $ZodType.init(inst, def);
5381
5196
  inst._zod.pattern = _null;
@@ -5409,6 +5224,29 @@ var init_schemas = __esm(() => {
5409
5224
  return payload;
5410
5225
  };
5411
5226
  });
5227
+ $ZodDate = /* @__PURE__ */ $constructor("$ZodDate", (inst, def) => {
5228
+ $ZodType.init(inst, def);
5229
+ inst._zod.parse = (payload, _ctx) => {
5230
+ if (def.coerce) {
5231
+ try {
5232
+ payload.value = new Date(payload.value);
5233
+ } catch (_err) {}
5234
+ }
5235
+ const input = payload.value;
5236
+ const isDate = input instanceof Date;
5237
+ const isValidDate = isDate && !Number.isNaN(input.getTime());
5238
+ if (isValidDate)
5239
+ return payload;
5240
+ payload.issues.push({
5241
+ expected: "date",
5242
+ code: "invalid_type",
5243
+ input,
5244
+ ...isDate ? { received: "Invalid Date" } : {},
5245
+ inst
5246
+ });
5247
+ return payload;
5248
+ };
5249
+ });
5412
5250
  $ZodArray = /* @__PURE__ */ $constructor("$ZodArray", (inst, def) => {
5413
5251
  $ZodType.init(inst, def);
5414
5252
  const memo = globalConfig.memoizer;
@@ -6620,6 +6458,13 @@ function _string(Class, params) {
6620
6458
  ...normalizeParams(params)
6621
6459
  });
6622
6460
  }
6461
+ function _coercedString(Class, params) {
6462
+ return new Class({
6463
+ type: "string",
6464
+ coerce: true,
6465
+ ...normalizeParams(params)
6466
+ });
6467
+ }
6623
6468
  function _email(Class, params) {
6624
6469
  return new Class({
6625
6470
  type: "string",
@@ -6864,6 +6709,14 @@ function _number(Class, params) {
6864
6709
  ...normalizeParams(params)
6865
6710
  });
6866
6711
  }
6712
+ function _coercedNumber(Class, params) {
6713
+ return new Class({
6714
+ type: "number",
6715
+ coerce: true,
6716
+ checks: [],
6717
+ ...normalizeParams(params)
6718
+ });
6719
+ }
6867
6720
  function _int(Class, params) {
6868
6721
  return new Class({
6869
6722
  type: "number",
@@ -6879,6 +6732,20 @@ function _boolean(Class, params) {
6879
6732
  ...normalizeParams(params)
6880
6733
  });
6881
6734
  }
6735
+ function _coercedBoolean(Class, params) {
6736
+ return new Class({
6737
+ type: "boolean",
6738
+ coerce: true,
6739
+ ...normalizeParams(params)
6740
+ });
6741
+ }
6742
+ function _coercedBigint(Class, params) {
6743
+ return new Class({
6744
+ type: "bigint",
6745
+ coerce: true,
6746
+ ...normalizeParams(params)
6747
+ });
6748
+ }
6882
6749
  function _null2(Class, params) {
6883
6750
  return new Class({
6884
6751
  type: "null",
@@ -6896,6 +6763,13 @@ function _never(Class, params) {
6896
6763
  ...normalizeParams(params)
6897
6764
  });
6898
6765
  }
6766
+ function _coercedDate(Class, params) {
6767
+ return new Class({
6768
+ type: "date",
6769
+ coerce: true,
6770
+ ...normalizeParams(params)
6771
+ });
6772
+ }
6899
6773
  function _lt(value, params) {
6900
6774
  return new $ZodCheckLessThan({
6901
6775
  check: "less_than",
@@ -8517,7 +8391,7 @@ function preprocess(fn, schema) {
8517
8391
  out: schema
8518
8392
  });
8519
8393
  }
8520
- var ZodType, _ZodString, ZodString, ZodStringFormat, ZodISODateTime, ZodISODate, ZodISOTime, ZodISODuration, ZodEmail, ZodGUID, ZodUUID, ZodURL, ZodEmoji, ZodNanoID, ZodCUID, ZodCUID2, ZodULID, ZodXID, ZodKSUID, ZodIPv4, ZodIPv6, ZodCIDRv4, ZodCIDRv6, ZodBase64, ZodBase64URL, ZodE164, ZodJWT, ZodNumber, ZodNumberFormat, ZodBoolean, ZodNull, ZodUnknown, ZodNever, ZodArray, ZodObject, ZodUnion, ZodDiscriminatedUnion, ZodIntersection, ZodRecord, ZodEnum, ZodLiteral, ZodTransform, ZodOptional, ZodExactOptional, ZodNullable, ZodDefault, ZodPrefault, ZodNonOptional, ZodCatch, ZodPipe, ZodPreprocess, ZodReadonly, ZodCustom;
8394
+ var ZodType, _ZodString, ZodString, ZodStringFormat, ZodISODateTime, ZodISODate, ZodISOTime, ZodISODuration, ZodEmail, ZodGUID, ZodUUID, ZodURL, ZodEmoji, ZodNanoID, ZodCUID, ZodCUID2, ZodULID, ZodXID, ZodKSUID, ZodIPv4, ZodIPv6, ZodCIDRv4, ZodCIDRv6, ZodBase64, ZodBase64URL, ZodE164, ZodJWT, ZodNumber, ZodNumberFormat, ZodBoolean, ZodBigInt, ZodNull, ZodUnknown, ZodNever, ZodDate, ZodArray, ZodObject, ZodUnion, ZodDiscriminatedUnion, ZodIntersection, ZodRecord, ZodEnum, ZodLiteral, ZodTransform, ZodOptional, ZodExactOptional, ZodNullable, ZodDefault, ZodPrefault, ZodNonOptional, ZodCatch, ZodPipe, ZodPreprocess, ZodReadonly, ZodCustom;
8521
8395
  var init_schemas2 = __esm(() => {
8522
8396
  init_core2();
8523
8397
  init_core2();
@@ -8991,6 +8865,49 @@ var init_schemas2 = __esm(() => {
8991
8865
  ZodType.init(inst, def);
8992
8866
  inst._zod.processJSONSchema = (ctx, json, params) => booleanProcessor(inst, ctx, json, params);
8993
8867
  });
8868
+ ZodBigInt = /* @__PURE__ */ $constructor("ZodBigInt", (inst, def) => {
8869
+ $ZodBigInt.init(inst, def);
8870
+ ZodType.init(inst, def);
8871
+ inst._zod.processJSONSchema = (ctx, json, params) => bigintProcessor(inst, ctx, json, params);
8872
+ const bag = inst._zod.bag;
8873
+ inst.minValue = bag.minimum ?? null;
8874
+ inst.maxValue = bag.maximum ?? null;
8875
+ inst.format = bag.format ?? null;
8876
+ }, {
8877
+ gte(value, params) {
8878
+ return this.check(_gte(value, params));
8879
+ },
8880
+ min(value, params) {
8881
+ return this.check(_gte(value, params));
8882
+ },
8883
+ gt(value, params) {
8884
+ return this.check(_gt(value, params));
8885
+ },
8886
+ lt(value, params) {
8887
+ return this.check(_lt(value, params));
8888
+ },
8889
+ lte(value, params) {
8890
+ return this.check(_lte(value, params));
8891
+ },
8892
+ max(value, params) {
8893
+ return this.check(_lte(value, params));
8894
+ },
8895
+ positive(params) {
8896
+ return this.check(_gt(BigInt(0), params));
8897
+ },
8898
+ negative(params) {
8899
+ return this.check(_lt(BigInt(0), params));
8900
+ },
8901
+ nonpositive(params) {
8902
+ return this.check(_lte(BigInt(0), params));
8903
+ },
8904
+ nonnegative(params) {
8905
+ return this.check(_gte(BigInt(0), params));
8906
+ },
8907
+ multipleOf(value, params) {
8908
+ return this.check(_multipleOf(value, params));
8909
+ }
8910
+ });
8994
8911
  ZodNull = /* @__PURE__ */ $constructor("ZodNull", (inst, def) => {
8995
8912
  $ZodNull.init(inst, def);
8996
8913
  ZodType.init(inst, def);
@@ -9006,6 +8923,16 @@ var init_schemas2 = __esm(() => {
9006
8923
  ZodType.init(inst, def);
9007
8924
  inst._zod.processJSONSchema = (ctx, json, params) => neverProcessor(inst, ctx, json, params);
9008
8925
  });
8926
+ ZodDate = /* @__PURE__ */ $constructor("ZodDate", (inst, def) => {
8927
+ $ZodDate.init(inst, def);
8928
+ ZodType.init(inst, def);
8929
+ inst._zod.processJSONSchema = (ctx, json, params) => dateProcessor(inst, ctx, json, params);
8930
+ inst.min = (value, params) => inst.check(_gte(value, params));
8931
+ inst.max = (value, params) => inst.check(_lte(value, params));
8932
+ const c = inst._zod.bag;
8933
+ inst.minDate = c.minimum ? new Date(c.minimum) : null;
8934
+ inst.maxDate = c.maximum ? new Date(c.maximum) : null;
8935
+ });
9009
8936
  ZodArray = /* @__PURE__ */ $constructor("ZodArray", (inst, def) => {
9010
8937
  _ensureDefaultMemoizer();
9011
8938
  $ZodArray.init(inst, def);
@@ -9272,7 +9199,33 @@ var init_iso = __esm(() => {
9272
9199
  });
9273
9200
 
9274
9201
  // node_modules/.bun/zod@4.5.4/node_modules/zod/v4/classic/coerce.js
9275
- var init_coerce = () => {};
9202
+ var exports_coerce = {};
9203
+ __export(exports_coerce, {
9204
+ bigint: () => bigint2,
9205
+ boolean: () => boolean3,
9206
+ date: () => date2,
9207
+ number: () => number3,
9208
+ string: () => string3
9209
+ });
9210
+ function string3(params) {
9211
+ return _coercedString(ZodString, params);
9212
+ }
9213
+ function number3(params) {
9214
+ return _coercedNumber(ZodNumber, params);
9215
+ }
9216
+ function boolean3(params) {
9217
+ return _coercedBoolean(ZodBoolean, params);
9218
+ }
9219
+ function bigint2(params) {
9220
+ return _coercedBigint(ZodBigInt, params);
9221
+ }
9222
+ function date2(params) {
9223
+ return _coercedDate(ZodDate, params);
9224
+ }
9225
+ var init_coerce = __esm(() => {
9226
+ init_core2();
9227
+ init_schemas2();
9228
+ });
9276
9229
 
9277
9230
  // node_modules/.bun/zod@4.5.4/node_modules/zod/v4/classic/external.js
9278
9231
  var init_external = __esm(() => {
@@ -9293,6 +9246,429 @@ var init_zod = __esm(() => {
9293
9246
  init_external();
9294
9247
  });
9295
9248
 
9249
+ // package.json
9250
+ var package_default;
9251
+ var init_package = __esm(() => {
9252
+ package_default = {
9253
+ name: "@macintacos/caret",
9254
+ version: "1.1.0",
9255
+ license: "MIT",
9256
+ type: "module",
9257
+ description: "A Claude Code plugin that replaces the terminal plan-approval prompt with a local web UI for inline review, annotation, and approval.",
9258
+ main: "./opencode/index.ts",
9259
+ exports: {
9260
+ ".": "./opencode/index.ts"
9261
+ },
9262
+ bin: {
9263
+ caret: "./bin/caret"
9264
+ },
9265
+ homepage: "https://github.com/macintacos/caret",
9266
+ repository: {
9267
+ type: "git",
9268
+ url: "git+https://github.com/macintacos/caret.git"
9269
+ },
9270
+ publishConfig: {
9271
+ access: "public"
9272
+ },
9273
+ files: [
9274
+ "dist/",
9275
+ "ui/dist/",
9276
+ "bin/caret",
9277
+ "bin/caret-launcher",
9278
+ "hooks/",
9279
+ "commands/",
9280
+ "opencode/",
9281
+ "templates/",
9282
+ ".claude-plugin/plugin.json"
9283
+ ],
9284
+ scripts: {
9285
+ test: "bun test --conditions browser --parallel --timeout 35000",
9286
+ lint: "mise run lint",
9287
+ format: "mise run format",
9288
+ build: "mise run build"
9289
+ },
9290
+ devDependencies: {
9291
+ "@clack/prompts": "^1.7.0",
9292
+ "@codemirror/autocomplete": "^6.20.3",
9293
+ "@codemirror/commands": "^6.10.3",
9294
+ "@codemirror/lang-markdown": "^6.5.0",
9295
+ "@codemirror/language": "^6.12.3",
9296
+ "@codemirror/language-data": "^6.5.2",
9297
+ "@codemirror/state": "^6.6.0",
9298
+ "@codemirror/view": "^6.43.0",
9299
+ "@commander-js/extra-typings": "^15.0.0",
9300
+ "@happy-dom/global-registrator": "^20.11.2",
9301
+ "@internationalized/date": "^3.12.2",
9302
+ "@laststance/tailwind-suggest-canonical-classes": "^0.1.1",
9303
+ "@lezer/highlight": "^1.2.3",
9304
+ "@modelcontextprotocol/sdk": "^1.30.0",
9305
+ "@pierre/diffs": "1.3.6",
9306
+ "@pierre/trees": "1.0.0-beta.6",
9307
+ "@playwright/test": "^1.62.1",
9308
+ "@sveltejs/vite-plugin-svelte": "^7.3.0",
9309
+ "@tailwindcss/vite": "^4.3.3",
9310
+ "@types/bun": "^1.4.0",
9311
+ "@types/semver": "^7.7.1",
9312
+ "@typescript/native": "npm:typescript@^7.0.2",
9313
+ "bits-ui": "^2.18.1",
9314
+ clsx: "^2.1.1",
9315
+ commander: "^15.0.0",
9316
+ cuelume: "^0.2.2",
9317
+ "happy-dom": "^20.11.2",
9318
+ "jsonc-parser": "^3.3.1",
9319
+ listr2: "^11.0.0",
9320
+ marked: "^18.0.4",
9321
+ pino: "^10.3.1",
9322
+ "pino-pretty": "^13.1.3",
9323
+ semver: "^7.8.1",
9324
+ shiki: "^4.4.3",
9325
+ "smol-toml": "^1.8.0",
9326
+ svelte: "^5.56.9",
9327
+ "svelte-check": "^4.7.5",
9328
+ "tailwind-merge": "^3.6.0",
9329
+ "tailwind-variants": "^3.2.2",
9330
+ tailwindcss: "^4.3.3",
9331
+ "tw-animate-css": "^1.4.0",
9332
+ typescript: "^6.0.3",
9333
+ vite: "^8.2.1",
9334
+ xss: "^1.0.15",
9335
+ zod: "^4.4.3"
9336
+ },
9337
+ dependencies: {
9338
+ "@opencode-ai/plugin": "^1.18.17"
9339
+ },
9340
+ pinned: {
9341
+ "//": "bun.lock is committed, so it is what pins every dependency exactly; a range above is the upgrade policy, not the resolved version. A bare version therefore means `bun update` must never move this one on its own, and these entries are the whole set. Everything else takes a range deliberately \u2014 zod and the TOML parser reading the config.toml trust boundary included: the lockfile already holds them, so a range costs no reproducibility and keeps them visible to an upgrade sweep. test/structure/exact-pin.test.ts fails on an exact pin with no reason, and on a reason that outlived its pin.",
9342
+ "@pierre/diffs": "Version-coupled to shiki through the resolve.alias block in ui/vite.config.ts, where an alias that stops matching grows the bundle by ~600 KB. That growth is no longer silent \u2014 the caret-bundle-budget plugin fails `vite build` against the recorded budget in ui/bundle-budget.ts \u2014 but the gate reports the size, not the cause, so the pair still moves as one reviewed bump, never on a sweep.",
9343
+ "@pierre/trees": "No stable release exists \u2014 the `latest` and `beta` dist-tags both point at the same prerelease. Exact rather than caret because a prerelease under a caret range is still a caret range: `^1.0.0-beta.6` expands to `>=1.0.0-beta.6 <2.0.0-0`, so a sweep would take a stable 1.0.0 the day it lands, and beta-to-stable is the jump that wants a human."
9344
+ },
9345
+ held: {
9346
+ "//": "A dependency whose range deliberately stops below the current major, with the evidence for that and the condition that lifts it. Distinct from `pinned` above, which is about exact versions: an entry here names a range that is doing its job by excluding something. Unlike a pin, a held range is self-describing to `bun outdated` \u2014 it shows up as a major behind \u2014 so this block answers the question it raises rather than surfacing a decision nothing else would show.",
9347
+ typescript: "NOT a hold on adoption \u2014 TypeScript 7 IS what type-checks this tree (EXC-1089). It arrives as the `@typescript/native` alias above; hk.pkl runs the backend program through node_modules/@typescript/native/bin/tsc and the UI program through `svelte-check --tsgo`, and both were proved live against injected type errors rather than trusted because they exited 0. This range stops at ^6 because svelte-check requires BOTH majors installed: 4.7.6 hard-throws from bin/ts-version-check.js once `typescript` is major >= 7, and the recipe it throws is the arrangement used here verbatim \u2014 `typescript@~6 @typescript/native@npm:typescript@7`, plus --tsgo. Its README repeats it and dist/src/index.js resolves `@typescript/native` before `@typescript/native-preview`, so this is upstream's supported setup rather than a workaround. A second reason keeps 6.x honest even so: the test/structure/ suites that import `typescript` use its compiler API as a PARSING LIBRARY, and 7.x exports lib/version.cjs as `.` with the API behind ./unstable/* subpaths that offer no standalone parse (unstable/ast's createSourceFile is a factory over Statement[]; the only SourceFile producer is Program.getSourceFile). With 6.x installed those suites need no port, and `tsc` 7 type-checks them clean \u2014 the 50 errors EXC-1083 measured were never caret's code, only two files typed against a package whose `.` export had moved. What lifts this: a svelte-check release whose peerDependencies.typescript admits ^7 \u2014 but that clears only the first reason. Collapsing to a single `typescript@7` means dropping the alias, moving this range to ^7, AND porting those suites off the compiler API in the same pass, or they break on `ts.ScriptTarget` being undefined. test/structure/typescript-arrangement.test.ts reds on exactly that widening, and also pins the tsc path, the --tsgo flag, and this entry's own existence, because losing any of them silently reverts the adoption. Full evidence: EXC-1089, and the trial in EXC-1083."
9348
+ }
9349
+ };
9350
+ });
9351
+
9352
+ // src/lib/upstream.ts
9353
+ async function readJson(url, fetchImpl, headers) {
9354
+ try {
9355
+ const res = await fetchImpl(url, {
9356
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
9357
+ ...headers ? { headers } : {}
9358
+ });
9359
+ return res.ok ? await res.json() : null;
9360
+ } catch {
9361
+ return null;
9362
+ }
9363
+ }
9364
+ async function publishedCaretVersion(fetchImpl = fetch) {
9365
+ const body = await readJson(NPM_LATEST_URL, fetchImpl);
9366
+ const v = body?.version;
9367
+ return typeof v === "string" && v.length > 0 ? v : null;
9368
+ }
9369
+ async function latestReleaseTag(fetchImpl = fetch) {
9370
+ const body = await readJson(LATEST_RELEASE_URL, fetchImpl, GITHUB_HEADERS);
9371
+ const tag = body?.tag_name;
9372
+ if (typeof tag !== "string" || tag.length === 0)
9373
+ return null;
9374
+ const stripped = tag.replace(/^v/, "");
9375
+ return stripped.length > 0 ? stripped : null;
9376
+ }
9377
+ async function commitsAheadOfTrunk(commit, fetchImpl = fetch) {
9378
+ const url = `${COMPARE_URL}/${encodeURIComponent(commit)}...trunk`;
9379
+ const body = await readJson(url, fetchImpl, GITHUB_HEADERS);
9380
+ const ahead = body?.ahead_by;
9381
+ return typeof ahead === "number" && Number.isFinite(ahead) ? ahead : null;
9382
+ }
9383
+ async function readParsed(url, schema, fetchImpl) {
9384
+ const parsed = schema.safeParse(await readJson(url, fetchImpl, GITHUB_HEADERS));
9385
+ return parsed.success ? parsed.data : null;
9386
+ }
9387
+ function listReleases(fetchImpl = fetch) {
9388
+ return readParsed(RELEASES_URL, array(ReleaseSchema), fetchImpl);
9389
+ }
9390
+ function compareToTrunk(commit, fetchImpl = fetch) {
9391
+ return readParsed(`${COMPARE_URL}/${encodeURIComponent(commit)}...trunk`, CompareSchema, fetchImpl);
9392
+ }
9393
+ var NPM_LATEST_URL, LATEST_RELEASE_URL = "https://api.github.com/repos/macintacos/caret/releases/latest", COMPARE_URL = "https://api.github.com/repos/macintacos/caret/compare", RELEASES_URL = "https://api.github.com/repos/macintacos/caret/releases?per_page=100", UPSTREAM_TIMEOUT_MS = 3000, GITHUB_HEADERS, ReleaseSchema, CommitSchema, CompareSchema;
9394
+ var init_upstream = __esm(() => {
9395
+ init_zod();
9396
+ init_package();
9397
+ NPM_LATEST_URL = `https://registry.npmjs.org/${package_default.name}/latest`;
9398
+ GITHUB_HEADERS = { "user-agent": "caret" };
9399
+ ReleaseSchema = object({
9400
+ tag_name: string2(),
9401
+ body: string2().nullable(),
9402
+ draft: boolean2(),
9403
+ prerelease: boolean2()
9404
+ });
9405
+ CommitSchema = object({
9406
+ sha: string2(),
9407
+ commit: object({ message: string2() })
9408
+ });
9409
+ CompareSchema = object({
9410
+ total_commits: number2(),
9411
+ commits: array(CommitSchema)
9412
+ });
9413
+ });
9414
+
9415
+ // src/adapters/opencode/upgrade.ts
9416
+ import { existsSync as existsSync2, readFileSync as readFileSync3, rmSync } from "fs";
9417
+ import { join as join6 } from "path";
9418
+ function upgradeVerdict(input) {
9419
+ const { entry, cached, published } = input;
9420
+ if (published === null) {
9421
+ return { kind: "unknown", reason: "could not reach npm for the published caret version" };
9422
+ }
9423
+ if (parseVersionTriple(published) === null) {
9424
+ return { kind: "unknown", reason: `npm reported an unreadable version (${published})` };
9425
+ }
9426
+ if (entry === null)
9427
+ return { kind: "fresh" };
9428
+ const pinned = splitPluginSpecifier(entry).version;
9429
+ if (pinned !== null && parseVersionTriple(pinned) !== null) {
9430
+ return isNewer(published, pinned) ? { kind: "stale-pin", entry, pinned, published } : { kind: "current", version: pinned };
9431
+ }
9432
+ if (cached === null)
9433
+ return { kind: "fresh" };
9434
+ if (parseVersionTriple(cached) === null) {
9435
+ return { kind: "unknown", reason: `OpenCode cached an unreadable version (${cached})` };
9436
+ }
9437
+ return isNewer(published, cached) ? { kind: "stale-cache", cached, published } : { kind: "current", version: cached };
9438
+ }
9439
+ function upgradeVerdictLine(verdict) {
9440
+ switch (verdict.kind) {
9441
+ case "fresh":
9442
+ return "OpenCode will resolve caret on its next start";
9443
+ case "current":
9444
+ return `OpenCode's caret is ${verdict.version} \u2014 already current`;
9445
+ case "stale-cache":
9446
+ return `OpenCode's cached caret is ${verdict.cached}; ${verdict.published} is published`;
9447
+ case "stale-pin":
9448
+ return `Your config pins ${verdict.entry}; ${verdict.published} is published`;
9449
+ case "unknown":
9450
+ return "Could not check which caret OpenCode would load";
9451
+ }
9452
+ }
9453
+ function upgradeCheck(verdict) {
9454
+ const base = {
9455
+ id: "opencode-caret-version",
9456
+ title: "OpenCode's caret",
9457
+ detail: upgradeVerdictLine(verdict)
9458
+ };
9459
+ switch (verdict.kind) {
9460
+ case "unknown":
9461
+ return { ...base, status: "unknown", reason: verdict.reason };
9462
+ case "stale-cache":
9463
+ return {
9464
+ ...base,
9465
+ status: "fail",
9466
+ remedy: "run `caret install --refresh` to clear the cached copy"
9467
+ };
9468
+ case "stale-pin":
9469
+ return { ...base, status: "fail", remedy: "run `caret install --refresh` to bump the pin" };
9470
+ case "fresh":
9471
+ case "current":
9472
+ return { ...base, status: "pass" };
9473
+ }
9474
+ }
9475
+ async function readUpgradeVerdict(deps) {
9476
+ return upgradeVerdict({
9477
+ entry: findPluginEntry(readConfigText(deps.configFile), CARET_PACKAGE),
9478
+ cached: readCachedCaretVersion((deps.cacheDirs ?? existingOpencodeCachePackageDirs)()),
9479
+ published: await (deps.published ?? publishedCaretVersion)()
9480
+ });
9481
+ }
9482
+ function hasCaretPluginEntry(configFile) {
9483
+ return findPluginEntry(readConfigText(configFile), CARET_PACKAGE) !== null;
9484
+ }
9485
+ function readConfigText(path) {
9486
+ return existsSync2(path) ? readFileSync3(path, "utf-8") : null;
9487
+ }
9488
+ function readCachedCaretVersion(dirs = existingOpencodeCachePackageDirs()) {
9489
+ for (const d of dirs) {
9490
+ try {
9491
+ const deps = JSON.parse(readFileSync3(join6(d, "package.json"), "utf-8")).dependencies;
9492
+ const v = deps?.[CARET_PACKAGE];
9493
+ if (typeof v === "string" && v.length > 0)
9494
+ return v;
9495
+ } catch {}
9496
+ }
9497
+ return null;
9498
+ }
9499
+ function clearCachedCaret(dirs = existingOpencodeCachePackageDirs()) {
9500
+ const cleared = [];
9501
+ for (const d of dirs) {
9502
+ if (!existsSync2(d))
9503
+ continue;
9504
+ rmSync(d, { recursive: true, force: true });
9505
+ cleared.push(d);
9506
+ }
9507
+ return cleared;
9508
+ }
9509
+ var init_upgrade = __esm(() => {
9510
+ init_config_plugin();
9511
+ init_paths2();
9512
+ init_upstream();
9513
+ });
9514
+
9515
+ // src/adapters/opencode/install.ts
9516
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
9517
+ import { join as join7 } from "path";
9518
+ function readOpencodeInstallState() {
9519
+ const dir = opencodeConfigDir();
9520
+ if (!existsSync3(dir)) {
9521
+ return { pluginVersion: "unknown", pluginEnabled: "unknown", hookInUserSettings: "unknown" };
9522
+ }
9523
+ const version = readCachedCaretVersion(existingOpencodeCachePackageDirs()) ?? "unknown";
9524
+ return {
9525
+ pluginVersion: version,
9526
+ pluginEnabled: version !== "unknown",
9527
+ hookInUserSettings: readCaretInPluginArray(dir)
9528
+ };
9529
+ }
9530
+ function readCaretInPluginArray(dir) {
9531
+ let sawConfig = false;
9532
+ for (const name of CONFIG_FILENAMES) {
9533
+ const path = join7(dir, name);
9534
+ if (!existsSync3(path))
9535
+ continue;
9536
+ let cfg;
9537
+ try {
9538
+ cfg = parse3(readFileSync4(path, "utf-8"));
9539
+ } catch {
9540
+ continue;
9541
+ }
9542
+ if (cfg === undefined || cfg === null)
9543
+ continue;
9544
+ sawConfig = true;
9545
+ const arr = cfg.plugin;
9546
+ if (Array.isArray(arr) && arr.some((e) => typeof e === "string" && e.includes("caret"))) {
9547
+ return true;
9548
+ }
9549
+ }
9550
+ return sawConfig ? false : "unknown";
9551
+ }
9552
+ var init_install3 = __esm(() => {
9553
+ init_main();
9554
+ init_paths2();
9555
+ init_upgrade();
9556
+ });
9557
+
9558
+ // src/adapters/opencode/skills.ts
9559
+ import { readdir as readdir2 } from "fs/promises";
9560
+ import { sep as sep2 } from "path";
9561
+ async function commandNamesUnder(dir) {
9562
+ let entries;
9563
+ try {
9564
+ entries = await readdir2(dir, { recursive: true });
9565
+ } catch {
9566
+ return [];
9567
+ }
9568
+ return entries.filter((rel) => rel.endsWith(".md")).map((rel) => rel.slice(0, -".md".length).split(sep2).join("/"));
9569
+ }
9570
+ async function readOpencodeCommands() {
9571
+ const perDir = await Promise.all(commandDirs(opencodeConfigDir()).map(commandNamesUnder));
9572
+ const names = new Set(perDir.flat());
9573
+ return [...names].sort().map((name) => ({ name, origin: "command" }));
9574
+ }
9575
+ async function readOpencodeCommandDescription(name) {
9576
+ const relative = `${name}.md`;
9577
+ for (const dir of commandDirs(opencodeConfigDir())) {
9578
+ const abs = await containedFile(dir, relative);
9579
+ if (abs !== null)
9580
+ return descriptionOf(abs);
9581
+ }
9582
+ return null;
9583
+ }
9584
+ var init_skills2 = __esm(() => {
9585
+ init_paths2();
9586
+ init_skill_doc();
9587
+ });
9588
+
9589
+ // src/adapters/opencode/index.ts
9590
+ var opencodeAdapter;
9591
+ var init_opencode = __esm(() => {
9592
+ init_approve3();
9593
+ init_feedback3();
9594
+ init_install3();
9595
+ init_skills2();
9596
+ opencodeAdapter = {
9597
+ id: "opencode",
9598
+ approveVariants: APPROVE_VARIANTS3,
9599
+ restartHint: "Then restart OpenCode.",
9600
+ parseHookInput(stdin) {
9601
+ const hook = parseHookStdin(stdin);
9602
+ return {
9603
+ sessionId: hook.session_id,
9604
+ cwd: hook.cwd,
9605
+ plan: hook.tool_input?.plan,
9606
+ planFilePath: hook.tool_input?.planFilePath
9607
+ };
9608
+ },
9609
+ emitDecision(decision) {
9610
+ return JSON.stringify(toWireDecision(decision));
9611
+ },
9612
+ fatalDenyLine,
9613
+ readInstallState() {
9614
+ return readOpencodeInstallState();
9615
+ },
9616
+ listSkills() {
9617
+ return readOpencodeCommands();
9618
+ },
9619
+ readSkillDescription(_cwd, skill) {
9620
+ return readOpencodeCommandDescription(skill.name);
9621
+ }
9622
+ };
9623
+ });
9624
+
9625
+ // src/adapters/index.ts
9626
+ function agentIds() {
9627
+ return Object.keys(REGISTRY);
9628
+ }
9629
+ function selectAdapter(id) {
9630
+ const raw = (id ?? process.env.CARET_AGENT ?? "").trim();
9631
+ const key = raw === "" ? DEFAULT_AGENT : raw;
9632
+ const adapter = REGISTRY[key];
9633
+ if (!adapter) {
9634
+ throw new Error(`unknown agent adapter: "${key}" (known: ${agentIds().join(", ")})`);
9635
+ }
9636
+ return adapter;
9637
+ }
9638
+ function fatalDeny(reason) {
9639
+ let adapter;
9640
+ try {
9641
+ adapter = selectAdapter();
9642
+ } catch {}
9643
+ if (adapter) {
9644
+ try {
9645
+ return adapter.emitDecision({ behavior: "deny", feedback: reason, decidedAt: Date.now() });
9646
+ } catch {}
9647
+ try {
9648
+ return adapter.fatalDenyLine(reason);
9649
+ } catch {}
9650
+ }
9651
+ return permissionRequestDenyLine(reason);
9652
+ }
9653
+ var DEFAULT_AGENT = "claude", CLAUDE_MCP_AGENT = "claude-mcp", REGISTRY;
9654
+ var init_adapters = __esm(() => {
9655
+ init_claude();
9656
+ init_codex();
9657
+ init_opencode();
9658
+ REGISTRY = {
9659
+ claude: claudeAdapter,
9660
+ codex: codexAdapter,
9661
+ opencode: opencodeAdapter,
9662
+ [CLAUDE_MCP_AGENT]: {
9663
+ ...claudeAdapter,
9664
+ id: CLAUDE_MCP_AGENT,
9665
+ parseHookInput: opencodeAdapter.parseHookInput,
9666
+ emitDecision: opencodeAdapter.emitDecision,
9667
+ fatalDenyLine: opencodeAdapter.fatalDenyLine
9668
+ }
9669
+ };
9670
+ });
9671
+
9296
9672
  // src/config/constants.ts
9297
9673
  function hasKnownFileExtension(path) {
9298
9674
  const base = path.split("/").pop() ?? "";
@@ -9353,6 +9729,7 @@ var init_constants = __esm(() => {
9353
9729
  "php",
9354
9730
  "swift",
9355
9731
  "dart",
9732
+ "lua",
9356
9733
  "txt",
9357
9734
  "lock",
9358
9735
  "cfg",
@@ -9428,6 +9805,9 @@ function daemonStderrLogFile() {
9428
9805
  function daemonLock() {
9429
9806
  return `${stateDir()}/daemon.lock`;
9430
9807
  }
9808
+ function daemonBootMarker() {
9809
+ return `${stateDir()}/daemon.boot`;
9810
+ }
9431
9811
  function ensureStateDir(target = stateDir()) {
9432
9812
  mkdirSync(target, { recursive: true, mode: 448 });
9433
9813
  chmodSync(target, 448);
@@ -9513,109 +9893,6 @@ async function loadUiAssets() {
9513
9893
  }
9514
9894
  var init_assets = () => {};
9515
9895
 
9516
- // package.json
9517
- var package_default;
9518
- var init_package = __esm(() => {
9519
- package_default = {
9520
- name: "@macintacos/caret",
9521
- version: "1.0.2",
9522
- license: "MIT",
9523
- type: "module",
9524
- description: "A Claude Code plugin that replaces the terminal plan-approval prompt with a local web UI for inline review, annotation, and approval.",
9525
- main: "./opencode/index.ts",
9526
- exports: {
9527
- ".": "./opencode/index.ts"
9528
- },
9529
- bin: {
9530
- caret: "./bin/caret"
9531
- },
9532
- homepage: "https://github.com/macintacos/caret",
9533
- repository: {
9534
- type: "git",
9535
- url: "git+https://github.com/macintacos/caret.git"
9536
- },
9537
- publishConfig: {
9538
- access: "public"
9539
- },
9540
- files: [
9541
- "dist/",
9542
- "ui/dist/",
9543
- "bin/caret",
9544
- "bin/caret-launcher",
9545
- "hooks/",
9546
- "commands/",
9547
- "opencode/",
9548
- "templates/",
9549
- ".claude-plugin/plugin.json"
9550
- ],
9551
- scripts: {
9552
- test: "bun test --conditions browser --parallel --timeout 35000",
9553
- lint: "mise run lint",
9554
- format: "mise run format",
9555
- build: "mise run build"
9556
- },
9557
- devDependencies: {
9558
- "@clack/prompts": "^1.7.0",
9559
- "@codemirror/autocomplete": "^6.20.3",
9560
- "@codemirror/commands": "^6.10.3",
9561
- "@codemirror/lang-markdown": "^6.5.0",
9562
- "@codemirror/language": "^6.12.3",
9563
- "@codemirror/language-data": "^6.5.2",
9564
- "@codemirror/state": "^6.6.0",
9565
- "@codemirror/view": "^6.43.0",
9566
- "@commander-js/extra-typings": "^15.0.0",
9567
- "@happy-dom/global-registrator": "^20.11.2",
9568
- "@internationalized/date": "^3.12.2",
9569
- "@laststance/tailwind-suggest-canonical-classes": "^0.1.1",
9570
- "@lezer/highlight": "^1.2.3",
9571
- "@modelcontextprotocol/sdk": "^1.30.0",
9572
- "@pierre/diffs": "1.3.6",
9573
- "@pierre/trees": "1.0.0-beta.6",
9574
- "@playwright/test": "^1.62.1",
9575
- "@sveltejs/vite-plugin-svelte": "^7.3.0",
9576
- "@tailwindcss/vite": "^4.3.3",
9577
- "@types/bun": "^1.4.0",
9578
- "@types/semver": "^7.7.1",
9579
- "@typescript/native": "npm:typescript@^7.0.2",
9580
- "bits-ui": "^2.18.1",
9581
- clsx: "^2.1.1",
9582
- commander: "^15.0.0",
9583
- cuelume: "^0.2.2",
9584
- "happy-dom": "^20.11.2",
9585
- "jsonc-parser": "^3.3.1",
9586
- listr2: "^11.0.0",
9587
- marked: "^18.0.4",
9588
- pino: "^10.3.1",
9589
- "pino-pretty": "^13.1.3",
9590
- semver: "^7.8.1",
9591
- shiki: "^4.4.3",
9592
- "smol-toml": "^1.8.0",
9593
- svelte: "^5.56.9",
9594
- "svelte-check": "^4.7.5",
9595
- "tailwind-merge": "^3.6.0",
9596
- "tailwind-variants": "^3.2.2",
9597
- tailwindcss: "^4.3.3",
9598
- "tw-animate-css": "^1.4.0",
9599
- typescript: "^6.0.3",
9600
- vite: "^8.2.1",
9601
- xss: "^1.0.15",
9602
- zod: "^4.4.3"
9603
- },
9604
- dependencies: {
9605
- "@opencode-ai/plugin": "^1.18.17"
9606
- },
9607
- pinned: {
9608
- "//": "bun.lock is committed, so it is what pins every dependency exactly; a range above is the upgrade policy, not the resolved version. A bare version therefore means `bun update` must never move this one on its own, and these entries are the whole set. Everything else takes a range deliberately \u2014 zod and the TOML parser reading the config.toml trust boundary included: the lockfile already holds them, so a range costs no reproducibility and keeps them visible to an upgrade sweep. test/structure/exact-pin.test.ts fails on an exact pin with no reason, and on a reason that outlived its pin.",
9609
- "@pierre/diffs": "Version-coupled to shiki through the resolve.alias block in ui/vite.config.ts, where an alias that stops matching grows the bundle by ~600 KB. That growth is no longer silent \u2014 the caret-bundle-budget plugin fails `vite build` against the recorded budget in ui/bundle-budget.ts \u2014 but the gate reports the size, not the cause, so the pair still moves as one reviewed bump, never on a sweep.",
9610
- "@pierre/trees": "No stable release exists \u2014 the `latest` and `beta` dist-tags both point at the same prerelease. Exact rather than caret because a prerelease under a caret range is still a caret range: `^1.0.0-beta.6` expands to `>=1.0.0-beta.6 <2.0.0-0`, so a sweep would take a stable 1.0.0 the day it lands, and beta-to-stable is the jump that wants a human."
9611
- },
9612
- held: {
9613
- "//": "A dependency whose range deliberately stops below the current major, with the evidence for that and the condition that lifts it. Distinct from `pinned` above, which is about exact versions: an entry here names a range that is doing its job by excluding something. Unlike a pin, a held range is self-describing to `bun outdated` \u2014 it shows up as a major behind \u2014 so this block answers the question it raises rather than surfacing a decision nothing else would show.",
9614
- typescript: "NOT a hold on adoption \u2014 TypeScript 7 IS what type-checks this tree (EXC-1089). It arrives as the `@typescript/native` alias above; hk.pkl runs the backend program through node_modules/@typescript/native/bin/tsc and the UI program through `svelte-check --tsgo`, and both were proved live against injected type errors rather than trusted because they exited 0. This range stops at ^6 because svelte-check requires BOTH majors installed: 4.7.6 hard-throws from bin/ts-version-check.js once `typescript` is major >= 7, and the recipe it throws is the arrangement used here verbatim \u2014 `typescript@~6 @typescript/native@npm:typescript@7`, plus --tsgo. Its README repeats it and dist/src/index.js resolves `@typescript/native` before `@typescript/native-preview`, so this is upstream's supported setup rather than a workaround. A second reason keeps 6.x honest even so: the test/structure/ suites that import `typescript` use its compiler API as a PARSING LIBRARY, and 7.x exports lib/version.cjs as `.` with the API behind ./unstable/* subpaths that offer no standalone parse (unstable/ast's createSourceFile is a factory over Statement[]; the only SourceFile producer is Program.getSourceFile). With 6.x installed those suites need no port, and `tsc` 7 type-checks them clean \u2014 the 50 errors EXC-1083 measured were never caret's code, only two files typed against a package whose `.` export had moved. What lifts this: a svelte-check release whose peerDependencies.typescript admits ^7 \u2014 but that clears only the first reason. Collapsing to a single `typescript@7` means dropping the alias, moving this range to ^7, AND porting those suites off the compiler API in the same pass, or they break on `ts.ScriptTarget` being undefined. test/structure/typescript-arrangement.test.ts reds on exactly that widening, and also pins the tsc path, the --tsgo flag, and this entry's own existence, because losing any of them silently reverts the adoption. Full evidence: EXC-1089, and the trial in EXC-1083."
9615
- }
9616
- };
9617
- });
9618
-
9619
9896
  // src/lib/build-id.ts
9620
9897
  import { createHash } from "crypto";
9621
9898
  function buildKind(argv1 = process.argv[1]) {
@@ -11641,7 +11918,7 @@ var require_indexes = __commonJS(function(exports, module) {
11641
11918
 
11642
11919
  // node_modules/.bun/thread-stream@4.2.0/node_modules/thread-stream/index.js
11643
11920
  var require_thread_stream = __commonJS(function(exports, module) {
11644
- var __dirname = "/Users/juliantorres/GitLocal/Play/caret/trunk/node_modules/.bun/thread-stream@4.2.0/node_modules/thread-stream";
11921
+ var __dirname = "/Users/juliantorres/GitLocal/Play/caret/node_modules/.bun/thread-stream@4.2.0/node_modules/thread-stream";
11645
11922
  var { version } = require_package();
11646
11923
  var { EventEmitter } = __require("events");
11647
11924
  var { Worker } = __require("worker_threads");
@@ -12157,7 +12434,7 @@ var require_thread_stream = __commonJS(function(exports, module) {
12157
12434
 
12158
12435
  // node_modules/.bun/pino@10.3.1/node_modules/pino/lib/transport.js
12159
12436
  var require_transport = __commonJS(function(exports, module) {
12160
- var __dirname = "/Users/juliantorres/GitLocal/Play/caret/trunk/node_modules/.bun/pino@10.3.1/node_modules/pino/lib";
12437
+ var __dirname = "/Users/juliantorres/GitLocal/Play/caret/node_modules/.bun/pino@10.3.1/node_modules/pino/lib";
12161
12438
  var { createRequire } = __require("module");
12162
12439
  var { existsSync } = __require("fs");
12163
12440
  var getCallers = require_caller();
@@ -14294,9 +14571,10 @@ var init_node = __esm(() => {
14294
14571
  });
14295
14572
 
14296
14573
  // src/lib/log.ts
14297
- function wrap(logger, liveLevel, liveRedact, source, rotate) {
14574
+ function wrap(logger, opts) {
14575
+ const { liveLevel, liveRedact, source, rotate, bound } = opts;
14298
14576
  function fields(extra, step, redact) {
14299
- const out = scrubValue({ ...extra }, redact);
14577
+ const out = scrubValue(Object.assign({}, ...bound, extra), redact);
14300
14578
  out.step = step;
14301
14579
  if (out.source == null) {
14302
14580
  out.source = source;
@@ -14322,17 +14600,19 @@ function wrap(logger, liveLevel, liveRedact, source, rotate) {
14322
14600
  debug: (step, msg, extra) => emit("debug", step, msg, extra),
14323
14601
  info: (step, msg, extra) => emit("info", step, msg, extra),
14324
14602
  warn: (step, msg, extra) => emit("warn", step, msg, extra),
14325
- error(step, err, extra) {
14603
+ error(step, code, err, extra) {
14326
14604
  try {
14327
14605
  rotate?.();
14328
14606
  const r = liveRedact();
14329
- const f = fields(extra, step, r);
14607
+ const record = fields(extra, step, r);
14608
+ record.code = code;
14330
14609
  if (err instanceof Error)
14331
- f.err = scrubValue(import_pino.default.stdSerializers.errWithCause(err), r);
14610
+ record.err = scrubValue(import_pino.default.stdSerializers.errWithCause(err), r);
14332
14611
  const msg = errorMessage(err);
14333
- logger.error(f, r ? scrubString(msg) : msg);
14612
+ logger.error(record, r ? scrubString(msg) : msg);
14334
14613
  } catch {}
14335
- }
14614
+ },
14615
+ child: (ctx) => wrap(logger, { ...opts, bound: [...bound, ctx] })
14336
14616
  };
14337
14617
  }
14338
14618
  function createHookLogger(level, redact, limits) {
@@ -14341,7 +14621,14 @@ function createHookLogger(level, redact, limits) {
14341
14621
  ensureLogsDir();
14342
14622
  const dest = import_pino.default.destination({ dest: path, sync: true, mode: 384 });
14343
14623
  const rotate = () => rotateIfOversized(path, limits.maxSize(), limits.keep());
14344
- return { log: wrap(import_pino.default(pinoOpts, dest), level, redact, "hook", rotate), dest, path };
14624
+ const log = wrap(import_pino.default(pinoOpts, dest), {
14625
+ liveLevel: level,
14626
+ liveRedact: redact,
14627
+ source: "hook",
14628
+ rotate,
14629
+ bound: []
14630
+ });
14631
+ return { log, dest, path };
14345
14632
  } catch {
14346
14633
  return { log: noopLogger, dest: null, path };
14347
14634
  }
@@ -14349,14 +14636,14 @@ function createHookLogger(level, redact, limits) {
14349
14636
  function hook() {
14350
14637
  const path = logFile();
14351
14638
  if (hookState.instance && hookState.instance.path === path) {
14352
- return hookState.instance.log;
14639
+ return hookState.instance.log.child(hookState.context);
14353
14640
  }
14354
14641
  try {
14355
14642
  hookState.instance?.dest?.destroy();
14356
14643
  } catch {}
14357
14644
  const next = createHookLogger(() => hookState.level, () => hookState.redact, { maxSize: () => hookState.maxSize, keep: () => hookState.keep });
14358
14645
  hookState.instance = next.dest ? next : null;
14359
- return next.log;
14646
+ return next.log.child(hookState.context);
14360
14647
  }
14361
14648
  function setLogLevel(level) {
14362
14649
  hookState.level = level;
@@ -14368,6 +14655,9 @@ function setLogRotation(maxSize, keep) {
14368
14655
  hookState.maxSize = maxSize;
14369
14656
  hookState.keep = keep;
14370
14657
  }
14658
+ function setLogContext(ctx) {
14659
+ hookState.context = { ...ctx };
14660
+ }
14371
14661
  function logDebug(step, msg, extra) {
14372
14662
  hook().debug(step, msg, extra);
14373
14663
  }
@@ -14377,8 +14667,8 @@ function logInfo(step, msg, extra) {
14377
14667
  function logWarn(step, msg, extra) {
14378
14668
  hook().warn(step, msg, extra);
14379
14669
  }
14380
- function logError(step, err, ctx) {
14381
- hook().error(step, err, ctx);
14670
+ function logError(step, code, err, extra) {
14671
+ hook().error(step, code, err, extra);
14382
14672
  }
14383
14673
  function createDaemonLogger(level, dest = daemonLogFile(), redact = () => false, limits) {
14384
14674
  try {
@@ -14390,7 +14680,13 @@ function createDaemonLogger(level, dest = daemonLogFile(), redact = () => false,
14390
14680
  });
14391
14681
  const logger = import_pino.default({ ...pinoOpts, base: { pid: process.pid } }, target);
14392
14682
  const rotate = limits && typeof dest === "string" ? () => rotateIfOversized(dest, limits.maxSize(), limits.keep()) : undefined;
14393
- return wrap(logger, level, redact, "daemon", rotate);
14683
+ return wrap(logger, {
14684
+ liveLevel: level,
14685
+ liveRedact: redact,
14686
+ source: "daemon",
14687
+ rotate,
14688
+ bound: []
14689
+ });
14394
14690
  } catch {
14395
14691
  return noopLogger;
14396
14692
  }
@@ -14413,13 +14709,15 @@ var init_log = __esm(() => {
14413
14709
  debug: () => {},
14414
14710
  info: () => {},
14415
14711
  warn: () => {},
14416
- error: () => {}
14712
+ error: () => {},
14713
+ child: () => noopLogger
14417
14714
  };
14418
14715
  hookState = {
14419
14716
  level: "info",
14420
14717
  redact: false,
14421
14718
  maxSize: DEFAULT_LOG_MAX_SIZE,
14422
14719
  keep: DEFAULT_LOG_KEEP,
14720
+ context: {},
14423
14721
  instance: null
14424
14722
  };
14425
14723
  });
@@ -14438,7 +14736,7 @@ function freeze(s) {
14438
14736
  }
14439
14737
  function logValidationFailure(err) {
14440
14738
  const safe = err.issues.map((i) => `${i.path.length ? i.path.join(".") : "(root)"}: ${i.code}`).join("; ");
14441
- logError("settings", new Error(`config.toml ignored \u2014 invalid: ${safe}`));
14739
+ logError("settings", "config-invalid", new Error(`config.toml ignored \u2014 invalid: ${safe}`));
14442
14740
  }
14443
14741
  function parseAndValidate(text, isCompiled) {
14444
14742
  let raw;
@@ -14675,18 +14973,24 @@ async function postReview(baseUrl, input) {
14675
14973
  throw new Error(`POST /api/reviews failed: ${res.status}`);
14676
14974
  return await res.json();
14677
14975
  }
14678
- async function expireReview(baseUrl, id) {
14679
- const res = await fetch(`${baseUrl}/api/reviews/${id}/expire`, {
14976
+ function versionSearch(version) {
14977
+ return version === undefined ? "" : `?version=${version}`;
14978
+ }
14979
+ async function expireReview(baseUrl, id, version) {
14980
+ const res = await fetch(`${baseUrl}/api/reviews/${id}/expire${versionSearch(version)}`, {
14680
14981
  method: "POST",
14681
14982
  signal: AbortSignal.timeout(1000)
14682
14983
  });
14683
- if (!res.ok && res.status !== 404)
14984
+ if (!res.ok && res.status !== 404 && res.status !== 409) {
14684
14985
  throw new Error(`POST /expire failed: ${res.status}`);
14986
+ }
14685
14987
  }
14686
- async function longPoll(baseUrl, id) {
14687
- const res = await fetch(`${baseUrl}/api/reviews/${id}/decision`);
14988
+ async function longPoll(baseUrl, id, version) {
14989
+ const res = await fetch(`${baseUrl}/api/reviews/${id}/decision${versionSearch(version)}`);
14688
14990
  if (res.status === 204)
14689
14991
  return null;
14992
+ if (res.status === 409)
14993
+ return "superseded";
14690
14994
  if (!res.ok)
14691
14995
  throw new Error(`decision long-poll failed: ${res.status}`);
14692
14996
  return await res.json();
@@ -14714,9 +15018,12 @@ import {
14714
15018
  chmodSync as chmodSync2,
14715
15019
  constants,
14716
15020
  existsSync as existsSync6,
15021
+ linkSync,
14717
15022
  openSync as openSync2,
14718
15023
  readFileSync as readFileSync8,
14719
- unlinkSync as unlinkSync2
15024
+ renameSync as renameSync2,
15025
+ unlinkSync as unlinkSync2,
15026
+ writeFileSync as writeFileSync3
14720
15027
  } from "fs";
14721
15028
  import { join as join9, normalize } from "path";
14722
15029
  function isAddrInUse(e) {
@@ -14738,7 +15045,9 @@ async function ensureDaemon(deps, mode = "takeover") {
14738
15045
  let windowSpent = mode === "successor" ? await awaitDrained(deps, windowEnd) : false;
14739
15046
  let supervised;
14740
15047
  let spawnedPid;
15048
+ let waitingOn;
14741
15049
  for (let attempt = 0;attempt < timing.maxAttempts && timing.now() < deadline; attempt++) {
15050
+ waitingOn = undefined;
14742
15051
  windowSpent ||= timing.now() >= windowEnd;
14743
15052
  const h = await deps.health(deps.baseUrl);
14744
15053
  if (h && h.service === "caret") {
@@ -14783,17 +15092,27 @@ async function ensureDaemon(deps, mode = "takeover") {
14783
15092
  deps.removeLock();
14784
15093
  logDebug("spawn", "orphan daemon lock removed");
14785
15094
  }
14786
- if (spawnedPid === undefined || !deps.isAlive(spawnedPid)) {
15095
+ const claim = claimBoot(deps);
15096
+ if (claim.claimed && spawnedPid !== undefined && deps.isAlive(spawnedPid)) {
15097
+ deps.bootMarker.clear();
15098
+ } else if (claim.claimed) {
14787
15099
  try {
14788
15100
  spawnedPid = deps.spawn();
15101
+ deps.bootMarker.assign(spawnedPid);
14789
15102
  logDebug("spawn", "daemon spawned", { pid: spawnedPid });
14790
15103
  } catch (e) {
15104
+ deps.bootMarker.clear();
14791
15105
  if (!isAddrInUse(e))
14792
15106
  throw e;
14793
15107
  }
15108
+ } else if (claim.bootPid !== spawnedPid) {
15109
+ waitingOn = claim.bootPid;
14794
15110
  }
14795
15111
  await timing.backoff(attempt);
14796
15112
  }
15113
+ if (waitingOn !== undefined) {
15114
+ logDebug("spawn", "gave up waiting on a booting daemon", { bootPid: waitingOn });
15115
+ }
14797
15116
  const final = await deps.health(deps.baseUrl);
14798
15117
  if (final && final.service === "caret") {
14799
15118
  if (isForeignWorld(final, deps.currentStateDir))
@@ -14802,6 +15121,22 @@ async function ensureDaemon(deps, mode = "takeover") {
14802
15121
  }
14803
15122
  throw new Error("caret daemon did not become healthy in time");
14804
15123
  }
15124
+ function claimBoot(deps) {
15125
+ const { bootMarker, timing } = deps;
15126
+ if (bootMarker.claim({ pid: process.pid, claimedAt: timing.wallNow() })) {
15127
+ return { claimed: true };
15128
+ }
15129
+ const held = bootMarker.read();
15130
+ if (!held)
15131
+ return { claimed: false };
15132
+ const age = timing.wallNow() - held.claimedAt;
15133
+ if (deps.isAlive(held.pid) && age >= 0 && age < BOOT_MARKER_TTL_MS) {
15134
+ return { claimed: false, bootPid: held.pid };
15135
+ }
15136
+ bootMarker.clear();
15137
+ logDebug("spawn", "stale boot marker removed", { bootPid: held.pid });
15138
+ return { claimed: false };
15139
+ }
14805
15140
  async function awaitDrained(deps, until) {
14806
15141
  const h = await deps.health(deps.baseUrl);
14807
15142
  if (h?.service !== "caret")
@@ -14868,6 +15203,51 @@ function readDaemonLock() {
14868
15203
  return lock;
14869
15204
  return null;
14870
15205
  }
15206
+ function readBootMarker() {
15207
+ const m = readJsonFileSync(daemonBootMarker());
15208
+ if (m && typeof m.pid === "number" && typeof m.claimedAt === "number")
15209
+ return m;
15210
+ return null;
15211
+ }
15212
+ function fileBootMarker() {
15213
+ const write = (marker, place) => {
15214
+ const path = daemonBootMarker();
15215
+ const tmp = `${path}.${process.pid}.tmp`;
15216
+ writeFileSync3(tmp, JSON.stringify(marker), { mode: 384 });
15217
+ try {
15218
+ place(tmp, path);
15219
+ } finally {
15220
+ rmQuiet(tmp);
15221
+ }
15222
+ };
15223
+ return {
15224
+ claim: (marker) => {
15225
+ try {
15226
+ ensureStateDir();
15227
+ write(marker, linkSync);
15228
+ return true;
15229
+ } catch (e) {
15230
+ return e.code !== "EEXIST";
15231
+ }
15232
+ },
15233
+ assign: (pid) => {
15234
+ try {
15235
+ write({ pid, claimedAt: readBootMarker()?.claimedAt ?? 0 }, renameSync2);
15236
+ } catch {}
15237
+ },
15238
+ read: readBootMarker,
15239
+ clear: () => rmQuiet(daemonBootMarker())
15240
+ };
15241
+ }
15242
+ function removeOwnBootMarker() {
15243
+ if (readBootMarker()?.pid === process.pid)
15244
+ rmQuiet(daemonBootMarker());
15245
+ }
15246
+ function rmQuiet(path) {
15247
+ try {
15248
+ unlinkSync2(path);
15249
+ } catch {}
15250
+ }
14871
15251
  function isPidAlive(pid) {
14872
15252
  try {
14873
15253
  process.kill(pid, 0);
@@ -14877,9 +15257,7 @@ function isPidAlive(pid) {
14877
15257
  }
14878
15258
  }
14879
15259
  function removeDaemonLock() {
14880
- try {
14881
- unlinkSync2(daemonLock());
14882
- } catch {}
15260
+ rmQuiet(daemonLock());
14883
15261
  }
14884
15262
  function removeOwnDaemonLock() {
14885
15263
  if (readDaemonLock()?.pid === process.pid)
@@ -14972,16 +15350,18 @@ async function prodEnsureDeps(s, service, reserveMs) {
14972
15350
  retire: (baseUrl, lock) => retireDaemon(baseUrl, lock, world),
14973
15351
  removeLock: removeDaemonLock,
14974
15352
  spawn: () => spawnDaemon(s),
15353
+ bootMarker: fileBootMarker(),
14975
15354
  timing: {
14976
15355
  backoff,
14977
15356
  maxAttempts: PROD_MAX_ATTEMPTS,
14978
15357
  now: () => performance.now(),
15358
+ wallNow: Date.now,
14979
15359
  windowMs: SUPERVISOR_WINDOW_MS,
14980
15360
  reserveMs
14981
15361
  }
14982
15362
  };
14983
15363
  }
14984
- var FOREIGN_WORLD_ERROR = "port serves a different caret world (state dir mismatch) \u2014 set CARET_PORT to a free port", DAEMON_CWD = "/", PROD_MAX_ATTEMPTS = 12, SUPERVISOR_WINDOW_MS;
15364
+ var BOOT_MARKER_TTL_MS = 30000, FOREIGN_WORLD_ERROR = "port serves a different caret world (state dir mismatch) \u2014 set CARET_PORT to a free port", DAEMON_CWD = "/", PROD_MAX_ATTEMPTS = 12, SUPERVISOR_WINDOW_MS;
14985
15365
  var init_lifecycle = __esm(() => {
14986
15366
  init_paths3();
14987
15367
  init_settings();
@@ -16342,7 +16722,7 @@ __export(exports_dist, {
16342
16722
  box: () => box,
16343
16723
  cancel: () => cancel,
16344
16724
  confirm: () => confirm,
16345
- date: () => date2,
16725
+ date: () => date3,
16346
16726
  formatInstructionFooter: () => formatInstructionFooter,
16347
16727
  group: () => group,
16348
16728
  groupMultiselect: () => groupMultiselect,
@@ -16375,8 +16755,8 @@ __export(exports_dist, {
16375
16755
  });
16376
16756
  import { styleText as styleText2, stripVTControlCharacters } from "util";
16377
16757
  import process$1 from "process";
16378
- import { existsSync as existsSync13, lstatSync, readdirSync as readdirSync6 } from "fs";
16379
- import { dirname as dirname9, join as join18 } from "path";
16758
+ import { existsSync as existsSync10, lstatSync, readdirSync as readdirSync4 } from "fs";
16759
+ import { dirname as dirname8, join as join15 } from "path";
16380
16760
  function isUnicodeSupported() {
16381
16761
  if (process$1.platform !== "win32") {
16382
16762
  return process$1.env.TERM !== "linux";
@@ -16725,7 +17105,7 @@ ${g}
16725
17105
  }
16726
17106
  }
16727
17107
  }).prompt();
16728
- }, date2 = (e) => {
17108
+ }, date3 = (e) => {
16729
17109
  const r = e.validate;
16730
17110
  return new U2({
16731
17111
  ...e,
@@ -17087,10 +17467,10 @@ ${n}
17087
17467
  return [];
17088
17468
  try {
17089
17469
  let i;
17090
- existsSync13(t) ? lstatSync(t).isDirectory() && (!e.directory || t.endsWith("/")) ? i = t : i = dirname9(t) : i = dirname9(t);
17470
+ existsSync10(t) ? lstatSync(t).isDirectory() && (!e.directory || t.endsWith("/")) ? i = t : i = dirname8(t) : i = dirname8(t);
17091
17471
  const c = t.length > 1 && t.endsWith("/") ? t.slice(0, -1) : t;
17092
- return readdirSync6(i).map((r) => {
17093
- const n = join18(i, r), m = lstatSync(n);
17472
+ return readdirSync4(i).map((r) => {
17473
+ const n = join15(i, r), m = lstatSync(n);
17094
17474
  return {
17095
17475
  name: r,
17096
17476
  path: n,
@@ -18675,7 +19055,7 @@ var handleResult = (ctx, result) => {
18675
19055
  }
18676
19056
  };
18677
19057
  }
18678
- }, cuidRegex, cuid2Regex, ulidRegex, uuidRegex, nanoidRegex, jwtRegex, durationRegex, emailRegex, _emojiRegex = `^[\\p{Extended_Pictographic}\\p{Emoji_Component}]+$`, emojiRegex, ipv4Regex, ipv4CidrRegex, ipv6Regex, ipv6CidrRegex, base64Regex, base64urlRegex, dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`, dateRegex, ZodString2, ZodNumber2, ZodBigInt, ZodBoolean2, ZodDate, ZodSymbol, ZodUndefined, ZodNull2, ZodAny, ZodUnknown2, ZodNever2, ZodVoid, ZodArray2, ZodObject2, ZodUnion2, getDiscriminator = (type) => {
19058
+ }, cuidRegex, cuid2Regex, ulidRegex, uuidRegex, nanoidRegex, jwtRegex, durationRegex, emailRegex, _emojiRegex = `^[\\p{Extended_Pictographic}\\p{Emoji_Component}]+$`, emojiRegex, ipv4Regex, ipv4CidrRegex, ipv6Regex, ipv6CidrRegex, base64Regex, base64urlRegex, dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`, dateRegex, ZodString2, ZodNumber2, ZodBigInt2, ZodBoolean2, ZodDate2, ZodSymbol, ZodUndefined, ZodNull2, ZodAny, ZodUnknown2, ZodNever2, ZodVoid, ZodArray2, ZodObject2, ZodUnion2, getDiscriminator = (type) => {
18679
19059
  if (type instanceof ZodLazy) {
18680
19060
  return getDiscriminator(type.schema);
18681
19061
  } else if (type instanceof ZodEffects) {
@@ -19502,7 +19882,7 @@ var init_types = __esm(() => {
19502
19882
  ...processCreateParams(params)
19503
19883
  });
19504
19884
  };
19505
- ZodBigInt = class ZodBigInt extends ZodType2 {
19885
+ ZodBigInt2 = class ZodBigInt2 extends ZodType2 {
19506
19886
  constructor() {
19507
19887
  super(...arguments);
19508
19888
  this.min = this.gte;
@@ -19587,7 +19967,7 @@ var init_types = __esm(() => {
19587
19967
  return this.setLimit("max", value, false, errorUtil.toString(message));
19588
19968
  }
19589
19969
  setLimit(kind, value, inclusive, message) {
19590
- return new ZodBigInt({
19970
+ return new ZodBigInt2({
19591
19971
  ...this._def,
19592
19972
  checks: [
19593
19973
  ...this._def.checks,
@@ -19601,7 +19981,7 @@ var init_types = __esm(() => {
19601
19981
  });
19602
19982
  }
19603
19983
  _addCheck(check) {
19604
- return new ZodBigInt({
19984
+ return new ZodBigInt2({
19605
19985
  ...this._def,
19606
19986
  checks: [...this._def.checks, check]
19607
19987
  });
@@ -19666,8 +20046,8 @@ var init_types = __esm(() => {
19666
20046
  return max;
19667
20047
  }
19668
20048
  };
19669
- ZodBigInt.create = (params) => {
19670
- return new ZodBigInt({
20049
+ ZodBigInt2.create = (params) => {
20050
+ return new ZodBigInt2({
19671
20051
  checks: [],
19672
20052
  typeName: ZodFirstPartyTypeKind2.ZodBigInt,
19673
20053
  coerce: params?.coerce ?? false,
@@ -19699,7 +20079,7 @@ var init_types = __esm(() => {
19699
20079
  ...processCreateParams(params)
19700
20080
  });
19701
20081
  };
19702
- ZodDate = class ZodDate extends ZodType2 {
20082
+ ZodDate2 = class ZodDate2 extends ZodType2 {
19703
20083
  _parse(input) {
19704
20084
  if (this._def.coerce) {
19705
20085
  input.data = new Date(input.data);
@@ -19760,7 +20140,7 @@ var init_types = __esm(() => {
19760
20140
  };
19761
20141
  }
19762
20142
  _addCheck(check) {
19763
- return new ZodDate({
20143
+ return new ZodDate2({
19764
20144
  ...this._def,
19765
20145
  checks: [...this._def.checks, check]
19766
20146
  });
@@ -19800,8 +20180,8 @@ var init_types = __esm(() => {
19800
20180
  return max != null ? new Date(max) : null;
19801
20181
  }
19802
20182
  };
19803
- ZodDate.create = (params) => {
19804
- return new ZodDate({
20183
+ ZodDate2.create = (params) => {
20184
+ return new ZodDate2({
19805
20185
  checks: [],
19806
20186
  coerce: params?.coerce || false,
19807
20187
  typeName: ZodFirstPartyTypeKind2.ZodDate,
@@ -21467,9 +21847,9 @@ var init_types = __esm(() => {
21467
21847
  stringType = ZodString2.create;
21468
21848
  numberType = ZodNumber2.create;
21469
21849
  nanType = ZodNaN.create;
21470
- bigIntType = ZodBigInt.create;
21850
+ bigIntType = ZodBigInt2.create;
21471
21851
  booleanType = ZodBoolean2.create;
21472
- dateType = ZodDate.create;
21852
+ dateType = ZodDate2.create;
21473
21853
  symbolType = ZodSymbol.create;
21474
21854
  undefinedType = ZodUndefined.create;
21475
21855
  nullType = ZodNull2.create;
@@ -33494,6 +33874,89 @@ function optedOut(text) {
33494
33874
  // src/commands/daemon.ts
33495
33875
  init_settings();
33496
33876
 
33877
+ // src/daemon/update-changes.ts
33878
+ var COMMIT_CAP = 50;
33879
+ var RELEASE_TAG = /^v?\d+\.\d+\.\d+$/;
33880
+ var FULL_SHA = /^[0-9a-f]{40}$/;
33881
+ var COMPARABLE_COMMIT = /^[0-9a-f]{7,40}$/;
33882
+ function selectReleases(releases, running, target) {
33883
+ return releases.filter((r) => !r.draft && !r.prerelease && RELEASE_TAG.test(r.tag_name)).map((r) => ({ version: r.tag_name.replace(/^v/, ""), body: r.body ?? "" })).filter((r) => isNewer(r.version, running) && !isNewer(r.version, target)).sort((a, b) => isNewer(a.version, b.version) ? -1 : isNewer(b.version, a.version) ? 1 : 0);
33884
+ }
33885
+ function selectCommits(newestFirst, total, cap) {
33886
+ const commits = newestFirst.filter((c) => FULL_SHA.test(c.sha)).slice(0, cap).map((c) => {
33887
+ const subject = c.commit.message.split(`
33888
+ `, 1)[0] ?? "";
33889
+ return { sha: c.sha, subject, pr: prNumber(subject) };
33890
+ });
33891
+ return { kind: "commits", commits, more: Math.max(0, total - commits.length) };
33892
+ }
33893
+ function prNumber(subject) {
33894
+ const match = /\(#(\d+)\)$/.exec(subject);
33895
+ return match ? Number(match[1]) : null;
33896
+ }
33897
+ function createUpdateChanges(deps) {
33898
+ let slot = null;
33899
+ return () => {
33900
+ const status = deps.status();
33901
+ if (slot?.status === status)
33902
+ return slot.promise;
33903
+ const promise = changesFor(deps, status).catch(() => failed(deps.log)).then((changes) => {
33904
+ if (changes === null && slot?.promise === promise)
33905
+ slot = null;
33906
+ return changes;
33907
+ });
33908
+ slot = { status, promise };
33909
+ return promise;
33910
+ };
33911
+ }
33912
+ async function changesFor(deps, status) {
33913
+ if (deps.install === "bundle" && status.kind === "behind-release") {
33914
+ const releases = await deps.releases();
33915
+ if (!releases)
33916
+ return failed(deps.log);
33917
+ return { kind: "releases", releases: selectReleases(releases, deps.version, status.available) };
33918
+ }
33919
+ const behind = status.kind === "behind-release" || status.kind === "behind-commit";
33920
+ if (deps.install !== "binary" || !behind || !COMPARABLE_COMMIT.test(deps.commit))
33921
+ return null;
33922
+ const comparison = await deps.compare(deps.commit);
33923
+ if (!comparison)
33924
+ return failed(deps.log);
33925
+ return selectCommits([...comparison.commits].reverse(), comparison.total_commits, COMMIT_CAP);
33926
+ }
33927
+ function failed(log) {
33928
+ log.warn("update", "update changes read failed");
33929
+ return null;
33930
+ }
33931
+
33932
+ // src/daemon/dev-update.ts
33933
+ var RELEASES_BACK = 3;
33934
+ var COMMITS_BACK = 60;
33935
+ function devUpdateIdentity(raw, built, git) {
33936
+ if (built.install !== "dev")
33937
+ return null;
33938
+ if (raw === "release") {
33939
+ const tags = git(["tag", "--list", "v*", "--sort=-v:refname"])?.split(`
33940
+ `) ?? [];
33941
+ const tag = tags.filter((t) => RELEASE_TAG.test(t))[RELEASES_BACK];
33942
+ return tag ? { install: "bundle", version: tag.replace(/^v/, ""), commit: built.commit } : null;
33943
+ }
33944
+ if (raw === "commit") {
33945
+ const commit = git(["rev-parse", "--verify", "--quiet", `origin/trunk~${COMMITS_BACK}`]);
33946
+ return commit ? { install: "binary", version: built.version, commit } : null;
33947
+ }
33948
+ return null;
33949
+ }
33950
+ function sourceGit(args) {
33951
+ try {
33952
+ const r = Bun.spawnSync(["git", "-C", import.meta.dir, ...args]);
33953
+ const out = r.exitCode === 0 ? r.stdout.toString().trim() : "";
33954
+ return out || null;
33955
+ } catch {
33956
+ return null;
33957
+ }
33958
+ }
33959
+
33497
33960
  // src/daemon/diagnostics.ts
33498
33961
  init_settings();
33499
33962
  init_core3();
@@ -33533,7 +33996,7 @@ function buildDiagnostics(deps) {
33533
33996
  init_lifecycle();
33534
33997
 
33535
33998
  // src/daemon/server.ts
33536
- import { renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "fs";
33999
+ import { renameSync as renameSync4, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6 } from "fs";
33537
34000
  import { dirname as dirname6 } from "path";
33538
34001
  init_constants();
33539
34002
  init_paths3();
@@ -33686,8 +34149,10 @@ var BehaviorSchema = _enum(["allow", "deny"]);
33686
34149
  var ResolveBodySchema = object({
33687
34150
  behavior: BehaviorSchema.catch("allow"),
33688
34151
  feedback: string2().optional(),
33689
- acceptMode: string2().optional().catch(undefined)
34152
+ acceptMode: string2().optional().catch(undefined),
34153
+ version: number2().int().positive().optional().catch(undefined)
33690
34154
  }).catch({ behavior: "allow" });
34155
+ var VersionQuerySchema = exports_coerce.number().int().positive().optional().catch(undefined);
33691
34156
  var ConfigPatchSchema = object({ updates: object({ check: boolean2() }).strict().optional() }).strict();
33692
34157
  var LineAnnotationSchema = object({
33693
34158
  id: string2(),
@@ -33835,7 +34300,8 @@ var EXT_LANGUAGE = {
33835
34300
  ".gql": "graphql",
33836
34301
  ".php": "php",
33837
34302
  ".swift": "swift",
33838
- ".dart": "dart"
34303
+ ".dart": "dart",
34304
+ ".lua": "lua"
33839
34305
  };
33840
34306
  function languageForPath(path) {
33841
34307
  return EXT_LANGUAGE[extname(path).toLowerCase()] ?? "text";
@@ -35529,23 +35995,37 @@ var un = x.lex;
35529
35995
  init_log();
35530
35996
 
35531
35997
  // src/plan/canonical-file.ts
35532
- import { appendFileSync, existsSync as existsSync7, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
35998
+ import { appendFileSync, existsSync as existsSync7, readFileSync as readFileSync10, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
35999
+ function isPlanFile(path) {
36000
+ return path.endsWith(".md") && existsSync7(path) && statSync3(path).isFile();
36001
+ }
36002
+ function readPlanFile(path) {
36003
+ try {
36004
+ return isPlanFile(path) ? readFileSync10(path, "utf8") : undefined;
36005
+ } catch {
36006
+ return;
36007
+ }
36008
+ }
35533
36009
  function guardedPlanFileWrite(planFilePath, log, failMsg, write) {
35534
36010
  try {
35535
- if (!planFilePath.endsWith(".md"))
35536
- return;
35537
- if (!existsSync7(planFilePath) || !statSync3(planFilePath).isFile())
36011
+ if (!isPlanFile(planFilePath))
35538
36012
  return;
35539
- write(planFilePath);
36013
+ return write(planFilePath);
35540
36014
  } catch (err) {
35541
36015
  const code = err?.code;
35542
- log.warn("review", failMsg, code ? { code } : {});
36016
+ log.warn("review", failMsg, code ? { fsCode: code } : {});
36017
+ return;
35543
36018
  }
35544
36019
  }
35545
- function writeCanonicalPlanFile(planFilePath, canonical, log) {
35546
- if (!planFilePath)
35547
- return;
35548
- guardedPlanFileWrite(planFilePath, log, "plan file canonicalize failed", (p) => writeFileSync3(p, canonical));
36020
+ function writeCanonicalPlanFile(input, canonical, log) {
36021
+ return guardedPlanFileWrite(input.planFilePath, log, "plan file canonicalize failed", (p) => {
36022
+ if (readFileSync10(p, "utf8") !== (input.plan ?? "")) {
36023
+ log.info("review", "plan file changed; rewrite skipped");
36024
+ return "changed";
36025
+ }
36026
+ writeFileSync4(p, canonical);
36027
+ return "written";
36028
+ }) ?? "skipped";
35549
36029
  }
35550
36030
  function appendReviewerNotesToPlanFile(planFilePath, notes, log) {
35551
36031
  const section = reviewerNotesSection(notes);
@@ -35560,7 +36040,7 @@ init_log();
35560
36040
  // src/plan/rumdl.ts
35561
36041
  init_paths3();
35562
36042
  import { createHash as createHash2 } from "crypto";
35563
- import { chmodSync as chmodSync3, existsSync as existsSync8, mkdirSync as mkdirSync2, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
36043
+ import { chmodSync as chmodSync3, existsSync as existsSync8, mkdirSync as mkdirSync2, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
35564
36044
  import { mkdtemp, rm as rm3 } from "fs/promises";
35565
36045
  import { tmpdir } from "os";
35566
36046
  import { dirname as dirname5, join as join13 } from "path";
@@ -35620,7 +36100,7 @@ async function downloadRumdl(binPath, asset, fetchImpl = fetch) {
35620
36100
  const tmp = `${binPath}.${process.pid}.tmp`;
35621
36101
  await Bun.write(tmp, Bun.file(extracted));
35622
36102
  chmodSync3(tmp, 493);
35623
- renameSync2(tmp, binPath);
36103
+ renameSync3(tmp, binPath);
35624
36104
  return binPath;
35625
36105
  } finally {
35626
36106
  await rm3(work, { recursive: true, force: true });
@@ -35645,7 +36125,7 @@ var acquisition = { cooldownUntil: 0 };
35645
36125
  async function ensureRumdl(install = installPinnedRumdl, now = Date.now, state = acquisition) {
35646
36126
  ensureStateDir(rumdlDir());
35647
36127
  const config = rumdlConfig();
35648
- writeFileSync4(config, RUMDL_CONFIG);
36128
+ writeFileSync5(config, RUMDL_CONFIG);
35649
36129
  const override = process.env.CARET_RUMDL_BIN?.trim();
35650
36130
  if (override && await installedVersion(override) === RUMDL_VERSION) {
35651
36131
  return { bin: override, config, installed: false };
@@ -35727,21 +36207,25 @@ function deriveTitle(plan) {
35727
36207
  }
35728
36208
  async function routeIncomingPlan(input, store, log = noopLogger) {
35729
36209
  const sessionId = input.sessionId ?? `anon-${Date.now()}`;
35730
- const plan = await formatPlanMarkdown(input.plan ?? "", log);
35731
- writeCanonicalPlanFile(input.planFilePath, plan, log);
36210
+ const sessionLog = log.child({ sessionId });
36211
+ const plan = await formatPlanMarkdown(input.plan ?? "", sessionLog);
36212
+ const { planFilePath } = input;
36213
+ const planFile = planFilePath ? {
36214
+ planFileCurrent: writeCanonicalPlanFile({ ...input, planFilePath }, plan, sessionLog) !== "changed"
36215
+ } : {};
35732
36216
  const now = Date.now();
36217
+ const [latest, ...older] = store.bySession(sessionId);
35733
36218
  const expired = [];
35734
- for (const stale of store.bySession(sessionId).filter((r) => r.status === "pending")) {
35735
- await store.expire(stale.id);
35736
- expired.push(stale.id);
35737
- log.info("review", `review superseded: ${shortId(stale.id)}`, {
35738
- reviewId: stale.id,
36219
+ for (const orphan of older.filter((r) => r.status === "pending")) {
36220
+ await store.expire(orphan.id);
36221
+ expired.push(orphan.id);
36222
+ log.info("review", `review superseded: ${shortId(orphan.id)}`, {
36223
+ reviewId: orphan.id,
35739
36224
  sessionId,
35740
36225
  action: "supersede"
35741
36226
  });
35742
36227
  }
35743
- const latest = store.bySession(sessionId)[0];
35744
- if (latest && latest.status === "rejected") {
36228
+ if (latest && (latest.status === "rejected" || latest.status === "pending")) {
35745
36229
  const version = latest.versions.length + 1;
35746
36230
  await store.update(latest.id, (r) => {
35747
36231
  r.versions.push({ version, plan, annotations: [], createdAt: now });
@@ -35760,7 +36244,8 @@ async function routeIncomingPlan(input, store, log = noopLogger) {
35760
36244
  id: latest.id,
35761
36245
  action: "append",
35762
36246
  version,
35763
- expired
36247
+ expired,
36248
+ ...planFile
35764
36249
  };
35765
36250
  }
35766
36251
  const id = newReviewId();
@@ -35782,7 +36267,7 @@ async function routeIncomingPlan(input, store, log = noopLogger) {
35782
36267
  action: "new",
35783
36268
  version: 1
35784
36269
  });
35785
- return { id, action: "new", version: 1, expired };
36270
+ return { id, action: "new", version: 1, expired, ...planFile };
35786
36271
  }
35787
36272
 
35788
36273
  // src/service/manager.ts
@@ -35824,7 +36309,16 @@ var MAX_BODY_BYTES = 64 * 1024;
35824
36309
  var MAX_EVENTS = 100;
35825
36310
  var MAX_MSG_LEN = 256;
35826
36311
  var STEP_RE = /^[a-z][a-z0-9-]{0,31}$/;
35827
- var RESERVED_KEYS = new Set(["level", "time", "msg", "step", "pid", "err", "caller"]);
36312
+ var RESERVED_KEYS = new Set([
36313
+ "level",
36314
+ "time",
36315
+ "msg",
36316
+ "step",
36317
+ "pid",
36318
+ "err",
36319
+ "caller",
36320
+ "code"
36321
+ ]);
35828
36322
  var CONTROL_RE = /[\u0000-\u0008\u000A-\u001F\u007F-\u009F]/g;
35829
36323
  function sanitizeString(s) {
35830
36324
  return s.replace(CONTROL_RE, "");
@@ -35898,6 +36392,8 @@ function resolveOptions(opts) {
35898
36392
  readSkillDescription: opts.readSkillDescription,
35899
36393
  diagnostics: opts.diagnostics,
35900
36394
  updateReport: opts.updateReport,
36395
+ updateChanges: opts.updateChanges,
36396
+ restartHint: opts.restartHint,
35901
36397
  onUpdatesEnabled: opts.onUpdatesEnabled,
35902
36398
  onDecisionAwaited: opts.onDecisionAwaited,
35903
36399
  markPaneRead: opts.markPaneRead ?? ((pane) => markPaneRead(pane, { log: opts.log })),
@@ -35916,6 +36412,19 @@ function ifNoneMatchHit(header, etag) {
35916
36412
  return false;
35917
36413
  return header.split(",").some((t) => t.trim().replace(/^W\//, "") === etag);
35918
36414
  }
36415
+ function isStaleVersion(review, version) {
36416
+ return review !== undefined && version !== undefined && currentVersion(review).version !== version;
36417
+ }
36418
+ function expiredDeny() {
36419
+ return {
36420
+ behavior: "deny",
36421
+ feedback: "caret: this review expired before a decision; denying so no unreviewed plan ships.",
36422
+ decidedAt: Date.now()
36423
+ };
36424
+ }
36425
+ function versionQuery(req) {
36426
+ return VersionQuerySchema.parse(new URL(req.url).searchParams.get("version") ?? undefined);
36427
+ }
35919
36428
  function createServer(opts) {
35920
36429
  const cfg = resolveOptions(opts);
35921
36430
  const { store, idle, heartbeat, resident, assets, onShutdown, routePlan, configPath, log } = cfg;
@@ -35955,6 +36464,9 @@ function createServer(opts) {
35955
36464
  function notFound() {
35956
36465
  return new Response("not found", { status: 404 });
35957
36466
  }
36467
+ function versionConflict() {
36468
+ return new Response("superseded by a newer version", { status: 409 });
36469
+ }
35958
36470
  function tooLarge() {
35959
36471
  return new Response("too large", { status: 413 });
35960
36472
  }
@@ -35970,7 +36482,8 @@ function createServer(opts) {
35970
36482
  supervised: isSupervised(),
35971
36483
  ...process.env.CARET_FRESH === "1" ? { fresh: true } : {},
35972
36484
  ...approveVariants ? { approveVariants: [...approveVariants] } : {},
35973
- ...source ? { source } : {}
36485
+ ...source ? { source } : {},
36486
+ ...cfg.restartHint ? { restartHint: cfg.restartHint } : {}
35974
36487
  };
35975
36488
  return Response.json(body);
35976
36489
  }
@@ -35984,6 +36497,12 @@ function createServer(opts) {
35984
36497
  return notFound();
35985
36498
  return Response.json(await cfg.updateReport());
35986
36499
  }
36500
+ async function handleUpdateChanges() {
36501
+ if (!cfg.updateChanges)
36502
+ return notFound();
36503
+ const changes = await cfg.updateChanges();
36504
+ return changes ? Response.json(changes) : new Response("upstream unavailable", { status: 502 });
36505
+ }
35987
36506
  function handleRetire() {
35988
36507
  log.info("retire", "retire requested");
35989
36508
  liveness.drain();
@@ -36023,19 +36542,21 @@ function createServer(opts) {
36023
36542
  const cacheControl = path.startsWith("/assets/") ? ASSET_CACHE_CONTROL : INDEX_CACHE_CONTROL;
36024
36543
  return assetResponse(req, file, cacheControl);
36025
36544
  }
36026
- async function handleCreateReview(req) {
36545
+ async function handleCreateReview(req, routed) {
36027
36546
  if (liveness.isDraining()) {
36028
36547
  log.warn("drain", "review refused: draining");
36029
36548
  return new Response("draining", { status: 503 });
36030
36549
  }
36031
36550
  const body = await parseBody(req, PlanInputSchema);
36032
- const routed = await routePlan(body, store, log);
36033
- for (const staleId of routed.expired)
36551
+ if (body.sessionId)
36552
+ routed.log = log.child({ sessionId: body.sessionId });
36553
+ const routing = await routePlan(body, store, routed.log);
36554
+ for (const staleId of routing.expired)
36034
36555
  clearDecision(staleId);
36035
- if (routed.action === "append")
36036
- clearDecision(routed.id);
36556
+ if (routing.action === "append")
36557
+ clearDecision(routing.id);
36037
36558
  const hasLiveClient = isClientLive(lastReviewsPollAt, Date.now(), LIVE_CLIENT_WINDOW_MS);
36038
- return Response.json({ ...routed, hasLiveClient });
36559
+ return Response.json({ ...routing, hasLiveClient });
36039
36560
  }
36040
36561
  async function handleLogs(req) {
36041
36562
  const reject = (status) => {
@@ -36057,8 +36578,12 @@ function createServer(opts) {
36057
36578
  const result = parseUiLogBatch(parsed);
36058
36579
  if ("status" in result)
36059
36580
  return reject(result.status);
36060
- for (const ev of result.events)
36061
- log[ev.level](ev.step, ev.msg, ev.extra);
36581
+ for (const ev of result.events) {
36582
+ if (ev.level === "error")
36583
+ log.error(ev.step, "ui-error", ev.msg, ev.extra);
36584
+ else
36585
+ log[ev.level](ev.step, ev.msg, ev.extra);
36586
+ }
36062
36587
  return new Response(null, { status: 204 });
36063
36588
  }
36064
36589
  function handleListReviews() {
@@ -36095,11 +36620,12 @@ function createServer(opts) {
36095
36620
  cfg.onUpdatesEnabled?.();
36096
36621
  return Response.json({ ok: true });
36097
36622
  }
36098
- function handleGetReview(id) {
36623
+ function handleGetReview({ id }) {
36099
36624
  const r = store.get(id);
36100
36625
  return r ? Response.json(toClientReview(r)) : notFound();
36101
36626
  }
36102
- async function handleFileRefs(req, id) {
36627
+ async function handleFileRefs(req, route) {
36628
+ const { id } = route;
36103
36629
  const r = store.get(id);
36104
36630
  if (!r)
36105
36631
  return notFound();
@@ -36121,14 +36647,13 @@ function createServer(opts) {
36121
36647
  })));
36122
36648
  }
36123
36649
  const resolved = Object.fromEntries(entries.filter((e) => e !== null));
36124
- log.debug("request", `file-refs resolved: ${Object.keys(resolved).length}/${unique.length}`, {
36125
- reviewId: id,
36650
+ route.log.debug("request", `file-refs resolved: ${Object.keys(resolved).length}/${unique.length}`, {
36126
36651
  requested: paths.length,
36127
36652
  directories: Object.values(resolved).filter((k) => k === "directory").length
36128
36653
  });
36129
36654
  return Response.json({ resolved });
36130
36655
  }
36131
- async function handleFileExcerpt(req, id) {
36656
+ async function handleFileExcerpt(req, { id }) {
36132
36657
  const r = store.get(id);
36133
36658
  if (!r)
36134
36659
  return notFound();
@@ -36147,7 +36672,8 @@ function createServer(opts) {
36147
36672
  return Response.json(excerpt);
36148
36673
  return await isFileTooLargeToPreview(r.cwd, path) ? tooLarge() : notFound();
36149
36674
  }
36150
- async function handleDirListing(req, id) {
36675
+ async function handleDirListing(req, route) {
36676
+ const { id } = route;
36151
36677
  const r = store.get(id);
36152
36678
  if (!r)
36153
36679
  return notFound();
@@ -36155,24 +36681,25 @@ function createServer(opts) {
36155
36681
  const listing = await listDirectory(r.cwd, params.get("root") ?? "", params.get("path") ?? "");
36156
36682
  if (listing === null)
36157
36683
  return notFound();
36158
- log.debug("request", "dir listed", {
36159
- reviewId: id,
36684
+ route.log.debug("request", "dir listed", {
36160
36685
  total: listing.total,
36161
36686
  returned: listing.entries.length
36162
36687
  });
36163
36688
  return Response.json(listing);
36164
36689
  }
36165
- async function handleSkills(id) {
36690
+ async function handleSkills(route) {
36691
+ const { id } = route;
36166
36692
  if (!cfg.listSkills)
36167
36693
  return notFound();
36168
36694
  const r = store.get(id);
36169
36695
  if (!r)
36170
36696
  return notFound();
36171
36697
  const skills = await cfg.listSkills(r.cwd);
36172
- log.debug("request", "skills listed", { reviewId: id, count: skills.length });
36698
+ route.log.debug("request", "skills listed", { count: skills.length });
36173
36699
  return Response.json(skills);
36174
36700
  }
36175
- async function handleSkillDescription(req, id) {
36701
+ async function handleSkillDescription(req, route) {
36702
+ const { id } = route;
36176
36703
  if (!cfg.readSkillDescription)
36177
36704
  return notFound();
36178
36705
  const r = store.get(id);
@@ -36183,10 +36710,11 @@ function createServer(opts) {
36183
36710
  name: params.get("name") ?? "",
36184
36711
  origin: params.get("origin") ?? ""
36185
36712
  });
36186
- log.debug("request", "skill description read", { reviewId: id, found: description !== null });
36713
+ route.log.debug("request", "skill description read", { found: description !== null });
36187
36714
  return Response.json({ description });
36188
36715
  }
36189
- async function handleFileSearch(req, id) {
36716
+ async function handleFileSearch(req, route) {
36717
+ const { id } = route;
36190
36718
  const r = store.get(id);
36191
36719
  if (!r)
36192
36720
  return notFound();
@@ -36194,26 +36722,30 @@ function createServer(opts) {
36194
36722
  const result = await searchFiles(r.cwd, query);
36195
36723
  if (result === null)
36196
36724
  return notFound();
36197
- log.debug("request", "file search answered", {
36198
- reviewId: id,
36725
+ route.log.debug("request", "file search answered", {
36199
36726
  returned: result.paths.length,
36200
36727
  stoppedAt: result.stoppedAt
36201
36728
  });
36202
36729
  return Response.json(result);
36203
36730
  }
36204
- async function handleDecision(id) {
36731
+ async function handleDecision(req, route) {
36732
+ const { id } = route;
36205
36733
  const inMem = store.get(id);
36734
+ const disk = inMem ? undefined : await store.persisted(id);
36735
+ if (isStaleVersion(inMem ?? disk, versionQuery(req)))
36736
+ return versionConflict();
36206
36737
  if (inMem?.decision) {
36207
36738
  clearDecision(id);
36208
36739
  return Response.json(inMem.decision);
36209
36740
  }
36210
- if (!inMem) {
36211
- const disk = await store.persisted(id);
36212
- if (disk?.decision) {
36213
- log.debug("decision", `decision served from disk: ${shortId(id)}`, { reviewId: id });
36214
- clearDecision(id);
36215
- return Response.json(disk.decision);
36216
- }
36741
+ if (disk?.decision) {
36742
+ route.log.debug("decision", `decision served from disk: ${shortId(id)}`);
36743
+ clearDecision(id);
36744
+ return Response.json(disk.decision);
36745
+ }
36746
+ if (disk?.status === "expired") {
36747
+ route.log.debug("decision", `expired review denied: ${shortId(id)}`);
36748
+ return Response.json(expiredDeny());
36217
36749
  }
36218
36750
  const decision = await raceDecision(id, heartbeat);
36219
36751
  if (!decision)
@@ -36221,31 +36753,38 @@ function createServer(opts) {
36221
36753
  clearDecision(id);
36222
36754
  return Response.json(decision);
36223
36755
  }
36224
- async function handleDraft(req, id) {
36756
+ async function handleDraft(req, route) {
36757
+ const { id } = route;
36225
36758
  const raw = await req.json().catch(() => ({}));
36226
36759
  const invalid = malformedLineAnchor(raw);
36227
36760
  if (invalid)
36228
36761
  return Response.json({ error: invalid }, { status: 400 });
36229
36762
  const body = DraftBodySchema.parse(raw);
36230
36763
  const updated = await store.update(id, (r) => {
36231
- if (body.annotations != null) {
36232
- currentVersion(r).annotations = body.annotations;
36764
+ const cur = currentVersion(r);
36765
+ const isStale = body.version != null && body.version !== cur.version;
36766
+ if (body.annotations != null && !isStale) {
36767
+ cur.annotations = body.annotations;
36233
36768
  }
36234
36769
  if (body.generalCommentDraft != null) {
36235
36770
  r.generalCommentDraft = body.generalCommentDraft;
36236
36771
  }
36237
- const cur = currentVersion(r);
36238
- if (body.composerScratches != null && (body.version == null || body.version === cur.version)) {
36772
+ if (body.composerScratches != null && !isStale) {
36239
36773
  cur.composerScratches = body.composerScratches;
36240
36774
  }
36241
36775
  });
36242
36776
  if (updated)
36243
- log.debug("draft", `draft saved: ${shortId(id)}`, { reviewId: id });
36777
+ route.log.debug("draft", `draft saved: ${shortId(id)}`);
36244
36778
  return updated ? Response.json({ ok: true }) : notFound();
36245
36779
  }
36246
- async function handleResolve(req, id) {
36780
+ async function handleResolve(req, route) {
36781
+ const { id } = route;
36247
36782
  const body = await parseBody(req, ResolveBodySchema);
36248
36783
  const existing = store.get(id);
36784
+ if (isStaleVersion(existing, body.version)) {
36785
+ route.log.info("resolve", "resolve refused: stale version", { version: body.version });
36786
+ return versionConflict();
36787
+ }
36249
36788
  if (existing?.status !== "pending")
36250
36789
  return notFound();
36251
36790
  const decision = {
@@ -36270,26 +36809,28 @@ function createServer(opts) {
36270
36809
  if (existing.cmux)
36271
36810
  cfg.markPaneRead(existing.cmux);
36272
36811
  setTimeout(() => resolveDecision(id, decision), 0);
36273
- log.info("resolve", `review ${shortId(id)} resolved: ${decision.behavior}`, {
36274
- reviewId: id,
36812
+ route.log.info("resolve", `review ${shortId(id)} resolved: ${decision.behavior}`, {
36275
36813
  sessionId: existing.sessionId,
36276
36814
  acceptMode: decision.acceptMode
36277
36815
  });
36278
36816
  return Response.json({ ok: true });
36279
36817
  }
36280
- async function handleExpire(id) {
36281
- clearDecision(id);
36818
+ async function handleExpire(req, route) {
36819
+ const { id } = route;
36282
36820
  const existing = store.get(id);
36821
+ if (isStaleVersion(existing ?? await store.persisted(id), versionQuery(req))) {
36822
+ return versionConflict();
36823
+ }
36824
+ clearDecision(id);
36283
36825
  if (existing?.status !== "pending")
36284
36826
  return notFound();
36285
36827
  await store.expire(id);
36286
- log.info("review", `review expired: ${shortId(id)}`, {
36287
- reviewId: id,
36828
+ route.log.info("review", `review expired: ${shortId(id)}`, {
36288
36829
  sessionId: existing.sessionId
36289
36830
  });
36290
36831
  return Response.json({ ok: true });
36291
36832
  }
36292
- function handleSeen(id) {
36833
+ function handleSeen({ id }) {
36293
36834
  const existing = store.get(id);
36294
36835
  if (!existing)
36295
36836
  return notFound();
@@ -36297,7 +36838,8 @@ function createServer(opts) {
36297
36838
  cfg.markPaneRead(existing.cmux);
36298
36839
  return new Response(null, { status: 204 });
36299
36840
  }
36300
- async function dispatch(req, method, path) {
36841
+ async function dispatch(req, routed) {
36842
+ const { method, path, route } = routed;
36301
36843
  const readsUi = method === "GET" || method === "HEAD";
36302
36844
  if (method === "GET" && path === "/api/health")
36303
36845
  return handleHealth();
@@ -36305,12 +36847,14 @@ function createServer(opts) {
36305
36847
  return handleDiagnostics();
36306
36848
  if (method === "GET" && path === "/api/update")
36307
36849
  return handleUpdate();
36850
+ if (method === "GET" && path === "/api/update/changes")
36851
+ return handleUpdateChanges();
36308
36852
  if (method === "POST" && path === "/api/retire")
36309
36853
  return handleRetire();
36310
36854
  if (readsUi && (path === "/" || path === INDEX_PATH))
36311
36855
  return handleIndex(req);
36312
36856
  if (method === "POST" && path === "/api/reviews")
36313
- return handleCreateReview(req);
36857
+ return handleCreateReview(req, routed);
36314
36858
  if (method === "POST" && path === "/api/logs")
36315
36859
  return handleLogs(req);
36316
36860
  if (method === "POST" && path === "/api/ui/gone")
@@ -36319,34 +36863,33 @@ function createServer(opts) {
36319
36863
  return handleListReviews();
36320
36864
  if (method === "POST" && path === "/api/config")
36321
36865
  return handleSetConfig(req);
36322
- const route = matchIdRoute(path);
36323
36866
  if (route) {
36324
- const { id, sub } = route;
36867
+ const { sub } = route;
36325
36868
  if (method === "GET" && !sub)
36326
- return handleGetReview(id);
36869
+ return handleGetReview(route);
36327
36870
  if (method === "GET" && sub === "/file")
36328
- return handleFileExcerpt(req, id);
36871
+ return handleFileExcerpt(req, route);
36329
36872
  if (method === "GET" && sub === "/dir")
36330
- return handleDirListing(req, id);
36873
+ return handleDirListing(req, route);
36331
36874
  if (method === "GET" && sub === "/skills")
36332
- return handleSkills(id);
36875
+ return handleSkills(route);
36333
36876
  if (method === "GET" && sub === "/skill-description") {
36334
- return handleSkillDescription(req, id);
36877
+ return handleSkillDescription(req, route);
36335
36878
  }
36336
36879
  if (method === "POST" && sub === "/file-refs")
36337
- return handleFileRefs(req, id);
36880
+ return handleFileRefs(req, route);
36338
36881
  if (method === "POST" && sub === "/file-search")
36339
- return handleFileSearch(req, id);
36882
+ return handleFileSearch(req, route);
36340
36883
  if (method === "GET" && sub === "/decision")
36341
- return handleDecision(id);
36884
+ return handleDecision(req, route);
36342
36885
  if (method === "PUT" && sub === "/draft")
36343
- return handleDraft(req, id);
36886
+ return handleDraft(req, route);
36344
36887
  if (method === "POST" && sub === "/resolve")
36345
- return handleResolve(req, id);
36888
+ return handleResolve(req, route);
36346
36889
  if (method === "POST" && sub === "/expire")
36347
- return handleExpire(id);
36890
+ return handleExpire(req, route);
36348
36891
  if (method === "POST" && sub === "/seen")
36349
- return handleSeen(id);
36892
+ return handleSeen(route);
36350
36893
  }
36351
36894
  if (readsUi) {
36352
36895
  const asset = handleAsset(req, path);
@@ -36357,6 +36900,7 @@ function createServer(opts) {
36357
36900
  }
36358
36901
  async function handle(req, self) {
36359
36902
  const end = liveness.begin(req.method);
36903
+ let routed;
36360
36904
  try {
36361
36905
  const port = self.port ?? -1;
36362
36906
  if (isForeignHost(req, port)) {
@@ -36368,10 +36912,13 @@ function createServer(opts) {
36368
36912
  if (!isSafeMethod(method) && isCrossOrigin(req, port)) {
36369
36913
  return new Response("cross-origin request blocked", { status: 403 });
36370
36914
  }
36371
- return await dispatch(req, method, path);
36915
+ const idRoute = matchIdRoute(path);
36916
+ const route = idRoute && { ...idRoute, log: log.child({ reviewId: idRoute.id }) };
36917
+ routed = { method, path, route, log: route?.log ?? log };
36918
+ return await dispatch(req, routed);
36372
36919
  } catch (err) {
36373
36920
  try {
36374
- log.error("request", err);
36921
+ (routed?.log ?? log).error("request", "request-failed", err);
36375
36922
  } catch {}
36376
36923
  return new Response("internal error", { status: 500 });
36377
36924
  } finally {
@@ -36405,8 +36952,8 @@ function createServer(opts) {
36405
36952
  instanceId
36406
36953
  };
36407
36954
  const tmp = `${lockPath}.tmp.${process.pid}`;
36408
- writeFileSync5(tmp, JSON.stringify(lock));
36409
- renameSync3(tmp, lockPath);
36955
+ writeFileSync6(tmp, JSON.stringify(lock));
36956
+ renameSync4(tmp, lockPath);
36410
36957
  } catch {}
36411
36958
  }
36412
36959
  function removeLock() {
@@ -36432,7 +36979,7 @@ function createServer(opts) {
36432
36979
  // src/daemon/update-check.ts
36433
36980
  init_paths3();
36434
36981
  init_json_file();
36435
- import { writeFileSync as writeFileSync6 } from "fs";
36982
+ import { writeFileSync as writeFileSync7 } from "fs";
36436
36983
  import { dirname as dirname7 } from "path";
36437
36984
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
36438
36985
  var REFRESH_COMMAND = "bunx --no-cache @macintacos/caret@latest install --refresh";
@@ -36502,7 +37049,7 @@ function fileUpdateCache(path) {
36502
37049
  write: (entry) => {
36503
37050
  try {
36504
37051
  ensureStateDir(dirname7(path));
36505
- writeFileSync6(path, JSON.stringify(entry, null, 2), { mode: 384 });
37052
+ writeFileSync7(path, JSON.stringify(entry, null, 2), { mode: 384 });
36506
37053
  } catch {}
36507
37054
  }
36508
37055
  };
@@ -36564,44 +37111,7 @@ function startUpkeep({ tasks, log, everyMs, schedule }) {
36564
37111
  // src/commands/daemon.ts
36565
37112
  init_build_id();
36566
37113
  init_log();
36567
-
36568
- // src/lib/upstream.ts
36569
- init_package();
36570
- var NPM_LATEST_URL = `https://registry.npmjs.org/${package_default.name}/latest`;
36571
- var LATEST_RELEASE_URL = "https://api.github.com/repos/macintacos/caret/releases/latest";
36572
- var COMPARE_URL = "https://api.github.com/repos/macintacos/caret/compare";
36573
- var UPSTREAM_TIMEOUT_MS = 3000;
36574
- var GITHUB_HEADERS = { "user-agent": "caret" };
36575
- async function readJson(url, fetchImpl, headers) {
36576
- try {
36577
- const res = await fetchImpl(url, {
36578
- signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
36579
- ...headers ? { headers } : {}
36580
- });
36581
- return res.ok ? await res.json() : null;
36582
- } catch {
36583
- return null;
36584
- }
36585
- }
36586
- async function publishedCaretVersion(fetchImpl = fetch) {
36587
- const body = await readJson(NPM_LATEST_URL, fetchImpl);
36588
- const v = body?.version;
36589
- return typeof v === "string" && v.length > 0 ? v : null;
36590
- }
36591
- async function latestReleaseTag(fetchImpl = fetch) {
36592
- const body = await readJson(LATEST_RELEASE_URL, fetchImpl, GITHUB_HEADERS);
36593
- const tag = body?.tag_name;
36594
- if (typeof tag !== "string" || tag.length === 0)
36595
- return null;
36596
- const stripped = tag.replace(/^v/, "");
36597
- return stripped.length > 0 ? stripped : null;
36598
- }
36599
- async function commitsAheadOfTrunk(commit, fetchImpl = fetch) {
36600
- const url = `${COMPARE_URL}/${encodeURIComponent(commit)}...trunk`;
36601
- const body = await readJson(url, fetchImpl, GITHUB_HEADERS);
36602
- const ahead = body?.ahead_by;
36603
- return typeof ahead === "number" && Number.isFinite(ahead) ? ahead : null;
36604
- }
37114
+ init_upstream();
36605
37115
 
36606
37116
  // src/review/store.ts
36607
37117
  init_paths3();
@@ -36733,11 +37243,16 @@ async function runDaemon(opts) {
36733
37243
  });
36734
37244
  warnInvalidEnvVars((msg) => log.warn("env", msg));
36735
37245
  const adapter = selectAdapter();
36736
- const install = buildKind();
36737
- const version = VERSION;
36738
- const commit = currentCommit();
37246
+ const built = { install: buildKind(), version: VERSION, commit: currentCommit() };
37247
+ const devPose = process.env.CARET_DEV_UPDATE;
37248
+ const posed = devUpdateIdentity(devPose, built, sourceGit);
37249
+ if (devPose && built.install === "dev") {
37250
+ log.info("update", posed ? `CARET_DEV_UPDATE=${devPose}: posing as ${posed.install} ${posed.version} at ${posed.commit.slice(0, 7)}` : `CARET_DEV_UPDATE=${devPose}: no older build to pose as`);
37251
+ }
37252
+ const { install, version, commit } = posed ?? built;
36739
37253
  const updateCache = fileUpdateCache(updateCheckFile());
36740
37254
  let updateStatus = readCachedStatus(updateCache, version, commit);
37255
+ const servedReport = () => updateReportFor({ install, version, commit }, updateStatus, svc.current().updates.check);
36741
37256
  function refreshUpdate() {
36742
37257
  runUpdateCheck({
36743
37258
  kind: install,
@@ -36753,7 +37268,7 @@ async function runDaemon(opts) {
36753
37268
  }).then((status) => {
36754
37269
  if (status)
36755
37270
  updateStatus = status;
36756
- }, (err) => log.error("update", err));
37271
+ }, (err) => log.error("update", "update-check-failed", err));
36757
37272
  }
36758
37273
  const store = createStore(reviewsDir(), log);
36759
37274
  await store.rehydrate();
@@ -36778,10 +37293,14 @@ async function runDaemon(opts) {
36778
37293
  shutdown(0);
36779
37294
  });
36780
37295
  process.once("exit", removeOwnDaemonLock);
36781
- function exitTerminal(reason, err) {
37296
+ process.once("exit", removeOwnBootMarker);
37297
+ function exitTerminal(reason, code, err) {
36782
37298
  process.stderr.write(`caret: ${reason}; exiting.
36783
37299
  `);
36784
- log.error("fatal", err, { reason, exitStatus: SERVICE_TERMINAL_EXIT_STATUS });
37300
+ log.error("fatal", code, err, {
37301
+ reason,
37302
+ exitStatus: SERVICE_TERMINAL_EXIT_STATUS
37303
+ });
36785
37304
  process.exit(SERVICE_TERMINAL_EXIT_STATUS);
36786
37305
  }
36787
37306
  const buildId = await currentBuildId();
@@ -36798,7 +37317,7 @@ async function runDaemon(opts) {
36798
37317
  lockPath: daemonLock(),
36799
37318
  buildId,
36800
37319
  assetDigest,
36801
- commit,
37320
+ commit: built.commit,
36802
37321
  stateDir: stateDir(),
36803
37322
  instanceId: randomUUID().slice(0, 8),
36804
37323
  approveVariants: adapter.approveVariants,
@@ -36812,8 +37331,18 @@ async function runDaemon(opts) {
36812
37331
  settings: () => settings().current(),
36813
37332
  configPath: cfg
36814
37333
  })),
36815
- updateReport: () => updateReportFor({ install, version, commit }, updateStatus, svc.current().updates.check),
37334
+ updateReport: servedReport,
36816
37335
  onUpdatesEnabled: refreshUpdate,
37336
+ updateChanges: createUpdateChanges({
37337
+ install,
37338
+ version,
37339
+ commit,
37340
+ status: () => servedReport().status,
37341
+ releases: listReleases,
37342
+ compare: compareToTrunk,
37343
+ log
37344
+ }),
37345
+ restartHint: resident ? undefined : adapter.restartHint,
36817
37346
  log
36818
37347
  });
36819
37348
  } catch (e) {
@@ -36822,10 +37351,11 @@ async function runDaemon(opts) {
36822
37351
  `);
36823
37352
  process.exit(0);
36824
37353
  }
36825
- exitTerminal("cannot bind the daemon port", e);
37354
+ exitTerminal("cannot bind the daemon port", "daemon-bind-failed", e);
36826
37355
  }
37356
+ removeOwnBootMarker();
36827
37357
  const onFatal = (label) => (err) => {
36828
- log.error(label, err);
37358
+ log.error(label, "daemon-crashed", err);
36829
37359
  shutdown(1);
36830
37360
  };
36831
37361
  process.once("uncaughtException", onFatal("uncaughtException"));
@@ -36844,21 +37374,141 @@ async function runDaemon(opts) {
36844
37374
  return server.port;
36845
37375
  }
36846
37376
 
36847
- // src/commands/discovery.ts
37377
+ // src/commands/doctor.ts
36848
37378
  init_adapters();
37379
+ init_paths2();
37380
+ init_upgrade();
37381
+ import { existsSync as existsSync11 } from "fs";
37382
+ import { release } from "os";
37383
+
37384
+ // src/commands/install/ui.ts
37385
+ function isTerminal() {
37386
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
37387
+ }
37388
+ var silentUI = {
37389
+ intro: () => {},
37390
+ step: async (_label, work) => work(() => {}),
37391
+ info: () => {},
37392
+ warn: () => {},
37393
+ error: () => {},
37394
+ note: () => {},
37395
+ cancel: () => {},
37396
+ outro: () => {}
37397
+ };
37398
+ function plainUI(write) {
37399
+ const line = (s) => write(`caret: ${s}
37400
+ `);
37401
+ return {
37402
+ intro: (action) => line(action),
37403
+ async step(label, work, done) {
37404
+ try {
37405
+ const value = await work(() => {});
37406
+ line(done ? done(value) : label);
37407
+ return value;
37408
+ } catch (e) {
37409
+ line(`${label} \u2014 failed`);
37410
+ throw e;
37411
+ }
37412
+ },
37413
+ info: line,
37414
+ warn: line,
37415
+ error: line,
37416
+ note: (body, title) => line(`${title}
37417
+ ${body.split(`
37418
+ `).map((l) => ` ${l}`).join(`
37419
+ `)}`),
37420
+ cancel: line,
37421
+ outro: line
37422
+ };
37423
+ }
37424
+ async function createInstallUI(opts = {}) {
37425
+ const write = opts.write ?? ((s) => void process.stdout.write(s));
37426
+ const interactive = opts.interactive ?? (process.stdout.isTTY === true && !process.env.NO_COLOR && process.env.CI !== "true");
37427
+ if (!interactive)
37428
+ return plainUI(write);
37429
+ const clack = await Promise.resolve().then(() => (init_dist5(), {}));
37430
+ return {
37431
+ intro: (action) => intro(`caret \xB7 ${action}`),
37432
+ async step(label, work, done) {
37433
+ const spin = spinner();
37434
+ spin.start(label);
37435
+ try {
37436
+ const value = await work((m) => spin.message(m));
37437
+ spin.stop(done ? done(value) : label);
37438
+ return value;
37439
+ } catch (e) {
37440
+ spin.error(label);
37441
+ throw e;
37442
+ }
37443
+ },
37444
+ info: (m) => log.info(m),
37445
+ warn: (m) => log.warn(m),
37446
+ error: (m) => log.error(m),
37447
+ note: (body, title) => note(body, title),
37448
+ cancel: (m) => cancel(m),
37449
+ outro: (m) => outro(m)
37450
+ };
37451
+ }
37452
+
37453
+ // src/commands/doctor.ts
36849
37454
  init_paths3();
36850
37455
  init_settings();
36851
37456
  init_lifecycle();
36852
- import { existsSync as existsSync10 } from "fs";
36853
- import { release } from "os";
36854
37457
 
36855
- // src/discovery.ts
37458
+ // src/doctor/bundle.ts
37459
+ import { readdirSync as readdirSync5, readFileSync as readFileSync11 } from "fs";
37460
+ import { basename as basename3, join as join16 } from "path";
37461
+ function stamp2(now) {
37462
+ return now.toISOString().replace(/[-:.]/g, "");
37463
+ }
37464
+ function bundlePath(stateDir, now) {
37465
+ return join16(stateDir, `doctor-${stamp2(now)}.zip`);
37466
+ }
37467
+ function member(path, name) {
37468
+ try {
37469
+ return { name, data: readFileSync11(path) };
37470
+ } catch {
37471
+ return null;
37472
+ }
37473
+ }
37474
+ var MAX_REVIEW_MEMBERS = 5000;
37475
+ function entriesFor(deps) {
37476
+ const logs = deps.logPaths.map((p) => member(p, `logs/${basename3(p)}`));
37477
+ let reviews;
37478
+ try {
37479
+ reviews = readdirSync5(deps.reviewsDir).filter((f) => f.endsWith(".json"));
37480
+ } catch {
37481
+ reviews = [];
37482
+ }
37483
+ const records = reviews.slice(0, MAX_REVIEW_MEMBERS).map((f) => member(join16(deps.reviewsDir, f), `reviews/${f}`));
37484
+ return [...logs, ...records].filter((e) => e !== null);
37485
+ }
37486
+ async function runBundle(opts, deps) {
37487
+ if (!opts.yes) {
37488
+ if (!deps.isInteractive()) {
37489
+ return {
37490
+ kind: "refused",
37491
+ message: "a diagnostics bundle holds unredacted logs and full plan bodies, so it is never written unasked \u2014 re-run with --yes to confirm"
37492
+ };
37493
+ }
37494
+ if (await deps.confirm() !== true)
37495
+ return { kind: "declined" };
37496
+ }
37497
+ const now = deps.now();
37498
+ const path = bundlePath(deps.stateDir, now);
37499
+ deps.write(path, entriesFor(deps), now);
37500
+ return { kind: "written", path };
37501
+ }
37502
+
37503
+ // src/doctor/report.ts
36856
37504
  init_paths3();
36857
37505
  init_json_file();
36858
37506
  init_log();
36859
- import { readdirSync as readdirSync4, statSync as statSync4 } from "fs";
36860
- import { basename as basename3, join as join15 } from "path";
37507
+ init_node();
37508
+ import { readdirSync as readdirSync6, statSync as statSync4 } from "fs";
37509
+ import { basename as basename4, join as join17 } from "path";
36861
37510
  var CARET_BIN = "caret-native";
37511
+ var ERROR_RECORD_FIELDS = ["time", "source", "step", "code", "reviewId", "sessionId"];
36862
37512
  async function safe(build) {
36863
37513
  try {
36864
37514
  return await build();
@@ -36874,19 +37524,31 @@ async function collectReport(deps) {
36874
37524
  } catch (e) {
36875
37525
  healthError = { error: errorMessage(e) };
36876
37526
  }
36877
- const [system, install, settings, daemon, lockAndPort, processes, reviews, installState, logs] = await Promise.all([
37527
+ const [
37528
+ system,
37529
+ install,
37530
+ settings,
37531
+ daemon,
37532
+ lockAndPort,
37533
+ processes,
37534
+ reviews,
37535
+ installState,
37536
+ logs,
37537
+ failures
37538
+ ] = await Promise.all([
36878
37539
  safe(() => deps.system()),
36879
37540
  safe(() => deps.install()),
36880
37541
  safe(() => buildSettings(deps)),
36881
- healthError ?? safe(() => buildDaemon(health)),
37542
+ healthError ?? safe(() => buildDaemon(deps, health)),
36882
37543
  healthError ?? safe(() => buildLockAndPort(deps, health)),
36883
37544
  safe(() => buildProcesses(deps)),
36884
37545
  safe(() => tallyReviews(deps.listReviewFiles())),
36885
37546
  safe(() => deps.readAgentInstallState()),
36886
- safe(() => buildLogs(deps))
37547
+ safe(() => buildLogs(deps)),
37548
+ safe(() => buildFailures(deps))
36887
37549
  ]);
36888
37550
  return {
36889
- schema: "caret-discovery/1",
37551
+ schema: "caret-doctor/1",
36890
37552
  version: deps.version,
36891
37553
  generatedAt: deps.now().toISOString(),
36892
37554
  system,
@@ -36897,7 +37559,8 @@ async function collectReport(deps) {
36897
37559
  processes,
36898
37560
  reviews,
36899
37561
  installState,
36900
- logs
37562
+ logs,
37563
+ failures
36901
37564
  };
36902
37565
  }
36903
37566
  function buildSettings(deps) {
@@ -36918,11 +37581,13 @@ function buildSettings(deps) {
36918
37581
  effectiveHeartbeatMs: e.heartbeatMs
36919
37582
  };
36920
37583
  }
36921
- function buildDaemon(health) {
37584
+ function buildDaemon(deps, health) {
37585
+ const serviceInstalled = deps.serviceInstalled();
36922
37586
  if (!health)
36923
- return { reachable: false };
37587
+ return { reachable: false, serviceInstalled };
36924
37588
  return {
36925
37589
  reachable: true,
37590
+ serviceInstalled,
36926
37591
  service: health.service,
36927
37592
  daemonVersion: health.version,
36928
37593
  build: health.build,
@@ -36931,11 +37596,17 @@ function buildDaemon(health) {
36931
37596
  }
36932
37597
  function buildLockAndPort(deps, health) {
36933
37598
  const portServesCaret = health?.service === "caret";
37599
+ const marker = deps.readBootMarker();
37600
+ const bootFields = marker && {
37601
+ bootMarkerPid: marker.pid,
37602
+ bootMarkerAgeMs: deps.now().getTime() - marker.claimedAt
37603
+ };
36934
37604
  const lock = deps.readLock();
36935
37605
  if (!lock)
36936
- return { lockExists: false, portServesCaret };
37606
+ return { lockExists: false, portServesCaret, ...bootFields };
36937
37607
  return {
36938
37608
  lockExists: true,
37609
+ lockPath: daemonLock(),
36939
37610
  lockPid: lock.pid,
36940
37611
  lockPort: lock.port,
36941
37612
  lockBuild: lock.build,
@@ -36943,7 +37614,8 @@ function buildLockAndPort(deps, health) {
36943
37614
  lockStartedAt: lock.startedAt,
36944
37615
  pidAlive: deps.isPidAlive(lock.pid),
36945
37616
  portServesCaret,
36946
- portMismatch: lock.port !== deps.effective().port
37617
+ portMismatch: lock.port !== deps.effective().port,
37618
+ ...bootFields
36947
37619
  };
36948
37620
  }
36949
37621
  function buildProcesses(deps) {
@@ -36962,6 +37634,21 @@ async function buildLogs(deps) {
36962
37634
  ]);
36963
37635
  return { caret, daemon, daemonStderr };
36964
37636
  }
37637
+ async function buildFailures(deps) {
37638
+ const [caret, daemon] = await Promise.all([
37639
+ deps.logErrorRecords(deps.logPaths.caret),
37640
+ deps.logErrorRecords(deps.logPaths.daemon)
37641
+ ]);
37642
+ return groupFailures([...caret, ...daemon], deps.now().getTime());
37643
+ }
37644
+ var ERROR_WINDOW_HOURS = 24;
37645
+ var ERROR_WINDOW_MS = ERROR_WINDOW_HOURS * 60 * 60 * 1000;
37646
+ function inErrorWindow(time, generatedAt) {
37647
+ if (time === undefined)
37648
+ return true;
37649
+ const age = generatedAt - Date.parse(time);
37650
+ return Number.isNaN(age) || age < ERROR_WINDOW_MS;
37651
+ }
36965
37652
  function tallyReviews(records) {
36966
37653
  const counts = { pending: 0, approved: 0, rejected: 0, expired: 0, other: 0 };
36967
37654
  const pendingIds = [];
@@ -36987,11 +37674,47 @@ function tallyReviews(records) {
36987
37674
  }
36988
37675
  return { ...counts, total: records.length, pendingIds };
36989
37676
  }
36990
- var HEADER_KEYS = new Set(["schema", "version", "generatedAt"]);
36991
- function renderReport(report) {
37677
+ var FAILURES_CAP = 50;
37678
+ function groupFailures(records, generatedAt) {
37679
+ const recent = records.filter((r) => inErrorWindow(r.time, generatedAt)).sort((a, b) => compareTimes(a.time ?? "", b.time ?? ""));
37680
+ const newest = recent.slice(-FAILURES_CAP);
37681
+ const groups = new Map;
37682
+ const ungrouped = [];
37683
+ for (const { reviewId, sessionId, ...record } of newest) {
37684
+ const groupId = reviewId !== undefined ? { reviewId } : sessionId !== undefined ? { sessionId } : null;
37685
+ if (groupId === null) {
37686
+ ungrouped.push(record);
37687
+ continue;
37688
+ }
37689
+ const key = JSON.stringify(groupId);
37690
+ const group = groups.get(key) ?? { ...groupId, records: [] };
37691
+ group.records.push(record);
37692
+ groups.set(key, group);
37693
+ }
37694
+ return {
37695
+ windowHours: ERROR_WINDOW_HOURS,
37696
+ total: recent.length,
37697
+ omitted: recent.length - newest.length,
37698
+ groups: [...groups.values()],
37699
+ ungrouped
37700
+ };
37701
+ }
37702
+ function compareTimes(a, b) {
37703
+ return a < b ? -1 : a > b ? 1 : 0;
37704
+ }
37705
+ var HEADER_KEYS = new Set(["schema", "version", "generatedAt", "checks"]);
37706
+ var STATUS_GLYPH = { pass: "\u2713", fail: "\u2717", unknown: "?" };
37707
+ var STATUS_COLOR = { pass: "\x1B[32m", fail: "\x1B[31m", unknown: "\x1B[33m" };
37708
+ var COLOR_RESET = "\x1B[0m";
37709
+ function renderStdout(doc, format, color = false) {
37710
+ const redacted = scrubValue(doc, true);
37711
+ return format === "json" ? JSON.stringify(redacted, null, 2) : renderDocument(redacted, color);
37712
+ }
37713
+ function renderDocument(doc, color = false) {
36992
37714
  const lines = [];
36993
- lines.push(`caret discovery (${report.schema}) version ${report.version} at ${report.generatedAt}`);
36994
- const sections = Object.entries(report).filter(([key]) => !HEADER_KEYS.has(key));
37715
+ lines.push(`caret doctor (${doc.schema}) version ${doc.version} at ${doc.generatedAt}`);
37716
+ lines.push("", "checks:", ...renderChecks(doc.checks, color));
37717
+ const sections = Object.entries(doc).filter(([key]) => !HEADER_KEYS.has(key));
36995
37718
  for (const [title, value] of sections) {
36996
37719
  lines.push("");
36997
37720
  lines.push(`${title}:`);
@@ -37000,6 +37723,19 @@ function renderReport(report) {
37000
37723
  return lines.join(`
37001
37724
  `);
37002
37725
  }
37726
+ function renderChecks(checks, color) {
37727
+ const out = [];
37728
+ for (const c of checks) {
37729
+ const glyph = STATUS_GLYPH[c.status];
37730
+ const marker = color ? `${STATUS_COLOR[c.status]}${glyph}${COLOR_RESET}` : glyph;
37731
+ out.push(` ${marker} ${c.id}${c.detail ? ` \u2014 ${c.detail}` : ""}`);
37732
+ if (c.status === "fail")
37733
+ out.push(` remedy: ${c.remedy}`);
37734
+ if (c.status === "unknown")
37735
+ out.push(` reason: ${c.reason}`);
37736
+ }
37737
+ return out;
37738
+ }
37003
37739
  function isSectionError(v) {
37004
37740
  return typeof v === "object" && v !== null && "error" in v && typeof v.error === "string";
37005
37741
  }
@@ -37038,7 +37774,7 @@ function parsePsLines(text) {
37038
37774
  const [, pidStr, comm] = m ?? [];
37039
37775
  if (pidStr === undefined || comm === undefined)
37040
37776
  continue;
37041
- const name = basename3(comm.trim());
37777
+ const name = basename4(comm.trim());
37042
37778
  if (name === CARET_BIN)
37043
37779
  out.push({ pid: Number(pidStr), name });
37044
37780
  }
@@ -37058,7 +37794,7 @@ function listReviewFiles() {
37058
37794
  const dir = reviewsDir();
37059
37795
  let files;
37060
37796
  try {
37061
- files = readdirSync4(dir);
37797
+ files = readdirSync6(dir);
37062
37798
  } catch {
37063
37799
  return [];
37064
37800
  }
@@ -37068,7 +37804,7 @@ function listReviewFiles() {
37068
37804
  continue;
37069
37805
  if (out.length >= 5000)
37070
37806
  break;
37071
- const raw = readJsonFileSync(join15(dir, file));
37807
+ const raw = readJsonFileSync(join17(dir, file));
37072
37808
  if (raw && typeof raw.id === "string" && typeof raw.status === "string") {
37073
37809
  out.push({ id: raw.id, status: raw.status });
37074
37810
  }
@@ -37084,44 +37820,321 @@ async function logStats(path) {
37084
37820
  return { path, exists: false, size: 0, errors: 0, warns: 0 };
37085
37821
  }
37086
37822
  try {
37087
- const start = Math.max(0, size - TAIL_BYTES);
37088
- const text = await Bun.file(path).slice(start).text();
37089
- const { errors, warns } = countLogLevels(text, start > 0);
37090
- return { path, exists: true, size, errors, warns };
37823
+ const { text, partial } = await readTail(path, size);
37824
+ const { errors, warns, lastErrorAt } = countLogLevels(text, partial);
37825
+ return { path, exists: true, size, errors, warns, lastErrorAt };
37091
37826
  } catch {
37092
37827
  return { path, exists: true, size, errors: 0, warns: 0 };
37093
37828
  }
37094
37829
  }
37095
- function countLogLevels(tailText, dropFirstLine) {
37096
- let errors = 0;
37097
- let warns = 0;
37098
- const lines = tailText.split(`
37099
- `);
37100
- for (const [i, line] of lines.entries()) {
37830
+ async function readTail(path, size) {
37831
+ const start = Math.max(0, size - TAIL_BYTES);
37832
+ return { text: await Bun.file(path).slice(start).text(), partial: start > 0 };
37833
+ }
37834
+ async function logErrorRecords(path) {
37835
+ let size;
37836
+ try {
37837
+ size = statSync4(path).size;
37838
+ } catch {
37839
+ return [];
37840
+ }
37841
+ const { text, partial } = await readTail(path, size);
37842
+ return parseErrorRecords(text, partial);
37843
+ }
37844
+ function parseErrorRecords(tailText, dropFirstLine) {
37845
+ const out = [];
37846
+ for (const record of tailRecords(tailText, dropFirstLine)) {
37847
+ if (typeof record.level !== "number" || record.level < 50)
37848
+ continue;
37849
+ const triageFields = {};
37850
+ for (const field of ERROR_RECORD_FIELDS) {
37851
+ const value = record[field];
37852
+ if (typeof value === "string")
37853
+ triageFields[field] = value;
37854
+ }
37855
+ out.push(triageFields);
37856
+ }
37857
+ return out;
37858
+ }
37859
+ function* tailRecords(tailText, dropFirstLine) {
37860
+ for (const [i, line] of tailText.split(`
37861
+ `).entries()) {
37101
37862
  if (i === 0 && dropFirstLine)
37102
37863
  continue;
37103
37864
  if (!line.startsWith("{"))
37104
37865
  continue;
37105
- let level;
37866
+ let record;
37106
37867
  try {
37107
- level = JSON.parse(line).level;
37868
+ record = JSON.parse(line);
37108
37869
  } catch {
37109
37870
  continue;
37110
37871
  }
37111
- if (typeof level !== "number")
37872
+ if (typeof record === "object" && record !== null)
37873
+ yield record;
37874
+ }
37875
+ }
37876
+ function countLogLevels(tailText, dropFirstLine) {
37877
+ let errors = 0;
37878
+ let warns = 0;
37879
+ let lastErrorAt;
37880
+ for (const record of tailRecords(tailText, dropFirstLine)) {
37881
+ if (typeof record.level !== "number")
37112
37882
  continue;
37113
- if (level >= 50)
37883
+ if (record.level >= 50) {
37114
37884
  errors++;
37115
- else if (level === 40)
37885
+ lastErrorAt = typeof record.time === "string" ? record.time : undefined;
37886
+ } else if (record.level === 40)
37116
37887
  warns++;
37117
37888
  }
37118
- return { errors, warns };
37889
+ return { errors, warns, lastErrorAt };
37890
+ }
37891
+
37892
+ // src/doctor/checks.ts
37893
+ function present(value) {
37894
+ return isSectionError(value) ? undefined : value;
37895
+ }
37896
+ function undecided(id, title, section) {
37897
+ return {
37898
+ id,
37899
+ title,
37900
+ status: "unknown",
37901
+ detail: "",
37902
+ reason: `the ${section} section could not be collected`
37903
+ };
37904
+ }
37905
+ function daemonReachable(report) {
37906
+ const daemon = present(report.daemon);
37907
+ if (daemon === undefined)
37908
+ return undecided("daemon-reachable", "Daemon", "daemon");
37909
+ if (daemon.serviceInstalled && !daemon.reachable) {
37910
+ return {
37911
+ id: "daemon-reachable",
37912
+ title: "Daemon",
37913
+ status: "fail",
37914
+ detail: "a caret service is recorded for this install, but /api/health did not answer",
37915
+ remedy: "run `caret install --refresh` \u2014 or check the service is not disabled \u2014 then read logs/daemon-stderr.log"
37916
+ };
37917
+ }
37918
+ if (daemon.reachable && daemon.service !== "caret") {
37919
+ return {
37920
+ id: "daemon-reachable",
37921
+ title: "Daemon",
37922
+ status: "fail",
37923
+ detail: `the effective port is answering as ${daemon.service ?? "an unnamed service"}, not caret`,
37924
+ remedy: "stop whatever is bound to caret's effective port, or set `[daemon] port` to a free one"
37925
+ };
37926
+ }
37927
+ return {
37928
+ id: "daemon-reachable",
37929
+ title: "Daemon",
37930
+ status: "pass",
37931
+ detail: daemon.reachable ? "answering on the effective port" : "not running"
37932
+ };
37933
+ }
37934
+ function daemonLock2(report) {
37935
+ const lock = present(report.lockAndPort);
37936
+ if (lock === undefined)
37937
+ return undecided("daemon-lock", "Daemon lock", "lockAndPort");
37938
+ const faults = [];
37939
+ const remedies = [];
37940
+ if (lock.pidAlive === false) {
37941
+ faults.push(`its pid ${lock.lockPid} is not alive`);
37942
+ remedies.push(`delete the stale lock at ${lock.lockPath}`);
37943
+ }
37944
+ if (lock.portMismatch === true) {
37945
+ faults.push(`its port ${lock.lockPort} is not the effective one`);
37946
+ remedies.push("restart caret so it binds the configured port");
37947
+ }
37948
+ if (faults.length === 0) {
37949
+ return {
37950
+ id: "daemon-lock",
37951
+ title: "Daemon lock",
37952
+ status: "pass",
37953
+ detail: lock.lockExists ? "live, on the effective port" : "no lock file"
37954
+ };
37955
+ }
37956
+ return {
37957
+ id: "daemon-lock",
37958
+ title: "Daemon lock",
37959
+ status: "fail",
37960
+ detail: faults.join("; "),
37961
+ remedy: remedies.join("; ")
37962
+ };
37963
+ }
37964
+ function agentInstall(report) {
37965
+ const probe = present(report.installState);
37966
+ if (probe === undefined)
37967
+ return undecided("agent-install", "Agent install", "installState");
37968
+ if (probe.pluginEnabled === false) {
37969
+ return {
37970
+ id: "agent-install",
37971
+ title: "Agent install",
37972
+ status: "fail",
37973
+ detail: `${probe.agent} does not have caret enabled`,
37974
+ remedy: "run `caret install`"
37975
+ };
37976
+ }
37977
+ return {
37978
+ id: "agent-install",
37979
+ title: "Agent install",
37980
+ status: "pass",
37981
+ detail: probe.pluginEnabled === true ? `caret ${probe.pluginVersion} is enabled for ${probe.agent}` : `${probe.agent}'s install state could not be read`
37982
+ };
37983
+ }
37984
+ function describeErrors(log) {
37985
+ const when = log.lastErrorAt ? `, last at ${log.lastErrorAt}` : "";
37986
+ return `${log.errors} error record(s) in ${log.path}${when}`;
37987
+ }
37988
+ function logErrors(report) {
37989
+ const logs = present(report.logs);
37990
+ if (logs === undefined)
37991
+ return undecided("log-errors", "Logs", "logs");
37992
+ const noisy = Object.values(logs).filter((l) => l.errors > 0);
37993
+ if (noisy.length === 0) {
37994
+ return {
37995
+ id: "log-errors",
37996
+ title: "Logs",
37997
+ status: "pass",
37998
+ detail: "no NDJSON error records"
37999
+ };
38000
+ }
38001
+ const generatedAt = Date.parse(report.generatedAt);
38002
+ const erring = noisy.filter((l) => inErrorWindow(l.lastErrorAt, generatedAt));
38003
+ if (erring.length === 0) {
38004
+ return {
38005
+ id: "log-errors",
38006
+ title: "Logs",
38007
+ status: "pass",
38008
+ detail: `none in the last ${ERROR_WINDOW_HOURS}h; ${noisy.map(describeErrors).join("; ")}`
38009
+ };
38010
+ }
38011
+ return {
38012
+ id: "log-errors",
38013
+ title: "Logs",
38014
+ status: "fail",
38015
+ detail: erring.map(describeErrors).join("; "),
38016
+ remedy: `read ${erring.map((l) => l.path).join(" and ")}, then /systematic-debugging`
38017
+ };
38018
+ }
38019
+ function runChecks(report, adapterChecks = []) {
38020
+ return [
38021
+ daemonReachable(report),
38022
+ daemonLock2(report),
38023
+ agentInstall(report),
38024
+ logErrors(report),
38025
+ ...adapterChecks
38026
+ ];
38027
+ }
38028
+
38029
+ // src/doctor/zip.ts
38030
+ import { closeSync as closeSync2, openSync as openSync3, rmSync as rmSync2, writeSync } from "fs";
38031
+ import { crc32 } from "zlib";
38032
+ var LOCAL_SIG = 67324752;
38033
+ var CENTRAL_SIG = 33639248;
38034
+ var EOCD_SIG = 101010256;
38035
+ var ZIP_VERSION = 20;
38036
+ var DEFLATE = 8;
38037
+ var UNIX_HOST = 3;
38038
+ var ENTRY_MODE = 384;
38039
+ function dosStamp(now) {
38040
+ return {
38041
+ time: now.getUTCHours() << 11 | now.getUTCMinutes() << 5 | now.getUTCSeconds() >> 1,
38042
+ date: now.getUTCFullYear() - 1980 << 9 | now.getUTCMonth() + 1 << 5 | now.getUTCDate()
38043
+ };
38044
+ }
38045
+ function localHeader(r, stamp) {
38046
+ const head = new DataView(new ArrayBuffer(30));
38047
+ head.setUint32(0, LOCAL_SIG, true);
38048
+ head.setUint16(4, ZIP_VERSION, true);
38049
+ head.setUint16(6, 0, true);
38050
+ head.setUint16(8, DEFLATE, true);
38051
+ head.setUint16(10, stamp.time, true);
38052
+ head.setUint16(12, stamp.date, true);
38053
+ head.setUint32(14, r.crc, true);
38054
+ head.setUint32(18, r.compressedSize, true);
38055
+ head.setUint32(22, r.size, true);
38056
+ head.setUint16(26, r.name.length, true);
38057
+ head.setUint16(28, 0, true);
38058
+ return new Uint8Array(head.buffer);
38059
+ }
38060
+ function centralHeader(r, stamp) {
38061
+ const head = new DataView(new ArrayBuffer(46));
38062
+ head.setUint32(0, CENTRAL_SIG, true);
38063
+ head.setUint16(4, UNIX_HOST << 8 | ZIP_VERSION, true);
38064
+ head.setUint16(6, ZIP_VERSION, true);
38065
+ head.setUint16(8, 0, true);
38066
+ head.setUint16(10, DEFLATE, true);
38067
+ head.setUint16(12, stamp.time, true);
38068
+ head.setUint16(14, stamp.date, true);
38069
+ head.setUint32(16, r.crc, true);
38070
+ head.setUint32(20, r.compressedSize, true);
38071
+ head.setUint32(24, r.size, true);
38072
+ head.setUint16(28, r.name.length, true);
38073
+ head.setUint16(30, 0, true);
38074
+ head.setUint16(32, 0, true);
38075
+ head.setUint16(34, 0, true);
38076
+ head.setUint16(36, 0, true);
38077
+ head.setUint32(38, ENTRY_MODE << 16, true);
38078
+ head.setUint32(42, r.offset, true);
38079
+ return new Uint8Array(head.buffer);
38080
+ }
38081
+ function eocd(count, size, offset) {
38082
+ const head = new DataView(new ArrayBuffer(22));
38083
+ head.setUint32(0, EOCD_SIG, true);
38084
+ head.setUint16(4, 0, true);
38085
+ head.setUint16(6, 0, true);
38086
+ head.setUint16(8, count, true);
38087
+ head.setUint16(10, count, true);
38088
+ head.setUint32(12, size, true);
38089
+ head.setUint32(16, offset, true);
38090
+ head.setUint16(20, 0, true);
38091
+ return new Uint8Array(head.buffer);
38092
+ }
38093
+ function writeZip(path, entries, now) {
38094
+ const stamp = dosStamp(now);
38095
+ const encoder = new TextEncoder;
38096
+ const fd = openSync3(path, "wx", 384);
38097
+ try {
38098
+ const records = [];
38099
+ let offset = 0;
38100
+ const put = (bytes) => {
38101
+ const written = writeSync(fd, bytes);
38102
+ if (written !== bytes.length) {
38103
+ throw new Error(`zip write truncated: ${written} of ${bytes.length} bytes`);
38104
+ }
38105
+ offset += bytes.length;
38106
+ };
38107
+ for (const entry of entries) {
38108
+ const body = Bun.deflateSync(entry.data, { windowBits: -15 });
38109
+ const record = {
38110
+ name: encoder.encode(entry.name),
38111
+ compressedSize: body.length,
38112
+ crc: crc32(entry.data),
38113
+ size: entry.data.length,
38114
+ offset
38115
+ };
38116
+ records.push(record);
38117
+ put(localHeader(record, stamp));
38118
+ put(record.name);
38119
+ put(body);
38120
+ }
38121
+ const directoryStart = offset;
38122
+ for (const record of records) {
38123
+ put(centralHeader(record, stamp));
38124
+ put(record.name);
38125
+ }
38126
+ put(eocd(records.length, offset - directoryStart, directoryStart));
38127
+ } catch (e) {
38128
+ rmSync2(path, { force: true });
38129
+ throw e;
38130
+ } finally {
38131
+ closeSync2(fd);
38132
+ }
37119
38133
  }
37120
38134
 
37121
- // src/commands/discovery.ts
38135
+ // src/commands/doctor.ts
37122
38136
  init_build_id();
37123
- init_node();
37124
- function prodDiscoveryDeps(s) {
38137
+ function prodDoctorDeps(s) {
37125
38138
  return {
37126
38139
  now: () => new Date,
37127
38140
  version: VERSION,
@@ -37133,7 +38146,7 @@ function prodDiscoveryDeps(s) {
37133
38146
  }),
37134
38147
  settings: () => s,
37135
38148
  configPath: configFile(),
37136
- configExists: () => existsSync10(configFile()),
38149
+ configExists: () => existsSync11(configFile()),
37137
38150
  effective: () => ({
37138
38151
  port: getPort(s),
37139
38152
  idleMs: idleMs(s),
@@ -37142,12 +38155,18 @@ function prodDiscoveryDeps(s) {
37142
38155
  }),
37143
38156
  baseUrl: `http://localhost:${getPort(s)}`,
37144
38157
  health: httpHealth,
38158
+ serviceInstalled: () => existsSync11(launcherServiceFile()),
37145
38159
  readLock: readDaemonLock,
38160
+ readBootMarker,
37146
38161
  isPidAlive,
37147
38162
  listProcesses,
37148
38163
  listReviewFiles,
37149
- readAgentInstallState: () => selectAdapter().readInstallState(),
38164
+ readAgentInstallState: () => {
38165
+ const adapter = selectAdapter();
38166
+ return { agent: adapter.id, ...adapter.readInstallState() };
38167
+ },
37150
38168
  logStats,
38169
+ logErrorRecords,
37151
38170
  logPaths: {
37152
38171
  caret: logFile(),
37153
38172
  daemon: daemonLogFile(),
@@ -37155,19 +38174,66 @@ function prodDiscoveryDeps(s) {
37155
38174
  }
37156
38175
  };
37157
38176
  }
37158
- async function runDiscoverySubcommand(opts) {
38177
+ async function readAdapterChecks() {
37159
38178
  try {
38179
+ const config = resolveConfigFile(opencodeConfigDir());
38180
+ if (!hasCaretPluginEntry(config))
38181
+ return [];
38182
+ return [upgradeCheck(await readUpgradeVerdict({ configFile: config }))];
38183
+ } catch (e) {
38184
+ return [upgradeCheck({ kind: "unknown", reason: errorMessage(e) })];
38185
+ }
38186
+ }
38187
+ function prodBundleDeps() {
38188
+ return {
38189
+ stateDir: stateDir(),
38190
+ logPaths: [logFile(), daemonLogFile(), daemonStderrLogFile()],
38191
+ reviewsDir: reviewsDir(),
38192
+ now: () => new Date,
38193
+ isInteractive: isTerminal,
38194
+ confirm: confirmBundle,
38195
+ write: (path, entries, now) => {
38196
+ ensureStateDir();
38197
+ writeZip(path, entries, now);
38198
+ }
38199
+ };
38200
+ }
38201
+ async function confirmBundle() {
38202
+ await Promise.resolve().then(() => init_dist5());
38203
+ const answer = await confirm({
38204
+ message: "The bundle holds your logs and plan bodies, unredacted. Write it?"
38205
+ });
38206
+ return isCancel(answer) ? null : answer === true;
38207
+ }
38208
+ async function runDoctorSubcommand(opts) {
38209
+ try {
38210
+ const bundled = opts.bundle ? await runBundle({ yes: opts.yes }, prodBundleDeps()) : undefined;
38211
+ if (bundled?.kind === "refused") {
38212
+ process.stderr.write(`caret doctor: ${bundled.message}
38213
+ `);
38214
+ process.exit(2);
38215
+ }
38216
+ if (bundled?.kind === "written") {
38217
+ const note = opts.json ? process.stderr : process.stdout;
38218
+ note.write(`caret doctor: wrote ${bundled.path}
38219
+ `);
38220
+ note.write(`caret doctor: it is unredacted \u2014 move it over a channel you trust
38221
+ `);
38222
+ }
37160
38223
  const s = loadSettings();
37161
- const report = await collectReport(prodDiscoveryDeps(s));
37162
- const redacted = scrubValue(report, true);
37163
- const out = opts.json ? JSON.stringify(redacted, null, 2) : renderReport(redacted);
37164
- process.stdout.write(`${out}
38224
+ const [report, adapterChecks] = await Promise.all([
38225
+ collectReport(prodDoctorDeps(s)),
38226
+ readAdapterChecks()
38227
+ ]);
38228
+ const doc = { ...report, checks: runChecks(report, adapterChecks) };
38229
+ const color = process.stdout.isTTY === true && !process.env.NO_COLOR && process.env.CI !== "true";
38230
+ process.stdout.write(`${renderStdout(doc, opts.json ? "json" : "text", color)}
37165
38231
  `);
37166
- process.exit(0);
38232
+ process.exit(doc.checks.some((c) => c.status === "fail") ? 1 : 0);
37167
38233
  } catch (e) {
37168
- process.stderr.write(`caret discovery: ${e}
38234
+ process.stderr.write(`caret doctor: ${errorMessage(e)}
37169
38235
  `);
37170
- process.exit(1);
38236
+ process.exit(2);
37171
38237
  }
37172
38238
  }
37173
38239
 
@@ -37175,38 +38241,38 @@ async function runDiscoverySubcommand(opts) {
37175
38241
  import {
37176
38242
  accessSync as accessSync2,
37177
38243
  constants as constants2,
37178
- existsSync as existsSync12,
38244
+ existsSync as existsSync13,
37179
38245
  mkdirSync as mkdirSync3,
37180
- rmSync as rmSync2,
38246
+ rmSync as rmSync3,
37181
38247
  statSync as statSync5,
37182
38248
  symlinkSync,
37183
- writeFileSync as writeFileSync7
38249
+ writeFileSync as writeFileSync8
37184
38250
  } from "fs";
37185
- import { join as join17, resolve as resolve2 } from "path";
38251
+ import { join as join19, resolve as resolve2 } from "path";
37186
38252
 
37187
38253
  // src/adapters/opencode/packaging.ts
37188
- import { existsSync as existsSync11, readdirSync as readdirSync5, readFileSync as readFileSync10 } from "fs";
37189
- import { dirname as dirname8, join as join16 } from "path";
38254
+ import { existsSync as existsSync12, readdirSync as readdirSync7, readFileSync as readFileSync12 } from "fs";
38255
+ import { dirname as dirname9, join as join18 } from "path";
37190
38256
  function resolveCaretRoot() {
37191
38257
  const candidates = [];
37192
38258
  const script = process.argv[1];
37193
38259
  if (script)
37194
- candidates.push(join16(dirname8(script), ".."));
37195
- candidates.push(join16(dirname8(process.execPath), ".."));
38260
+ candidates.push(join18(dirname9(script), ".."));
38261
+ candidates.push(join18(dirname9(process.execPath), ".."));
37196
38262
  for (const root of candidates) {
37197
- if (existsSync11(join16(root, "opencode", "caret.plugin.ts")))
38263
+ if (existsSync12(join18(root, "opencode", "caret.plugin.ts")))
37198
38264
  return root;
37199
38265
  }
37200
38266
  throw new Error(`caret OpenCode packaging not found (no opencode/caret.plugin.ts under: ${candidates.join(", ") || "(none)"}). The OpenCode plugin source must ship with caret.`);
37201
38267
  }
37202
38268
  var DEMO_TEMPLATE = "templates/demo.md";
37203
38269
  function loadOpencodePackaging(root = resolveCaretRoot()) {
37204
- const binPath = join16(root, "bin", "caret");
37205
- const demoTemplate = readFileSync10(join16(root, DEMO_TEMPLATE), "utf-8");
37206
- const commandsDir = join16(root, "opencode", "commands");
38270
+ const binPath = join18(root, "bin", "caret");
38271
+ const demoTemplate = readFileSync12(join18(root, DEMO_TEMPLATE), "utf-8");
38272
+ const commandsDir = join18(root, "opencode", "commands");
37207
38273
  let commands = [];
37208
38274
  try {
37209
- commands = readdirSync5(commandsDir).filter((f) => f.endsWith(".md")).sort().map((f) => ({ name: f, contents: readFileSync10(join16(commandsDir, f), "utf-8") }));
38275
+ commands = readdirSync7(commandsDir).filter((f) => f.endsWith(".md")).sort().map((f) => ({ name: f, contents: readFileSync12(join18(commandsDir, f), "utf-8") }));
37210
38276
  } catch {
37211
38277
  commands = [];
37212
38278
  }
@@ -37216,17 +38282,17 @@ function loadOpencodePackaging(root = resolveCaretRoot()) {
37216
38282
  // src/commands/install/local.ts
37217
38283
  init_paths3();
37218
38284
  function devMarketplaceDir() {
37219
- return join17(stateDir(), "dev-marketplace");
38285
+ return join19(stateDir(), "dev-marketplace");
37220
38286
  }
37221
38287
  function resolveLocalCheckout(opts = {}, deps = {}) {
37222
38288
  const repoDir = resolve2(caretRoot(deps));
37223
- if (!existsSync12(join17(repoDir, ".claude-plugin", "marketplace.json"))) {
38289
+ if (!existsSync13(join19(repoDir, ".claude-plugin", "marketplace.json"))) {
37224
38290
  throw new Error(`--from-local must run from a caret checkout (no .claude-plugin/marketplace.json at ${repoDir}). A published caret installs with a plain \`caret install\`.`);
37225
38291
  }
37226
38292
  if (opts.requireArtifacts !== false) {
37227
38293
  const missing = [
37228
- isExecutable(join17(repoDir, "bin", "caret-native")) ? null : "bin/caret-native",
37229
- isDirectory(join17(repoDir, "bin", "ui")) ? null : "bin/ui"
38294
+ isExecutable(join19(repoDir, "bin", "caret-native")) ? null : "bin/caret-native",
38295
+ isDirectory(join19(repoDir, "bin", "ui")) ? null : "bin/ui"
37230
38296
  ].filter((m) => m !== null);
37231
38297
  if (missing.length > 0) {
37232
38298
  throw new Error(`--from-local reuses the build artifacts bin/caret-native + bin/ui, and ${repoDir} is missing ${missing.join(" + ")} \u2014 run \`mise run build\` first.`);
@@ -37244,10 +38310,10 @@ function caretRoot(deps) {
37244
38310
  }
37245
38311
  }
37246
38312
  function writeDevMarketplace(repoDir, outDir) {
37247
- rmSync2(outDir, { recursive: true, force: true });
37248
- mkdirSync3(join17(outDir, ".claude-plugin"), { recursive: true });
37249
- symlinkSync(resolve2(repoDir), join17(outDir, "caret"));
37250
- writeFileSync7(join17(outDir, ".claude-plugin", "marketplace.json"), `${JSON.stringify({
38313
+ rmSync3(outDir, { recursive: true, force: true });
38314
+ mkdirSync3(join19(outDir, ".claude-plugin"), { recursive: true });
38315
+ symlinkSync(resolve2(repoDir), join19(outDir, "caret"));
38316
+ writeFileSync8(join19(outDir, ".claude-plugin", "marketplace.json"), `${JSON.stringify({
37251
38317
  name: "caret",
37252
38318
  description: "Local caret dev build (generated by caret install --from-local).",
37253
38319
  owner: { name: "macintacos" },
@@ -37258,7 +38324,7 @@ function writeDevMarketplace(repoDir, outDir) {
37258
38324
  async function prewarmLocalBuild(repoDir, deps = {}) {
37259
38325
  const spawn = deps.spawn ?? runCaptured;
37260
38326
  try {
37261
- await spawn([join17(repoDir, "bin", "caret-native"), "prewarm"]);
38327
+ await spawn([join19(repoDir, "bin", "caret-native"), "prewarm"]);
37262
38328
  } catch {}
37263
38329
  }
37264
38330
  function isExecutable(path) {
@@ -37294,75 +38360,6 @@ async function runCaptured(cmd) {
37294
38360
  return await proc.exited;
37295
38361
  }
37296
38362
 
37297
- // src/commands/install/ui.ts
37298
- function isTerminal() {
37299
- return process.stdin.isTTY === true && process.stdout.isTTY === true;
37300
- }
37301
- var silentUI = {
37302
- intro: () => {},
37303
- step: async (_label, work) => work(() => {}),
37304
- info: () => {},
37305
- warn: () => {},
37306
- error: () => {},
37307
- note: () => {},
37308
- cancel: () => {},
37309
- outro: () => {}
37310
- };
37311
- function plainUI(write) {
37312
- const line = (s) => write(`caret: ${s}
37313
- `);
37314
- return {
37315
- intro: (action) => line(action),
37316
- async step(label, work, done) {
37317
- try {
37318
- const value = await work(() => {});
37319
- line(done ? done(value) : label);
37320
- return value;
37321
- } catch (e) {
37322
- line(`${label} \u2014 failed`);
37323
- throw e;
37324
- }
37325
- },
37326
- info: line,
37327
- warn: line,
37328
- error: line,
37329
- note: (body, title) => line(`${title}
37330
- ${body.split(`
37331
- `).map((l) => ` ${l}`).join(`
37332
- `)}`),
37333
- cancel: line,
37334
- outro: line
37335
- };
37336
- }
37337
- async function createInstallUI(opts = {}) {
37338
- const write = opts.write ?? ((s) => void process.stdout.write(s));
37339
- const interactive = opts.interactive ?? (process.stdout.isTTY === true && !process.env.NO_COLOR && process.env.CI !== "true");
37340
- if (!interactive)
37341
- return plainUI(write);
37342
- const clack = await Promise.resolve().then(() => (init_dist5(), {}));
37343
- return {
37344
- intro: (action) => intro(`caret \xB7 ${action}`),
37345
- async step(label, work, done) {
37346
- const spin = spinner();
37347
- spin.start(label);
37348
- try {
37349
- const value = await work((m) => spin.message(m));
37350
- spin.stop(done ? done(value) : label);
37351
- return value;
37352
- } catch (e) {
37353
- spin.error(label);
37354
- throw e;
37355
- }
37356
- },
37357
- info: (m) => log.info(m),
37358
- warn: (m) => log.warn(m),
37359
- error: (m) => log.error(m),
37360
- note: (body, title) => note(body, title),
37361
- cancel: (m) => cancel(m),
37362
- outro: (m) => outro(m)
37363
- };
37364
- }
37365
-
37366
38363
  // src/commands/install/claude.ts
37367
38364
  var MARKETPLACE_SOURCE = "macintacos/caret";
37368
38365
  var MARKETPLACE_NAME = "caret";
@@ -37537,11 +38534,11 @@ async function runInstallClaudeTarget(opts, deps = {}) {
37537
38534
 
37538
38535
  // src/commands/install/opencode.ts
37539
38536
  init_config_plugin();
37540
- import { existsSync as existsSync16, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
37541
- import { basename as basename4, join as join19 } from "path";
38537
+ import { existsSync as existsSync16, writeFileSync as writeFileSync10 } from "fs";
38538
+ import { basename as basename5, join as join20 } from "path";
37542
38539
 
37543
38540
  // src/adapters/opencode/deploy.ts
37544
- import { existsSync as existsSync14, mkdirSync as mkdirSync4, rmSync as rmSync3, writeFileSync as writeFileSync8 } from "fs";
38541
+ import { existsSync as existsSync14, mkdirSync as mkdirSync4, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "fs";
37545
38542
  import { dirname as dirname10 } from "path";
37546
38543
  function renderPlugin(source, opts) {
37547
38544
  return source.replaceAll("__CARET_VERSION__", () => opts.version).replaceAll("__CARET_BIN__", () => opts.binPath).replaceAll("__CARET_DEMO_TEMPLATE__", () => opts.demoTemplate);
@@ -37551,7 +38548,7 @@ function deployFiles(files, opts) {
37551
38548
  for (const f of files) {
37552
38549
  if (!opts.dryRun) {
37553
38550
  mkdirSync4(dirname10(f.path), { recursive: true });
37554
- writeFileSync8(f.path, f.contents);
38551
+ writeFileSync9(f.path, f.contents);
37555
38552
  }
37556
38553
  paths.push(f.path);
37557
38554
  }
@@ -37563,7 +38560,7 @@ function removeFiles(targets, opts) {
37563
38560
  if (!existsSync14(p))
37564
38561
  continue;
37565
38562
  if (!opts.dryRun)
37566
- rmSync3(p, { force: true });
38563
+ rmSync4(p, { force: true });
37567
38564
  paths.push(p);
37568
38565
  }
37569
38566
  return { paths, dryRun: opts.dryRun };
@@ -37573,6 +38570,9 @@ function removeFiles(targets, opts) {
37573
38570
  init_paths2();
37574
38571
  init_upgrade();
37575
38572
 
38573
+ // src/commands/install/prompt.ts
38574
+ init_upgrade();
38575
+
37576
38576
  // src/commands/install/targets.ts
37577
38577
  init_paths2();
37578
38578
  import { existsSync as existsSync15 } from "fs";
@@ -37631,20 +38631,6 @@ async function promptForServiceChoice() {
37631
38631
  });
37632
38632
  return isCancel(choice) ? null : choice;
37633
38633
  }
37634
- function upgradeVerdictLine(verdict) {
37635
- switch (verdict.kind) {
37636
- case "fresh":
37637
- return "OpenCode will resolve caret on its next start";
37638
- case "current":
37639
- return `OpenCode's caret is ${verdict.version} \u2014 already current`;
37640
- case "stale-cache":
37641
- return `OpenCode's cached caret is ${verdict.cached}; ${verdict.published} is published`;
37642
- case "stale-pin":
37643
- return `Your config pins ${verdict.entry}; ${verdict.published} is published`;
37644
- case "unknown":
37645
- return "Could not check which caret OpenCode would load";
37646
- }
37647
- }
37648
38634
  function upgradePromptMessage(verdict) {
37649
38635
  const remedy = verdict.kind === "stale-pin" ? "Bump the pin?" : "Clear the cached copy so OpenCode re-resolves?";
37650
38636
  return `${upgradeVerdictLine(verdict)}. ${remedy}`;
@@ -37658,7 +38644,7 @@ async function promptUpgrade(verdict, clack) {
37658
38644
  // src/commands/install/opencode.ts
37659
38645
  init_build_id();
37660
38646
  function isCaretCheckout(dir) {
37661
- return existsSync16(join19(dir, "opencode", "caret.plugin.ts"));
38647
+ return existsSync16(join20(dir, "opencode", "caret.plugin.ts"));
37662
38648
  }
37663
38649
  function caretEntries(text, isCheckout) {
37664
38650
  return pluginEntries(text).filter((entry) => {
@@ -37684,12 +38670,12 @@ async function runInstallOpencodeTarget(opts, deps = {}) {
37684
38670
  const ui = deps.ui ?? silentUI;
37685
38671
  const isCheckout = deps.isCheckout ?? isCaretCheckout;
37686
38672
  const configFile = resolveConfigFile(dir);
37687
- const commandPaths = pkg.commands.map((c) => join19(commandDir(dir), namespacedCommandFilename(c.name)));
38673
+ const commandPaths = pkg.commands.map((c) => join20(commandDir(dir), namespacedCommandFilename(c.name)));
37688
38674
  const legacy = existingLegacyInstallFiles(dir);
37689
38675
  const specifier = opts.local ? localPluginSpecifier(opts.local.repoDir) : CARET_PACKAGE;
37690
38676
  if (opts.dryRun) {
37691
38677
  const verb = opts.uninstall ? "remove" : "write";
37692
- const found = checks3(opts) ? ["", previewLine(await check(configFile, deps))] : [];
38678
+ const found = checks3(opts) ? ["", previewLine(await readVerdict(configFile, deps))] : [];
37693
38679
  const entry = opts.uninstall ? [] : ["", `plugin entry: ${specifier}`];
37694
38680
  const sweep = legacy.length === 0 ? [] : ["", "pre-array-install files to remove:", ...legacy];
37695
38681
  ui.note([configFile, ...commandPaths, ...entry, ...sweep, ...found].join(`
@@ -37697,17 +38683,17 @@ async function runInstallOpencodeTarget(opts, deps = {}) {
37697
38683
  return;
37698
38684
  }
37699
38685
  if (opts.uninstall) {
37700
- await ui.step("Removing caret from OpenCode's plugin array", async () => editConfig(configFile, (text) => text === null ? null : caretEntries(text, isCheckout).reduce((acc, entry) => removePluginFromConfigText(acc, entry), text)), (changed) => changed.length > 0 ? `Removed caret from ${basename4(configFile)}` : `caret was not in ${basename4(configFile)}`);
38686
+ await ui.step("Removing caret from OpenCode's plugin array", async () => editConfig(configFile, (text) => text === null ? null : caretEntries(text, isCheckout).reduce((acc, entry) => removePluginFromConfigText(acc, entry), text)), (changed) => changed.length > 0 ? `Removed caret from ${basename5(configFile)}` : `caret was not in ${basename5(configFile)}`);
37701
38687
  await ui.step("Removing the /caret:* command files", async () => removeFiles(commandPaths, { dryRun: false }), (removed) => `Removed ${removed.paths.length} command file(s) from ${dir}`);
37702
38688
  await sweepLegacy(legacy, dir, ui);
37703
38689
  return;
37704
38690
  }
37705
- await ui.step(`Adding ${specifier} to OpenCode's plugin array`, async () => editConfig(configFile, (text) => setCaretPluginEntry(text, specifier, isCheckout)), (changed) => changed.length > 0 ? `Added ${specifier} to ${basename4(configFile)}` : `${specifier} was already in ${basename4(configFile)}`);
38691
+ await ui.step(`Adding ${specifier} to OpenCode's plugin array`, async () => editConfig(configFile, (text) => setCaretPluginEntry(text, specifier, isCheckout)), (changed) => changed.length > 0 ? `Added ${specifier} to ${basename5(configFile)}` : `${specifier} was already in ${basename5(configFile)}`);
37706
38692
  if (checks3(opts))
37707
38693
  await upgradeStep(configFile, opts, deps, ui);
37708
38694
  await sweepLegacy(legacy, dir, ui);
37709
38695
  const files = pkg.commands.map((c) => ({
37710
- path: join19(commandDir(dir), namespacedCommandFilename(c.name)),
38696
+ path: join20(commandDir(dir), namespacedCommandFilename(c.name)),
37711
38697
  contents: renderPlugin(c.contents, {
37712
38698
  version: VERSION,
37713
38699
  binPath: pkg.binPath,
@@ -37728,15 +38714,11 @@ function previewLine(verdict) {
37728
38714
  const line = upgradeVerdictLine(verdict);
37729
38715
  return verdict.kind === "unknown" ? `${line} (${verdict.reason})` : line;
37730
38716
  }
37731
- async function check(configFile, deps) {
37732
- return upgradeVerdict({
37733
- entry: findPluginEntry(readConfigText(configFile), CARET_PACKAGE),
37734
- cached: readCachedCaretVersion((deps.cacheDirs ?? existingOpencodeCachePackageDirs)()),
37735
- published: await (deps.published ?? publishedCaretVersion)()
37736
- });
38717
+ async function readVerdict(configFile, deps) {
38718
+ return readUpgradeVerdict({ configFile, cacheDirs: deps.cacheDirs, published: deps.published });
37737
38719
  }
37738
38720
  async function upgradeStep(configFile, opts, deps, ui) {
37739
- const verdict = await ui.step("Checking OpenCode's caret version", () => check(configFile, deps), upgradeVerdictLine);
38721
+ const verdict = await ui.step("Checking OpenCode's caret version", () => readVerdict(configFile, deps), upgradeVerdictLine);
37740
38722
  if (verdict.kind === "unknown") {
37741
38723
  ui.warn(`Could not check OpenCode's caret version (${verdict.reason}) \u2014 nothing changed.`);
37742
38724
  return;
@@ -37757,36 +38739,33 @@ async function upgradeStep(configFile, opts, deps, ui) {
37757
38739
  await ui.step("Clearing OpenCode's cached caret", async () => (deps.clearCache ?? clearCachedCaret)(dirs), (cleared) => `Cleared ${cleared.length} cached ${cleared.length === 1 ? "copy" : "copies"} \u2014 OpenCode re-resolves on next start`);
37758
38740
  return;
37759
38741
  }
37760
- await ui.step(`Bumping ${CARET_PACKAGE} to ${verdict.published}`, async () => editConfig(configFile, (text) => text === null ? null : setPluginVersionInConfigText(text, CARET_PACKAGE, verdict.published)), (changed) => changed.length > 0 ? `Bumped the pin to ${CARET_PACKAGE}@${verdict.published}` : `Left ${basename4(configFile)} unchanged`);
37761
- }
37762
- function readConfigText(path) {
37763
- return existsSync16(path) ? readFileSync11(path, "utf-8") : null;
38742
+ await ui.step(`Bumping ${CARET_PACKAGE} to ${verdict.published}`, async () => editConfig(configFile, (text) => text === null ? null : setPluginVersionInConfigText(text, CARET_PACKAGE, verdict.published)), (changed) => changed.length > 0 ? `Bumped the pin to ${CARET_PACKAGE}@${verdict.published}` : `Left ${basename5(configFile)} unchanged`);
37764
38743
  }
37765
38744
  function editConfig(path, transform) {
37766
38745
  const existing = readConfigText(path);
37767
38746
  const next = transform(existing);
37768
38747
  if (next === null || next === existing)
37769
38748
  return [];
37770
- writeFileSync9(path, next);
38749
+ writeFileSync10(path, next);
37771
38750
  return [path];
37772
38751
  }
37773
38752
 
37774
38753
  // src/commands/install/launcher.ts
37775
- import { chmodSync as chmodSync4, copyFileSync, existsSync as existsSync17, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "fs";
37776
- import { dirname as dirname11, join as join20 } from "path";
38754
+ import { chmodSync as chmodSync4, copyFileSync, existsSync as existsSync17, renameSync as renameSync5, rmSync as rmSync5, writeFileSync as writeFileSync11 } from "fs";
38755
+ import { dirname as dirname11, join as join21 } from "path";
37777
38756
  init_paths3();
37778
38757
  init_build_id();
37779
38758
  function installLauncher(deps = {}) {
37780
- const source = (deps.source ?? (() => join20(resolveCaretRoot(), "bin", "caret-launcher")))();
38759
+ const source = (deps.source ?? (() => join21(resolveCaretRoot(), "bin", "caret-launcher")))();
37781
38760
  ensureStateDir(dirname11(launcherPath()));
37782
38761
  const tmp = `${launcherPath()}.${process.pid}.tmp`;
37783
38762
  copyFileSync(source, tmp);
37784
38763
  chmodSync4(tmp, 493);
37785
- renameSync4(tmp, launcherPath());
38764
+ renameSync5(tmp, launcherPath());
37786
38765
  const bunPath = deps.bunPath ?? (buildKind() === "binary" ? undefined : process.execPath);
37787
38766
  const unpinned = deps.pinnedRoot === undefined && existsSync17(launcherPinnedRootFile());
37788
38767
  if (unpinned)
37789
- rmSync4(launcherPinnedRootFile(), { force: true });
38768
+ rmSync5(launcherPinnedRootFile(), { force: true });
37790
38769
  if (bunPath === undefined && deps.serviceLabel === undefined && deps.pinnedRoot === undefined) {
37791
38770
  return { unpinned };
37792
38771
  }
@@ -37800,12 +38779,12 @@ function installLauncher(deps = {}) {
37800
38779
  return { unpinned };
37801
38780
  }
37802
38781
  function writeRecord(path, value) {
37803
- writeFileSync10(path, `${value}
38782
+ writeFileSync11(path, `${value}
37804
38783
  `, { mode: 384 });
37805
38784
  }
37806
38785
  function uninstallLauncher() {
37807
- rmSync4(dirname11(launcherPath()), { recursive: true, force: true });
37808
- rmSync4(launcherRecordDir(), { recursive: true, force: true });
38786
+ rmSync5(dirname11(launcherPath()), { recursive: true, force: true });
38787
+ rmSync5(launcherRecordDir(), { recursive: true, force: true });
37809
38788
  }
37810
38789
 
37811
38790
  // src/commands/install/service.ts
@@ -38051,9 +39030,9 @@ function servicePlatform(platform = process.platform) {
38051
39030
 
38052
39031
  // src/service/launchd-manager.ts
38053
39032
  init_log();
38054
- import { mkdirSync as mkdirSync5, rmSync as rmSync5, writeFileSync as writeFileSync11 } from "fs";
39033
+ import { mkdirSync as mkdirSync5, rmSync as rmSync6, writeFileSync as writeFileSync12 } from "fs";
38055
39034
  import { homedir as homedir6 } from "os";
38056
- import { join as join21 } from "path";
39035
+ import { join as join22 } from "path";
38057
39036
 
38058
39037
  // src/service/launchd.ts
38059
39038
  var EXIT_TIMEOUT_SEC = 20;
@@ -38114,10 +39093,10 @@ function commandError(tool, verb, result) {
38114
39093
  }
38115
39094
 
38116
39095
  // src/service/unit-file.ts
38117
- import { readFileSync as readFileSync12 } from "fs";
39096
+ import { readFileSync as readFileSync13 } from "fs";
38118
39097
  function unitUnchanged(path, text) {
38119
39098
  try {
38120
- return readFileSync12(path, "utf-8") === text;
39099
+ return readFileSync13(path, "utf-8") === text;
38121
39100
  } catch {
38122
39101
  return false;
38123
39102
  }
@@ -38129,14 +39108,14 @@ var RUNNING = [/^\s*state\s*=\s*running\b/m, /^\s*pid\s*=\s*\d+/m];
38129
39108
  var DISABLED2 = new RegExp(`"${LAUNCHD_LABEL.replace(/\./g, "\\.")}"\\s*=>\\s*(?:true|disabled)\\b`);
38130
39109
  var launchctlError = (action, result) => commandError("launchctl", action, result);
38131
39110
  function createLaunchdManager(deps = {}) {
38132
- const launchAgentsDir = deps.launchAgentsDir ?? join21(homedir6(), "Library", "LaunchAgents");
39111
+ const launchAgentsDir = deps.launchAgentsDir ?? join22(homedir6(), "Library", "LaunchAgents");
38133
39112
  const launchctl = deps.launchctl ?? spawnLaunchctl;
38134
39113
  const uid = deps.uid ?? process.getuid?.();
38135
39114
  if (uid === undefined)
38136
39115
  throw new Error("caret service: launchd needs a POSIX uid");
38137
39116
  const domain = `gui/${uid}`;
38138
39117
  const target = `${domain}/${LAUNCHD_LABEL}`;
38139
- const plistPath = join21(launchAgentsDir, `${LAUNCHD_LABEL}.plist`);
39118
+ const plistPath = join22(launchAgentsDir, `${LAUNCHD_LABEL}.plist`);
38140
39119
  async function readStatus() {
38141
39120
  const [printed, printDisabled] = await Promise.all([
38142
39121
  launchctl(["print", target]),
@@ -38165,7 +39144,7 @@ function createLaunchdManager(deps = {}) {
38165
39144
  }
38166
39145
  }
38167
39146
  mkdirSync5(launchAgentsDir, { recursive: true });
38168
- writeFileSync11(plistPath, text);
39147
+ writeFileSync12(plistPath, text);
38169
39148
  await launchctl(["bootout", target]);
38170
39149
  const bootstrapped = await launchctl(["bootstrap", domain, plistPath]);
38171
39150
  if (bootstrapped.code !== 0)
@@ -38173,7 +39152,7 @@ function createLaunchdManager(deps = {}) {
38173
39152
  },
38174
39153
  async uninstall() {
38175
39154
  await launchctl(["bootout", target]);
38176
- rmSync5(plistPath, { force: true });
39155
+ rmSync6(plistPath, { force: true });
38177
39156
  },
38178
39157
  status: readStatus,
38179
39158
  async restart() {
@@ -38186,9 +39165,9 @@ function createLaunchdManager(deps = {}) {
38186
39165
 
38187
39166
  // src/service/systemd-manager.ts
38188
39167
  init_log();
38189
- import { mkdirSync as mkdirSync6, rmSync as rmSync6, writeFileSync as writeFileSync12 } from "fs";
39168
+ import { mkdirSync as mkdirSync6, rmSync as rmSync7, writeFileSync as writeFileSync13 } from "fs";
38190
39169
  import { homedir as homedir7 } from "os";
38191
- import { join as join22 } from "path";
39170
+ import { join as join23 } from "path";
38192
39171
 
38193
39172
  // src/service/systemd.ts
38194
39173
  var START_LIMIT_INTERVAL_SEC = 60;
@@ -38230,10 +39209,10 @@ WantedBy=default.target
38230
39209
 
38231
39210
  // src/service/systemd-manager.ts
38232
39211
  function createSystemdManager(deps = {}) {
38233
- const configHome = process.env.XDG_CONFIG_HOME || join22(homedir7(), ".config");
38234
- const unitDir = deps.unitDir ?? join22(configHome, "systemd", "user");
39212
+ const configHome = process.env.XDG_CONFIG_HOME || join23(homedir7(), ".config");
39213
+ const unitDir = deps.unitDir ?? join23(configHome, "systemd", "user");
38235
39214
  const run = deps.run ?? runCommand;
38236
- const unitPath = join22(unitDir, SYSTEMD_UNIT);
39215
+ const unitPath = join23(unitDir, SYSTEMD_UNIT);
38237
39216
  const systemctl = (...args) => run(["systemctl", "--user", ...args]);
38238
39217
  async function probeSystemd() {
38239
39218
  const shown = await systemctl("show-environment");
@@ -38292,7 +39271,7 @@ function createSystemdManager(deps = {}) {
38292
39271
  }
38293
39272
  mkdirSync6(unitDir, { recursive: true });
38294
39273
  try {
38295
- writeFileSync12(unitPath, text);
39274
+ writeFileSync13(unitPath, text);
38296
39275
  const reloaded = await systemctl("daemon-reload");
38297
39276
  if (reloaded.code !== 0)
38298
39277
  throw commandError("systemctl", "daemon-reload", reloaded);
@@ -38304,14 +39283,14 @@ function createSystemdManager(deps = {}) {
38304
39283
  if (restarted.code !== 0)
38305
39284
  throw commandError("systemctl", "restart", restarted);
38306
39285
  } catch (err) {
38307
- rmSync6(unitPath, { force: true });
39286
+ rmSync7(unitPath, { force: true });
38308
39287
  throw err;
38309
39288
  }
38310
39289
  await enableLinger();
38311
39290
  },
38312
39291
  async uninstall() {
38313
39292
  await systemctl("disable", "--now", SYSTEMD_UNIT);
38314
- rmSync6(unitPath, { force: true });
39293
+ rmSync7(unitPath, { force: true });
38315
39294
  await systemctl("daemon-reload");
38316
39295
  },
38317
39296
  status: readStatus,
@@ -38369,79 +39348,6 @@ init_boot();
38369
39348
  init_settings();
38370
39349
  init_log();
38371
39350
 
38372
- // src/review/reconcile.ts
38373
- init_log();
38374
- async function runReconcile(stdin, deps) {
38375
- const ctx = {};
38376
- try {
38377
- const input = deps.parseHookInput(stdin);
38378
- ctx.sessionId = input.sessionId;
38379
- if (!input.sessionId)
38380
- return;
38381
- const match = (await deps.listReviews()).find((r) => r.sessionId === input.sessionId);
38382
- if (!match) {
38383
- logDebug("reconcile", "no pending review for session; nothing to reconcile", { ...ctx });
38384
- return;
38385
- }
38386
- ctx.reviewId = match.id;
38387
- await deps.resolveReview(match.id);
38388
- logInfo("reconcile", "terminal approval reconciled", { ...ctx });
38389
- } catch (err) {
38390
- logDebug("reconcile", "reconcile skipped", { ...ctx, err: String(err) });
38391
- }
38392
- }
38393
-
38394
- // src/commands/reconcile.ts
38395
- function prodReconcileDeps(baseUrl, adapter) {
38396
- return {
38397
- parseHookInput: (stdin) => adapter.parseHookInput(stdin),
38398
- listReviews: () => listReviews(baseUrl),
38399
- resolveReview: (id) => resolveReview(baseUrl, id, { behavior: "allow" })
38400
- };
38401
- }
38402
- async function runReconcileSubcommand() {
38403
- try {
38404
- const loaded = loadSettings();
38405
- bootHookLogging(loaded);
38406
- const adapter = selectAdapter();
38407
- const baseUrl = `http://localhost:${getPort(loaded)}`;
38408
- const stdin = await Bun.stdin.text();
38409
- await runReconcile(stdin, prodReconcileDeps(baseUrl, adapter));
38410
- } catch (err) {
38411
- logDebug("reconcile", "reconcile subcommand skipped", { err: String(err) });
38412
- }
38413
- process.exit(0);
38414
- }
38415
-
38416
- // src/commands/redact.ts
38417
- init_node();
38418
- function runRedactSubcommand() {
38419
- try {
38420
- const written = redactLogFiles();
38421
- if (written.length === 0) {
38422
- process.stdout.write(`caret redact: no logs found to redact.
38423
- `);
38424
- } else {
38425
- for (const path of written)
38426
- process.stdout.write(`${path}
38427
- `);
38428
- }
38429
- process.exit(0);
38430
- } catch (e) {
38431
- process.stderr.write(`caret redact: ${e}
38432
- `);
38433
- process.exit(1);
38434
- }
38435
- }
38436
-
38437
- // src/commands/review.ts
38438
- init_adapters();
38439
- init_boot();
38440
- init_paths3();
38441
- init_settings();
38442
- init_lifecycle();
38443
- init_log();
38444
-
38445
39351
  // src/review/orchestrate.ts
38446
39352
  init_constants();
38447
39353
  init_paths3();
@@ -38466,9 +39372,27 @@ function hasUntaggedCodeBlock(plan) {
38466
39372
  function denyDecision(reason) {
38467
39373
  return { behavior: "deny", feedback: reason, decidedAt: Date.now() };
38468
39374
  }
39375
+ function parseHook(parse, stdin) {
39376
+ try {
39377
+ return { input: parse(stdin) };
39378
+ } catch (error) {
39379
+ return { error };
39380
+ }
39381
+ }
38469
39382
 
38470
39383
  class TimeoutError extends Error {
38471
39384
  }
39385
+ var REVIEW_FAILURE_CODES = {
39386
+ parse: "hook-input-invalid",
39387
+ validatePlan: "unexpected",
39388
+ ensureDaemon: "daemon-unreachable",
39389
+ postReview: "review-create-failed",
39390
+ reconnect: "daemon-unreachable",
39391
+ longPoll: "unexpected"
39392
+ };
39393
+ function reviewFailureCode(step, err) {
39394
+ return err instanceof TimeoutError ? "review-timeout" : REVIEW_FAILURE_CODES[step];
39395
+ }
38472
39396
  function withTimeout(p, ms, message) {
38473
39397
  return new Promise((resolve, reject) => {
38474
39398
  const t = setTimeout(() => reject(new TimeoutError(message)), ms);
@@ -38485,25 +39409,29 @@ async function expireAbandoned(expire, handle) {
38485
39409
  if (!handle)
38486
39410
  return;
38487
39411
  try {
38488
- await expire(handle.baseUrl, handle.id);
39412
+ await expire(handle.baseUrl, handle.id, handle.version);
38489
39413
  } catch {}
38490
39414
  }
38491
- async function runReview(stdin, deps) {
39415
+ async function runReview(parsed, deps) {
38492
39416
  let step = "parse";
38493
- const ctx = {};
39417
+ setLogContext({});
38494
39418
  let baseUrl;
39419
+ let version;
39420
+ let reviewId;
38495
39421
  try {
38496
- const input = deps.parseHookInput(stdin);
38497
- ctx.sessionId = input.sessionId;
38498
- ctx.cwd = input.cwd;
38499
- logInfo("review", "review requested", { ...ctx });
39422
+ if ("error" in parsed)
39423
+ throw parsed.error;
39424
+ const input = parsed.input;
39425
+ const ids = { sessionId: input.sessionId, cwd: input.cwd };
39426
+ setLogContext(ids);
39427
+ logInfo("review", "review requested");
38500
39428
  step = "validatePlan";
38501
39429
  if (!input.plan?.trim()) {
38502
- logInfo(step, "plan rejected: plan is empty", ctx);
39430
+ logInfo(step, "plan rejected: plan is empty");
38503
39431
  return denyDecision(PLAN_EMPTY_DENY_MESSAGE);
38504
39432
  }
38505
39433
  if (hasUntaggedCodeBlock(input.plan)) {
38506
- logInfo(step, "plan rejected: code block missing language marker", ctx);
39434
+ logInfo(step, "plan rejected: code block missing language marker");
38507
39435
  return denyDecision(PLAN_FORMAT_DENY_MESSAGE);
38508
39436
  }
38509
39437
  step = "ensureDaemon";
@@ -38512,7 +39440,7 @@ async function runReview(stdin, deps) {
38512
39440
  const payload = { ...input, cmux: deps.readPane?.() };
38513
39441
  let created = await deps.postReview(baseUrl, payload);
38514
39442
  if (!created) {
38515
- logInfo("review", "review refused: daemon draining", { ...ctx });
39443
+ logInfo("review", "review refused: daemon draining");
38516
39444
  step = "reconnect";
38517
39445
  baseUrl = await deps.ensureDaemon("successor");
38518
39446
  step = "postReview";
@@ -38520,10 +39448,12 @@ async function runReview(stdin, deps) {
38520
39448
  if (!created)
38521
39449
  throw new Error("daemon draining; review not created");
38522
39450
  }
38523
- const { id, hasLiveClient } = created;
38524
- ctx.reviewId = id;
38525
- deps.onPosted?.(baseUrl, id);
38526
- logDebug("review", `review created: ${shortId(id)}`, { ...ctx });
39451
+ const { id, hasLiveClient, planFileCurrent } = created;
39452
+ version = created.version;
39453
+ reviewId = id;
39454
+ setLogContext({ ...ids, reviewId });
39455
+ deps.onPosted?.({ baseUrl, id, version, planFileCurrent });
39456
+ logDebug("review", `review created: ${shortId(id)}`);
38527
39457
  const open = new URL(baseUrl);
38528
39458
  open.hostname = VANITY_HOST;
38529
39459
  const url = `${open.origin}/?review=${id}`;
@@ -38538,7 +39468,12 @@ async function runReview(stdin, deps) {
38538
39468
  if (remaining <= 0)
38539
39469
  throw new TimeoutError("review timed out");
38540
39470
  try {
38541
- decision = await withTimeout(deps.longPoll(baseUrl, id), remaining, "review timed out") ?? undefined;
39471
+ const polled = await withTimeout(deps.longPoll(baseUrl, id, version), remaining, "review timed out");
39472
+ if (polled === "superseded") {
39473
+ logInfo("review", `review yielded to newer version: ${shortId(id)}`, { version });
39474
+ return denyDecision("caret: superseded by a newer revision of this plan.");
39475
+ }
39476
+ decision = polled ?? undefined;
38542
39477
  } catch (err) {
38543
39478
  if (err instanceof TimeoutError)
38544
39479
  throw err;
@@ -38548,19 +39483,19 @@ async function runReview(stdin, deps) {
38548
39483
  }
38549
39484
  }
38550
39485
  if (decision.behavior === "deny") {
38551
- logInfo("decision", "plan rejected", { ...ctx, feedbackChars: decision.feedback?.length });
39486
+ logInfo("decision", "plan rejected", { feedbackChars: decision.feedback?.length });
38552
39487
  } else {
38553
- logInfo("decision", "plan approved", { ...ctx, acceptMode: decision.acceptMode });
39488
+ logInfo("decision", "plan approved", { acceptMode: decision.acceptMode });
38554
39489
  }
38555
39490
  return decision;
38556
39491
  } catch (err) {
38557
- logError(step, err, ctx);
38558
- if (ctx.reviewId && baseUrl) {
39492
+ logError(step, reviewFailureCode(step, err), err);
39493
+ if (reviewId && baseUrl) {
38559
39494
  try {
38560
- await deps.expire(baseUrl, ctx.reviewId);
38561
- logDebug("review", `review expire requested: ${shortId(ctx.reviewId)}`, { ...ctx });
39495
+ await deps.expire(baseUrl, reviewId, version);
39496
+ logDebug("review", `review expire requested: ${shortId(reviewId)}`);
38562
39497
  } catch {
38563
- logDebug("review", "review expire failed; resubmit supersedes", { ...ctx });
39498
+ logDebug("review", "review expire failed; next plan reclaims it");
38564
39499
  }
38565
39500
  }
38566
39501
  const msg = errorMessage(err);
@@ -38568,7 +39503,79 @@ async function runReview(stdin, deps) {
38568
39503
  }
38569
39504
  }
38570
39505
 
39506
+ // src/review/reconcile.ts
39507
+ init_log();
39508
+ async function runReconcile(parsed, deps) {
39509
+ const ctx = {};
39510
+ try {
39511
+ if ("error" in parsed)
39512
+ throw parsed.error;
39513
+ const input = parsed.input;
39514
+ ctx.sessionId = input.sessionId;
39515
+ if (!input.sessionId)
39516
+ return;
39517
+ const match = (await deps.listReviews()).find((r) => r.sessionId === input.sessionId);
39518
+ if (!match) {
39519
+ logDebug("reconcile", "no pending review for session; nothing to reconcile", { ...ctx });
39520
+ return;
39521
+ }
39522
+ ctx.reviewId = match.id;
39523
+ await deps.resolveReview(match.id);
39524
+ logInfo("reconcile", "terminal approval reconciled", { ...ctx });
39525
+ } catch (err) {
39526
+ logDebug("reconcile", "reconcile skipped", { ...ctx, err: String(err) });
39527
+ }
39528
+ }
39529
+
39530
+ // src/commands/reconcile.ts
39531
+ function prodReconcileDeps(baseUrl) {
39532
+ return {
39533
+ listReviews: () => listReviews(baseUrl),
39534
+ resolveReview: (id) => resolveReview(baseUrl, id, { behavior: "allow" })
39535
+ };
39536
+ }
39537
+ async function runReconcileSubcommand() {
39538
+ try {
39539
+ const loaded = loadSettings();
39540
+ bootHookLogging(loaded);
39541
+ const adapter = selectAdapter();
39542
+ const baseUrl = `http://localhost:${getPort(loaded)}`;
39543
+ const stdin = await Bun.stdin.text();
39544
+ await runReconcile(parseHook(adapter.parseHookInput, stdin), prodReconcileDeps(baseUrl));
39545
+ } catch (err) {
39546
+ logDebug("reconcile", "reconcile subcommand skipped", { err: String(err) });
39547
+ }
39548
+ process.exit(0);
39549
+ }
39550
+
39551
+ // src/commands/redact.ts
39552
+ init_node();
39553
+ function runRedactSubcommand() {
39554
+ try {
39555
+ const written = redactLogFiles();
39556
+ if (written.length === 0) {
39557
+ process.stdout.write(`caret redact: no logs found to redact.
39558
+ `);
39559
+ } else {
39560
+ for (const path of written)
39561
+ process.stdout.write(`${path}
39562
+ `);
39563
+ }
39564
+ process.exit(0);
39565
+ } catch (e) {
39566
+ process.stderr.write(`caret redact: ${e}
39567
+ `);
39568
+ process.exit(1);
39569
+ }
39570
+ }
39571
+
38571
39572
  // src/commands/review.ts
39573
+ init_adapters();
39574
+ init_boot();
39575
+ init_paths3();
39576
+ init_settings();
39577
+ init_lifecycle();
39578
+ init_log();
38572
39579
  function browserOpenCmd(platform, url) {
38573
39580
  return platform === "darwin" ? ["open", url] : platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
38574
39581
  }
@@ -38576,6 +39583,13 @@ function reviewUrlLine(url) {
38576
39583
  return `caret: review this plan at ${url}
38577
39584
  `;
38578
39585
  }
39586
+ function notesAppendTarget(decision, planFilePath, posted) {
39587
+ if (decision.behavior !== "allow" || !decision.feedback || !planFilePath)
39588
+ return;
39589
+ if (posted?.planFileCurrent === false)
39590
+ return "skip-moved-on";
39591
+ return { path: planFilePath, notes: decision.feedback };
39592
+ }
38579
39593
  function openBrowser(url) {
38580
39594
  try {
38581
39595
  Bun.spawn(browserOpenCmd(process.platform, url), {
@@ -38584,10 +39598,9 @@ function openBrowser(url) {
38584
39598
  } catch {}
38585
39599
  }
38586
39600
  var REVIEW_RESERVE_MS = SUPERVISOR_WINDOW_MS;
38587
- function prodReviewDeps(s, adapter) {
39601
+ function prodReviewDeps(settings) {
38588
39602
  return {
38589
- parseHookInput: (stdin) => adapter.parseHookInput(stdin),
38590
- ensureDaemon: async (mode) => ensureDaemon(await prodEnsureDeps(s, () => prodService().manager, REVIEW_RESERVE_MS), mode),
39603
+ ensureDaemon: async (mode) => ensureDaemon(await prodEnsureDeps(settings, () => prodService().manager, REVIEW_RESERVE_MS), mode),
38591
39604
  postReview,
38592
39605
  longPoll,
38593
39606
  openBrowser,
@@ -38595,10 +39608,18 @@ function prodReviewDeps(s, adapter) {
38595
39608
  process.stderr.write(reviewUrlLine(url));
38596
39609
  },
38597
39610
  readPane: readCmuxPane,
38598
- timeoutMs: reviewTimeoutMs(s),
39611
+ timeoutMs: reviewTimeoutMs(settings),
38599
39612
  expire: expireReview
38600
39613
  };
38601
39614
  }
39615
+ async function reviewHookInput(parsed, deps, readPlan) {
39616
+ if ("error" in parsed)
39617
+ return { decision: await runReview(parsed, deps) };
39618
+ const payload = parsed.input;
39619
+ const fromFile = payload.planFilePath ? readPlan(payload.planFilePath) : undefined;
39620
+ const input = fromFile?.trim() ? { ...payload, plan: fromFile } : payload;
39621
+ return { decision: await runReview({ input }, deps), input };
39622
+ }
38602
39623
  async function runReviewSubcommand() {
38603
39624
  const loaded = loadSettings();
38604
39625
  bootHookLogging(loaded);
@@ -38615,7 +39636,7 @@ async function runReviewSubcommand() {
38615
39636
  };
38616
39637
  const denyAndExit = async (reason) => {
38617
39638
  if (!responded)
38618
- logError("signal", new Error(reason));
39639
+ logError("signal", "hook-interrupted", new Error(reason));
38619
39640
  respond({ behavior: "deny", feedback: `${reason} See ${logFile()}.`, decidedAt: Date.now() });
38620
39641
  await expireAbandoned(expireReview, posted);
38621
39642
  process.exit(0);
@@ -38623,18 +39644,17 @@ async function runReviewSubcommand() {
38623
39644
  process.once("SIGINT", () => void denyAndExit("caret: interrupted (SIGINT) \u2014 denying to fail safe."));
38624
39645
  process.once("SIGTERM", () => void denyAndExit("caret: terminated (SIGTERM) \u2014 denying to fail safe."));
38625
39646
  const stdin = await Bun.stdin.text();
38626
- try {
38627
- hookInput = adapter.parseHookInput(stdin);
38628
- } catch {
38629
- hookInput = undefined;
38630
- }
38631
- const deps = prodReviewDeps(loaded, adapter);
38632
- deps.onPosted = (baseUrl, id) => {
38633
- posted = { baseUrl, id };
38634
- };
38635
- const out = await runReview(stdin, deps);
38636
- if (out.behavior === "allow" && out.feedback && hookInput?.planFilePath) {
38637
- appendReviewerNotesToPlanFile(hookInput.planFilePath, out.feedback, { warn: logWarn });
39647
+ const deps = prodReviewDeps(loaded);
39648
+ deps.onPosted = (handle) => {
39649
+ posted = handle;
39650
+ };
39651
+ const { decision: out, input } = await reviewHookInput(parseHook(adapter.parseHookInput, stdin), deps, readPlanFile);
39652
+ hookInput = input;
39653
+ const target = notesAppendTarget(out, hookInput?.planFilePath, posted);
39654
+ if (target === "skip-moved-on") {
39655
+ logInfo("review", "plan file changed; notes append skipped");
39656
+ } else if (target) {
39657
+ appendReviewerNotesToPlanFile(target.path, target.notes, { warn: logWarn });
38638
39658
  }
38639
39659
  respond(out);
38640
39660
  process.exit(0);
@@ -40763,7 +41783,11 @@ function buildProgram() {
40763
41783
  program.command("reconcile").description("reconcile a terminal plan approval into the daemon (ExitPlanMode PostToolUse hook)").action(() => runReconcileSubcommand());
40764
41784
  program.command("steer").description("steer the model to title its plan (EnterPlanMode PostToolUse / UserPromptSubmit hook)").action(() => runSteerSubcommand());
40765
41785
  program.command("redact").description("write redacted copies of the logs").action(() => runRedactSubcommand());
40766
- program.command("discovery").description("print a diagnostics report").option("--json", "emit the machine-readable JSON document").action((opts) => runDiscoverySubcommand({ json: opts.json ?? false }));
41786
+ program.command("doctor").description("diagnose this caret install").option("--json", "emit the machine-readable JSON document").option("--bundle", "also write a zip of the raw logs and review records, to share").option("--yes", "write the bundle without asking \u2014 it holds unredacted content").action((opts) => runDoctorSubcommand({
41787
+ json: opts.json ?? false,
41788
+ bundle: opts.bundle ?? false,
41789
+ yes: opts.yes ?? false
41790
+ }));
40767
41791
  program.command("install").description("install caret into a coding agent, plus the rumdl plan formatter, and choose whether the review UI runs as a service").option("--uninstall", "remove caret from every agent on this machine, and its service, instead of installing").option("--dry-run", "print what would change without writing").option("--refresh", "take the published caret in every target without asking \u2014 clears OpenCode's cached copy, or bumps a pinned entry (Claude Code always takes an update) \u2014 and cycles caret's service onto it").option("--from-local", "dev loop: install the built caret checkout this binary runs from, then hand it the daemon").action(async (opts) => {
40768
41792
  const outcome = await runInstallSubcommand({
40769
41793
  uninstall: opts.uninstall ?? false,
@@ -40777,7 +41801,7 @@ function buildProgram() {
40777
41801
  }
40778
41802
  if (import.meta.main) {
40779
41803
  runProgram(buildProgram(), (err) => {
40780
- logError("fatal", err);
41804
+ logError("fatal", "unexpected", err);
40781
41805
  const reason = `caret: fatal ${err} \u2014 denying to fail safe. See ${logFile()}.`;
40782
41806
  process.stdout.write(`${fatalDeny(reason)}
40783
41807
  `);