harnery 0.31.6 → 0.33.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 (264) hide show
  1. package/README.md +2 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +5 -1
  4. package/dist/commands/agents.d.ts +0 -9
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +280 -94
  7. package/dist/commands/browse-session.d.ts +21 -0
  8. package/dist/commands/browse-session.d.ts.map +1 -0
  9. package/dist/commands/browse-session.js +157 -0
  10. package/dist/commands/browse.d.ts.map +1 -1
  11. package/dist/commands/browse.js +205 -37
  12. package/dist/commands/checkpoint.js +1 -1
  13. package/dist/commands/claude-desktop.d.ts +2 -2
  14. package/dist/commands/claude-desktop.d.ts.map +1 -1
  15. package/dist/commands/claude-desktop.js +74 -3
  16. package/dist/commands/deinit.d.ts.map +1 -1
  17. package/dist/commands/deinit.js +4 -0
  18. package/dist/commands/docs.d.ts.map +1 -1
  19. package/dist/commands/docs.js +40 -0
  20. package/dist/commands/doctor.d.ts +4 -2
  21. package/dist/commands/doctor.d.ts.map +1 -1
  22. package/dist/commands/doctor.js +141 -19
  23. package/dist/commands/init.d.ts +2 -2
  24. package/dist/commands/init.d.ts.map +1 -1
  25. package/dist/commands/init.js +26 -5
  26. package/dist/commands/instructions.d.ts +4 -0
  27. package/dist/commands/instructions.d.ts.map +1 -0
  28. package/dist/commands/instructions.js +100 -0
  29. package/dist/commands/presence.d.ts.map +1 -1
  30. package/dist/commands/presence.js +4 -1
  31. package/dist/commands/tunnel.d.ts.map +1 -1
  32. package/dist/commands/tunnel.js +2 -3
  33. package/dist/core/agents/canonical-emit.d.ts +2 -3
  34. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  35. package/dist/core/agents/canonical-emit.js +6 -5
  36. package/dist/core/agents/cli.js +128 -89
  37. package/dist/core/agents/coord-client.d.ts +18 -3
  38. package/dist/core/agents/coord-client.d.ts.map +1 -1
  39. package/dist/core/agents/coord-client.js +5 -4
  40. package/dist/core/agents/finalization.d.ts +68 -0
  41. package/dist/core/agents/finalization.d.ts.map +1 -0
  42. package/dist/core/agents/finalization.js +443 -0
  43. package/dist/core/agents/git-hook.d.ts +51 -0
  44. package/dist/core/agents/git-hook.d.ts.map +1 -0
  45. package/dist/core/agents/git-hook.js +118 -0
  46. package/dist/core/agents/index.d.ts +1 -0
  47. package/dist/core/agents/index.d.ts.map +1 -1
  48. package/dist/core/agents/index.js +1 -0
  49. package/dist/core/agents/render/prompt-context.d.ts +12 -5
  50. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  51. package/dist/core/agents/render/prompt-context.js +70 -16
  52. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  53. package/dist/core/agents/render/session-context.js +23 -4
  54. package/dist/core/agents/rules/claim-conflict.js +3 -3
  55. package/dist/core/agents/rules/commit-conflict.d.ts +15 -6
  56. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  57. package/dist/core/agents/rules/commit-conflict.js +21 -5
  58. package/dist/core/agents/rules/stop-hook.d.ts +3 -0
  59. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  60. package/dist/core/agents/rules/stop-hook.js +59 -23
  61. package/dist/core/agents/session-events.d.ts +8 -16
  62. package/dist/core/agents/session-events.d.ts.map +1 -1
  63. package/dist/core/agents/session-events.js +12 -26
  64. package/dist/core/agents/state/heartbeat-projector.d.ts +14 -0
  65. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  66. package/dist/core/agents/state/heartbeat-projector.js +98 -1
  67. package/dist/core/agents/state/heartbeat-writer.d.ts +47 -5
  68. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  69. package/dist/core/agents/state/heartbeat-writer.js +86 -17
  70. package/dist/core/agents/state/names.d.ts +22 -1
  71. package/dist/core/agents/state/names.d.ts.map +1 -1
  72. package/dist/core/agents/state/names.js +40 -2
  73. package/dist/core/agents/state/session-state.d.ts +44 -0
  74. package/dist/core/agents/state/session-state.d.ts.map +1 -0
  75. package/dist/core/agents/state/session-state.js +81 -0
  76. package/dist/core/config.d.ts +30 -2
  77. package/dist/core/config.d.ts.map +1 -1
  78. package/dist/core/config.js +74 -11
  79. package/dist/core/governor/planning.d.ts.map +1 -1
  80. package/dist/core/governor/planning.js +12 -13
  81. package/dist/core/hooks/adapter/detect.d.ts +2 -4
  82. package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
  83. package/dist/core/hooks/adapter/detect.js +4 -14
  84. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  85. package/dist/core/hooks/adapter/events.js +1 -0
  86. package/dist/core/hooks/adapter/output.d.ts +1 -1
  87. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  88. package/dist/core/hooks/adapter/output.js +5 -4
  89. package/dist/core/hooks/adapter/parse.d.ts +1 -1
  90. package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
  91. package/dist/core/hooks/adapter/parse.js +2 -0
  92. package/dist/core/hooks/adapter/wiring.d.ts +23 -0
  93. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  94. package/dist/core/hooks/adapter/wiring.js +32 -0
  95. package/dist/core/hooks/cli.d.ts +1 -2
  96. package/dist/core/hooks/cli.d.ts.map +1 -1
  97. package/dist/core/hooks/cli.js +258 -32
  98. package/dist/core/hooks/codex-authorization.d.ts +30 -0
  99. package/dist/core/hooks/codex-authorization.d.ts.map +1 -0
  100. package/dist/core/hooks/codex-authorization.js +199 -0
  101. package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
  102. package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
  103. package/dist/core/hooks/codex-wsl-bridge.js +137 -0
  104. package/dist/core/hooks/effects/index.d.ts +1 -1
  105. package/dist/core/hooks/effects/index.js +3 -3
  106. package/dist/core/hooks/events/schema.d.ts +57 -1
  107. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  108. package/dist/core/hooks/resolve/owner.d.ts +7 -1
  109. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  110. package/dist/core/hooks/resolve/owner.js +15 -0
  111. package/dist/core/hooks/resolve/transcript.d.ts +44 -0
  112. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  113. package/dist/core/hooks/resolve/transcript.js +176 -1
  114. package/dist/core/hooks/session-name-presence.d.ts +30 -0
  115. package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
  116. package/dist/core/hooks/session-name-presence.js +43 -0
  117. package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
  118. package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
  119. package/dist/core/hooks/unsafe-cross-shell.js +151 -0
  120. package/dist/core/presence/blob.d.ts +4 -0
  121. package/dist/core/presence/blob.d.ts.map +1 -1
  122. package/dist/core/presence/blob.js +12 -0
  123. package/dist/core/presence/index.js +15 -1
  124. package/dist/core/work/state.d.ts +4 -5
  125. package/dist/core/work/state.d.ts.map +1 -1
  126. package/dist/core/work/state.js +5 -8
  127. package/dist/core/workflow/index.d.ts +1 -1
  128. package/dist/core/workflow/index.d.ts.map +1 -1
  129. package/dist/core/workflow/proof.d.ts +2 -2
  130. package/dist/core/workflow/proof.d.ts.map +1 -1
  131. package/dist/core/workflow/proof.js +2 -2
  132. package/dist/core/workflow/run-state.d.ts +2 -2
  133. package/dist/core/workflow/run-state.d.ts.map +1 -1
  134. package/dist/core/workflow/run-state.js +2 -2
  135. package/dist/core/workflow/types.d.ts +3 -3
  136. package/dist/core/workflow/types.d.ts.map +1 -1
  137. package/dist/core/workflow/workspaces/execution.d.ts +2 -2
  138. package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
  139. package/dist/core/workflow/workspaces/execution.js +5 -0
  140. package/dist/core/workflow/workspaces/index.d.ts +1 -1
  141. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  142. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  143. package/dist/core/workflow/workspaces/local-git.js +1 -2
  144. package/dist/core/workflow/workspaces/paths.d.ts +2 -2
  145. package/dist/core/workflow/workspaces/types.d.ts +0 -6
  146. package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
  147. package/dist/core/workflow/workspaces/validate.d.ts +0 -2
  148. package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
  149. package/dist/core/workflow/workspaces/validate.js +0 -2
  150. package/dist/lib/browser/client.d.ts +118 -1
  151. package/dist/lib/browser/client.d.ts.map +1 -1
  152. package/dist/lib/browser/client.js +435 -6
  153. package/dist/lib/browser/geometry.d.ts.map +1 -1
  154. package/dist/lib/browser/geometry.js +180 -37
  155. package/dist/lib/browser/index.d.ts +5 -1
  156. package/dist/lib/browser/index.d.ts.map +1 -1
  157. package/dist/lib/browser/index.js +5 -1
  158. package/dist/lib/browser/netscape-cookies.d.ts +6 -0
  159. package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
  160. package/dist/lib/browser/netscape-cookies.js +31 -0
  161. package/dist/lib/browser/proxy.d.ts +24 -0
  162. package/dist/lib/browser/proxy.d.ts.map +1 -0
  163. package/dist/lib/browser/proxy.js +84 -0
  164. package/dist/lib/browser/runts.d.ts +6 -0
  165. package/dist/lib/browser/runts.d.ts.map +1 -1
  166. package/dist/lib/browser/runts.js +21 -4
  167. package/dist/lib/browser/session-control.d.ts +112 -0
  168. package/dist/lib/browser/session-control.d.ts.map +1 -0
  169. package/dist/lib/browser/session-control.js +670 -0
  170. package/dist/lib/claude-desktop.d.ts +47 -0
  171. package/dist/lib/claude-desktop.d.ts.map +1 -1
  172. package/dist/lib/claude-desktop.js +169 -1
  173. package/dist/lib/docs-links.d.ts +108 -0
  174. package/dist/lib/docs-links.d.ts.map +1 -0
  175. package/dist/lib/docs-links.js +555 -0
  176. package/dist/lib/exec.d.ts +1 -1
  177. package/dist/lib/exec.js +1 -1
  178. package/dist/lib/identities/assume.d.ts +4 -2
  179. package/dist/lib/identities/assume.d.ts.map +1 -1
  180. package/dist/lib/identities/assume.js +15 -2
  181. package/dist/lib/instructions/bundle.d.ts +33 -0
  182. package/dist/lib/instructions/bundle.d.ts.map +1 -0
  183. package/dist/lib/instructions/bundle.js +149 -0
  184. package/dist/lib/instructions/git-hooks.d.ts +75 -0
  185. package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
  186. package/dist/lib/instructions/git-hooks.js +238 -0
  187. package/dist/lib/instructions/splice.d.ts +10 -4
  188. package/dist/lib/instructions/splice.d.ts.map +1 -1
  189. package/dist/lib/instructions/splice.js +19 -11
  190. package/package.json +2 -2
  191. package/schemas/config.schema.json +32 -0
  192. package/src/commander.ts +5 -1
  193. package/src/commands/agents.ts +341 -113
  194. package/src/commands/browse-session.ts +245 -0
  195. package/src/commands/browse.ts +287 -44
  196. package/src/commands/checkpoint.ts +1 -1
  197. package/src/commands/claude-desktop.ts +106 -5
  198. package/src/commands/deinit.ts +7 -1
  199. package/src/commands/docs.ts +56 -0
  200. package/src/commands/doctor.ts +155 -21
  201. package/src/commands/init.ts +28 -9
  202. package/src/commands/instructions.ts +129 -0
  203. package/src/commands/presence.ts +7 -1
  204. package/src/commands/tunnel.ts +2 -5
  205. package/src/core/agents/canonical-emit.ts +7 -6
  206. package/src/core/agents/cli.ts +143 -89
  207. package/src/core/agents/coord-client.ts +21 -5
  208. package/src/core/agents/finalization.ts +595 -0
  209. package/src/core/agents/git-hook.ts +126 -0
  210. package/src/core/agents/index.ts +1 -0
  211. package/src/core/agents/render/prompt-context.ts +96 -16
  212. package/src/core/agents/render/session-context.ts +29 -4
  213. package/src/core/agents/rules/claim-conflict.ts +3 -3
  214. package/src/core/agents/rules/commit-conflict.ts +35 -8
  215. package/src/core/agents/rules/stop-hook.ts +71 -23
  216. package/src/core/agents/session-events.ts +14 -41
  217. package/src/core/agents/state/heartbeat-projector.ts +108 -1
  218. package/src/core/agents/state/heartbeat-writer.ts +129 -16
  219. package/src/core/agents/state/names.ts +54 -2
  220. package/src/core/agents/state/session-state.ts +131 -0
  221. package/src/core/config.ts +92 -12
  222. package/src/core/governor/planning.ts +17 -12
  223. package/src/core/hooks/adapter/detect.ts +4 -13
  224. package/src/core/hooks/adapter/events.ts +1 -0
  225. package/src/core/hooks/adapter/output.ts +9 -4
  226. package/src/core/hooks/adapter/parse.ts +3 -0
  227. package/src/core/hooks/adapter/wiring.ts +43 -0
  228. package/src/core/hooks/cli.ts +296 -27
  229. package/src/core/hooks/codex-authorization.ts +253 -0
  230. package/src/core/hooks/codex-wsl-bridge.ts +188 -0
  231. package/src/core/hooks/effects/index.ts +5 -5
  232. package/src/core/hooks/events/schema.ts +71 -0
  233. package/src/core/hooks/resolve/owner.ts +22 -1
  234. package/src/core/hooks/resolve/transcript.ts +164 -1
  235. package/src/core/hooks/session-name-presence.ts +52 -0
  236. package/src/core/hooks/unsafe-cross-shell.ts +160 -0
  237. package/src/core/presence/blob.ts +18 -0
  238. package/src/core/presence/index.ts +18 -1
  239. package/src/core/work/state.ts +8 -12
  240. package/src/core/workflow/engine.ts +2 -2
  241. package/src/core/workflow/index.ts +0 -1
  242. package/src/core/workflow/proof.ts +4 -4
  243. package/src/core/workflow/run-state.ts +5 -5
  244. package/src/core/workflow/types.ts +3 -3
  245. package/src/core/workflow/workspaces/execution.ts +11 -4
  246. package/src/core/workflow/workspaces/index.ts +0 -3
  247. package/src/core/workflow/workspaces/local-git.ts +1 -2
  248. package/src/core/workflow/workspaces/paths.ts +2 -2
  249. package/src/core/workflow/workspaces/types.ts +0 -9
  250. package/src/core/workflow/workspaces/validate.ts +1 -5
  251. package/src/lib/browser/client.ts +528 -6
  252. package/src/lib/browser/geometry.ts +197 -37
  253. package/src/lib/browser/index.ts +39 -0
  254. package/src/lib/browser/netscape-cookies.ts +39 -0
  255. package/src/lib/browser/proxy.ts +105 -0
  256. package/src/lib/browser/runts.ts +27 -3
  257. package/src/lib/browser/session-control.ts +892 -0
  258. package/src/lib/claude-desktop.ts +232 -1
  259. package/src/lib/docs-links.ts +674 -0
  260. package/src/lib/exec.ts +1 -1
  261. package/src/lib/identities/assume.ts +22 -1
  262. package/src/lib/instructions/bundle.ts +199 -0
  263. package/src/lib/instructions/git-hooks.ts +259 -0
  264. package/src/lib/instructions/splice.ts +41 -11
@@ -1,12 +1,16 @@
1
1
  import type { Command } from "commander";
2
- import type { EmitContext } from "../commander.ts";
2
+ import type { EmitContext, HarneryProgramContext } from "../commander.ts";
3
+ import { monorepoRoot } from "../core/agents/index.ts";
3
4
  import {
4
5
  applyMirror,
6
+ applyTidy,
5
7
  type DesktopAccount,
6
8
  findDesktopDataDirs,
7
9
  listAccounts,
8
10
  planMirror,
11
+ planTidy,
9
12
  readCliAccount,
13
+ readDesktopLifecycleIndex,
10
14
  } from "../lib/claude-desktop.ts";
11
15
 
12
16
  /**
@@ -24,12 +28,14 @@ import {
24
28
  * desktop app picks the copies up, and opening one resumes with full
25
29
  * history.
26
30
  */
27
- export function registerClaudeDesktopCommand(program: Command, emit: EmitContext): void {
31
+ export function registerClaudeDesktopCommand(
32
+ program: Command,
33
+ emit: EmitContext,
34
+ context?: HarneryProgramContext,
35
+ ): void {
28
36
  const cmd = program
29
37
  .command("claude-desktop")
30
- .description(
31
- "Claude desktop-app session index: list accounts/sessions, mirror sessions across accounts",
32
- )
38
+ .description("Claude desktop-app session index: list, mirror, and archive completed sessions")
33
39
  .action(() => {
34
40
  const dirs = requireDataDirs(emit, undefined);
35
41
  for (const dir of dirs) {
@@ -184,6 +190,101 @@ export function registerClaudeDesktopCommand(program: Command, emit: EmitContext
184
190
  }
185
191
  },
186
192
  );
193
+
194
+ cmd
195
+ .command("tidy")
196
+ .description(
197
+ "Archive desktop sidebar entries whose mapped Harnery lifecycle is done. " +
198
+ "Dry-run by default; --yes applies atomic entry updates.",
199
+ )
200
+ .option("--data-dir <path>", "Explicit desktop-app data directory")
201
+ .option(
202
+ "--account <uuid-prefix>",
203
+ "Only entries under accounts matching this UUID prefix (repeatable)",
204
+ collect,
205
+ [] as string[],
206
+ )
207
+ .option(
208
+ "--include-legacy-prefix",
209
+ "Also archive unmatched historical entries whose title starts with [DONE]",
210
+ )
211
+ .option("--yes", "Apply the archive plan (default is dry-run)")
212
+ .action(
213
+ async (opts: {
214
+ dataDir?: string;
215
+ account: string[];
216
+ includeLegacyPrefix?: boolean;
217
+ yes?: boolean;
218
+ }) => {
219
+ const dirs = requireDataDirs(emit, opts.dataDir);
220
+ const coordRoot =
221
+ context?.resolveCoordRoot?.() ?? context?.repoRoot ?? monorepoRoot() ?? process.cwd();
222
+ const lifecycle = await readDesktopLifecycleIndex(coordRoot);
223
+
224
+ for (const dir of dirs) {
225
+ const plan = planTidy(listAccounts(dir), lifecycle, {
226
+ accounts: opts.account,
227
+ includeLegacyPrefix: opts.includeLegacyPrefix,
228
+ });
229
+ const planned = plan.actions.map((action) => ({
230
+ account_uuid: action.entry.accountUuid,
231
+ cli_session_id: action.entry.cliSessionId,
232
+ title: action.entry.title,
233
+ source: action.source,
234
+ lifecycle_updated_at: action.lifecycle?.updatedAt ?? null,
235
+ file: action.entry.file,
236
+ }));
237
+ const skipped = plan.skipped
238
+ .map((skip) => ({
239
+ account_uuid: skip.entry.accountUuid,
240
+ cli_session_id: skip.entry.cliSessionId,
241
+ title: skip.entry.title,
242
+ reason: skip.reason,
243
+ lifecycle: skip.lifecycle?.state ?? null,
244
+ }))
245
+ .filter((skip) => skip.reason !== "already_archived");
246
+ const skippedByReason = Object.fromEntries(
247
+ [
248
+ "already_archived",
249
+ "blocked_title",
250
+ "lifecycle_active",
251
+ "lifecycle_blocked",
252
+ "unmatched",
253
+ ].map((reason) => [
254
+ reason,
255
+ plan.skipped.filter((skip) => skip.reason === reason).length,
256
+ ]),
257
+ );
258
+
259
+ if (!opts.yes) {
260
+ emit.data({
261
+ data_dir: dir,
262
+ dry_run: true,
263
+ planned,
264
+ skipped,
265
+ skipped_by_reason: skippedByReason,
266
+ hint: plan.actions.length > 0 ? "re-run with --yes to archive" : "nothing to archive",
267
+ });
268
+ continue;
269
+ }
270
+
271
+ const result = applyTidy(plan);
272
+ emit.data({
273
+ data_dir: dir,
274
+ dry_run: false,
275
+ archived: result.archived,
276
+ already_archived: result.alreadyArchived,
277
+ planned,
278
+ skipped,
279
+ skipped_by_reason: skippedByReason,
280
+ hint:
281
+ result.archived > 0
282
+ ? "fully quit the Claude desktop app (tray icon, not the X) and relaunch to refresh the sidebar"
283
+ : "nothing to archive",
284
+ });
285
+ }
286
+ },
287
+ );
187
288
  }
188
289
 
189
290
  function collect(value: string, previous: string[]): string[] {
@@ -34,8 +34,10 @@ import type { Command } from "commander";
34
34
  import type { EmitContext } from "../commander.ts";
35
35
  import { DEFAULT_BIN_NAME } from "../core/config.ts";
36
36
  import { ADAPTER_SPECS, type AdapterId } from "../core/hooks/adapter/events.ts";
37
+ import type { SettingsFile } from "../core/hooks/adapter/wiring.ts";
37
38
  import { removeInstructions } from "../lib/instructions/apply.ts";
38
- import { type SettingsFile, unwireHooks } from "./init.ts";
39
+ import { removeGitHooks } from "../lib/instructions/git-hooks.ts";
40
+ import { unwireHooks } from "./init.ts";
39
41
 
40
42
  interface DeinitOpts {
41
43
  adapter: string;
@@ -138,6 +140,10 @@ export function registerDeinitCommand(program: Command, emit: EmitContext, binNa
138
140
  const removed = removeInstructions(projectRoot, { adapter, dryRun });
139
141
  actions.push(...removed.actions, ...removed.warnings.map((w) => `! ${w}`));
140
142
 
143
+ // ── 1c. git-hook managed regions ───────────────────────────────────────
144
+ const gitHooks = removeGitHooks(projectRoot, { dryRun });
145
+ actions.push(...gitHooks.actions, ...gitHooks.warnings.map((w) => `! ${w}`));
146
+
141
147
  // ── 2. coord root (opt-in; destructive) ────────────────────────────────
142
148
  if (purgeState) {
143
149
  if (!coordExists) {
@@ -6,6 +6,7 @@ import {
6
6
  runFrontmatterMigration,
7
7
  } from "../lib/docs-frontmatter-migrate.ts";
8
8
  import { initDocsContext as initDocsIndex, runIndex } from "../lib/docs-index.ts";
9
+ import { initDocsContext as initDocsLinks, runLinks } from "../lib/docs-links.ts";
9
10
  import { initDocsContext as initDocsLint, runLint } from "../lib/docs-lint.ts";
10
11
  import { readDocsMetadata, readDocsMetadataKey } from "../lib/docs-meta.ts";
11
12
  import {
@@ -27,6 +28,7 @@ function ensureContext(context: HarneryProgramContext | undefined): void {
27
28
  extraExcludedPrefixes: context.extraDocsExcludedPrefixes,
28
29
  docsRootAllowlist: context.docsRootAllowlist,
29
30
  });
31
+ initDocsLinks({ ...opts, extraExcludedPrefixes: context.extraDocsExcludedPrefixes });
30
32
  initDocsSweep(opts);
31
33
  }
32
34
 
@@ -115,6 +117,34 @@ export function registerDocsCommand(
115
117
  }
116
118
  });
117
119
 
120
+ docs
121
+ .command("links")
122
+ .description("Check internal Markdown link targets and heading fragments")
123
+ .option("--repo <name>", "Limit to one submodule or '.' for parent")
124
+ .option("--no-fragments", "Check target existence only; skip heading-fragment validation")
125
+ .option("--strict", "Report findings in archive/audit/changelog docs as errors, not warnings")
126
+ .option("--check-escapes", "Also flag links that resolve outside their own repo root")
127
+ .option("--fail", "Exit non-zero when errors are found (advisory by default)")
128
+ .option("--format <type>", "Output format: human, json", "human")
129
+ .action(
130
+ async (opts: {
131
+ repo?: string;
132
+ fragments?: boolean;
133
+ strict?: boolean;
134
+ checkEscapes?: boolean;
135
+ fail?: boolean;
136
+ format: string;
137
+ }) => {
138
+ try {
139
+ ensureContext(context);
140
+ await handleLinks(opts);
141
+ } catch (err: unknown) {
142
+ const msg = err instanceof Error ? err.message : String(err);
143
+ emit.error({ code: "docs_error", message: msg });
144
+ }
145
+ },
146
+ );
147
+
118
148
  docs
119
149
  .command("sweep")
120
150
  .description(
@@ -232,6 +262,32 @@ async function handleLint(opts: { fast?: boolean; repo?: string; format: string
232
262
  if (errors.length > 0) emit.setExitCode(1);
233
263
  }
234
264
 
265
+ // --- `harn docs links` ---
266
+
267
+ /**
268
+ * Advisory by default: a clean exit code even with findings, because turning
269
+ * link health into a gate is an enforcement policy decision for the host, not
270
+ * something this command should assume. `--fail` is the opt-in for CI or a hook.
271
+ */
272
+ async function handleLinks(opts: {
273
+ repo?: string;
274
+ fragments?: boolean;
275
+ strict?: boolean;
276
+ checkEscapes?: boolean;
277
+ fail?: boolean;
278
+ format: string;
279
+ }): Promise<void> {
280
+ if (opts.format === "json") emit.config({ format: "json" });
281
+ const report = await runLinks({
282
+ repo: opts.repo,
283
+ noFragments: opts.fragments === false,
284
+ strict: opts.strict,
285
+ checkEscapes: opts.checkEscapes,
286
+ });
287
+ emit.data({ ...report, advisory: !opts.fail });
288
+ if (opts.fail && report.error_count > 0) emit.setExitCode(1);
289
+ }
290
+
235
291
  // --- `harn docs sweep` ---
236
292
 
237
293
  async function handleSweep(opts: { repo?: string; format: string }): Promise<void> {
@@ -19,7 +19,16 @@ import type { Command } from "commander";
19
19
  import type { EmitContext } from "../commander.ts";
20
20
  import { BUILTIN_ADAPTER_IDS } from "../core/adapters/index.ts";
21
21
  import { resolveBinName, ripgrepAutoInstall } from "../core/config.ts";
22
- import { loadAdapterWiring } from "../core/hooks/adapter/wiring.ts";
22
+ import { ADAPTER_SPECS, type AdapterId } from "../core/hooks/adapter/events.ts";
23
+ import { loadAdapterWiring, summarizeAdapterWiring } from "../core/hooks/adapter/wiring.ts";
24
+ import {
25
+ type CodexHookAuthorizationResult,
26
+ probeCodexHookAuthorization,
27
+ } from "../core/hooks/codex-authorization.ts";
28
+ import {
29
+ type CodexWslBridgeStatus,
30
+ inspectCodexWslBridge,
31
+ } from "../core/hooks/codex-wsl-bridge.ts";
23
32
  import {
24
33
  ADAPTER_BINARIES,
25
34
  ADAPTER_INSTALL_HINTS,
@@ -27,11 +36,12 @@ import {
27
36
  } from "../core/workflow/adapters.ts";
28
37
  import { probeBilling } from "../core/workflow/billing.ts";
29
38
  import type { AdapterName } from "../core/workflow/types.ts";
39
+ import { checkGitHooks, gitHooksInstalled } from "../lib/instructions/git-hooks.ts";
30
40
  import { findRg, installRg, managedRgPath, rgInstallSupported } from "../lib/tools/ripgrep.ts";
31
41
 
32
42
  type Severity = "ok" | "warn" | "fail";
33
43
 
34
- interface Check {
44
+ export interface Check {
35
45
  name: string;
36
46
  severity: Severity;
37
47
  detail: string;
@@ -63,7 +73,7 @@ export function registerDoctorCommand(program: Command, emit: EmitContext): void
63
73
  emit.text(`ripgrep install failed: ${(err as Error).message}\n`);
64
74
  }
65
75
  }
66
- const checks = runChecks();
76
+ const checks = await runChecks();
67
77
  const requiredFailed = checks.some((c) => c.severity === "fail");
68
78
 
69
79
  if (opts.json) {
@@ -96,15 +106,26 @@ export function registerDoctorCommand(program: Command, emit: EmitContext): void
96
106
  });
97
107
  }
98
108
 
99
- export function runChecks(): Check[] {
109
+ export async function runChecks(): Promise<Check[]> {
110
+ const codexWslStatus = inspectCodexWslBridge();
111
+ const codexWslBridge = checkCodexWslBridge(codexWslStatus);
112
+ // Probe the adapter CLIs before the hook check runs, not in output order:
113
+ // an unwired adapter only matters if its CLI is actually installed, and
114
+ // probing once here keeps that join to one spawn per CLI.
115
+ const workflow = BUILTIN_ADAPTER_IDS.map((id) => ({ id, ...checkWorkflowAdapter(id) }));
116
+ const installed = workflow.filter((w) => w.installed).map((w) => w.id as AdapterId);
117
+ const codexAuthorization = await checkCodexHookAuthorization(installed, codexWslStatus);
100
118
  return [
101
119
  checkNode(),
102
120
  checkGit(),
103
121
  checkBun(),
104
122
  checkRipgrep(),
105
123
  checkHarneryDir(),
106
- checkAdapterHooks(),
107
- ...BUILTIN_ADAPTER_IDS.map(checkWorkflowAdapter),
124
+ checkAdapterHooks(installed),
125
+ ...(codexAuthorization ? [codexAuthorization] : []),
126
+ checkGitHookRegions(),
127
+ ...(codexWslBridge ? [codexWslBridge] : []),
128
+ ...workflow.map((w) => w.check),
108
129
  checkRestic(),
109
130
  checkRclone(),
110
131
  checkPlaywright(),
@@ -112,6 +133,91 @@ export function runChecks(): Check[] {
112
133
  ];
113
134
  }
114
135
 
136
+ function checkGitHookRegions(): Check {
137
+ const root = findCoordProjectRoot();
138
+ if (!root) {
139
+ return { name: "git hooks", severity: "ok", detail: "n/a (no .harnery/ above cwd)" };
140
+ }
141
+ try {
142
+ if (!gitHooksInstalled(root)) {
143
+ const bin = resolveBinName(root);
144
+ return {
145
+ name: "git hooks",
146
+ severity: "warn",
147
+ detail:
148
+ `not installed - commits are not guarded against peer-claim conflicts and ` +
149
+ `claims never auto-release on commit/checkout; run \`${bin} init\` to install`,
150
+ };
151
+ }
152
+ const { status, issues } = checkGitHooks(root);
153
+ if (status === "fresh") {
154
+ return { name: "git hooks", severity: "ok", detail: "managed regions current" };
155
+ }
156
+ return { name: "git hooks", severity: "warn", detail: issues.join("; ") };
157
+ } catch (err) {
158
+ return {
159
+ name: "git hooks",
160
+ severity: "warn",
161
+ detail: `check failed: ${(err as Error).message}`,
162
+ };
163
+ }
164
+ }
165
+
166
+ function checkCodexWslBridge(status: CodexWslBridgeStatus | null): Check | null {
167
+ if (!status) return null;
168
+ return {
169
+ name: "codex:WSL bridge",
170
+ severity: status.ok ? "ok" : "warn",
171
+ detail: status.detail,
172
+ hint: status.ok
173
+ ? undefined
174
+ : "forward CODEX_THREAD_ID through WSLENV in the Codex shell environment policy, then start a fresh task",
175
+ };
176
+ }
177
+
178
+ export function codexAuthorizationCheck(result: CodexHookAuthorizationResult): Check {
179
+ const reviewHint =
180
+ "terminal UI: run `/hooks`; Codex Desktop: open Settings > Hooks; after approval, start a fresh task";
181
+ if (result.status === "runnable") {
182
+ return { name: "codex:hook authorization", severity: "ok", detail: result.detail };
183
+ }
184
+ if (result.status === "review_required" || result.status === "disabled") {
185
+ return {
186
+ name: "codex:hook authorization",
187
+ severity: "warn",
188
+ detail: result.detail,
189
+ hint: reviewHint,
190
+ };
191
+ }
192
+ return {
193
+ name: "codex:hook authorization",
194
+ severity: "warn",
195
+ detail: `authorization unverified: ${result.detail}`,
196
+ hint: reviewHint,
197
+ };
198
+ }
199
+
200
+ async function checkCodexHookAuthorization(
201
+ installedAdapters: AdapterId[],
202
+ codexWslStatus: CodexWslBridgeStatus | null,
203
+ ): Promise<Check | null> {
204
+ const root = findCoordProjectRoot();
205
+ if (!root || !summarizeAdapterWiring(root).wired.includes("codex")) return null;
206
+
207
+ if (codexWslStatus) {
208
+ return {
209
+ name: "codex:hook authorization",
210
+ severity: "warn",
211
+ detail:
212
+ "unverified from WSL; the Windows-native Codex runtime owns this workspace's trust state",
213
+ hint: "run the host's Windows bridge doctor, or open Codex Desktop Settings > Hooks; start a fresh task after approval",
214
+ };
215
+ }
216
+ if (!installedAdapters.includes("codex")) return null;
217
+
218
+ return codexAuthorizationCheck(await probeCodexHookAuthorization({ cwd: root }));
219
+ }
220
+
115
221
  function checkRipgrep(): Check {
116
222
  const found = findRg();
117
223
  if (found) {
@@ -157,26 +263,35 @@ function whichVersion(bin: string, args: string[] = ["--version"]): { ok: boolea
157
263
  * One workflow spawn target: is the adapter CLI installed, and how will its
158
264
  * headless children bill (subscription login vs API key — see billing.ts)?
159
265
  * Missing is a warn, not a fail: workflows degrade to the adapters you have.
266
+ *
267
+ * Reports `installed` alongside the check so `checkAdapterHooks` can reuse the
268
+ * probe instead of spawning every adapter CLI a second time.
160
269
  */
161
- function checkWorkflowAdapter(adapter: AdapterName): Check {
270
+ function checkWorkflowAdapter(adapter: AdapterName): { check: Check; installed: boolean } {
162
271
  const bin = ADAPTER_BINARIES[adapter];
163
272
  const name = `workflow:${adapter}`;
164
273
  const r = whichVersion(bin);
165
274
  if (!r.ok) {
166
275
  return {
167
- name,
168
- severity: "warn",
169
- detail: `${bin} missing (workflow --adapter ${adapter} unavailable)`,
170
- hint: `${ADAPTER_INSTALL_HINTS[adapter]} then: ${ADAPTER_LOGIN_HINTS[adapter]}`,
276
+ check: {
277
+ name,
278
+ severity: "warn",
279
+ detail: `${bin} missing (workflow --adapter ${adapter} unavailable)`,
280
+ hint: `${ADAPTER_INSTALL_HINTS[adapter]} then: ${ADAPTER_LOGIN_HINTS[adapter]}`,
281
+ },
282
+ installed: false,
171
283
  };
172
284
  }
173
285
  const probe = probeBilling(adapter);
174
286
  if (probe.login === "absent" && !probe.apiKeyPresent) {
175
287
  return {
176
- name,
177
- severity: "warn",
178
- detail: `${r.out} — installed, but no stored login or API key detected`,
179
- hint: ADAPTER_LOGIN_HINTS[adapter],
288
+ check: {
289
+ name,
290
+ severity: "warn",
291
+ detail: `${r.out} — installed, but no stored login or API key detected`,
292
+ hint: ADAPTER_LOGIN_HINTS[adapter],
293
+ },
294
+ installed: true,
180
295
  };
181
296
  }
182
297
  const billing =
@@ -185,7 +300,7 @@ function checkWorkflowAdapter(adapter: AdapterName): Check {
185
300
  ? "billing: subscription"
186
301
  : "billing: subscription (login unverifiable, CLI is the authority)"
187
302
  : `billing: ${probe.mode} (${probe.apiKeySource})`;
188
- return { name, severity: "ok", detail: `${r.out} — ${billing}` };
303
+ return { check: { name, severity: "ok", detail: `${r.out} — ${billing}` }, installed: true };
189
304
  }
190
305
 
191
306
  function checkNode(): Check {
@@ -331,13 +446,22 @@ function checkHarneryDir(): Check {
331
446
  * loadAdapterWiring — so a bare settings file never false-warns. The remedy is
332
447
  * always the same: re-run `<bin> init` (idempotent, additive).
333
448
  */
334
- function checkAdapterHooks(): Check {
449
+ function checkAdapterHooks(installedAdapters: AdapterId[] = []): Check {
335
450
  const root = findCoordProjectRoot();
336
451
  if (!root) {
337
452
  return { name: "adapter hooks", severity: "ok", detail: "n/a (no .harnery/ above cwd)" };
338
453
  }
339
454
  const drift = loadAdapterWiring(root);
340
- if (drift.length === 0) {
455
+ // A adapter with zero harnery hooks is only worth flagging when the project
456
+ // plainly uses harnery hooks (something else is wired) AND that adapter's CLI
457
+ // is installed. Both conditions matter: the first keeps a project that has
458
+ // never run init quiet, the second keeps a project that simply doesn't have
459
+ // Codex quiet. What's left is the case that reads as healthy but isn't — an
460
+ // agent can start a session through that adapter and register nothing.
461
+ const summary = summarizeAdapterWiring(root);
462
+ const unwired =
463
+ summary.wired.length > 0 ? summary.unwired.filter((id) => installedAdapters.includes(id)) : [];
464
+ if (drift.length === 0 && unwired.length === 0) {
341
465
  return { name: "adapter hooks", severity: "ok", detail: "wired + current" };
342
466
  }
343
467
  const bin = resolveBinName(root);
@@ -356,12 +480,22 @@ function checkAdapterHooks(): Check {
356
480
  }
357
481
  return `${d.settingsFile}: ${bits.join("; ")}`;
358
482
  });
483
+ for (const id of unwired) {
484
+ parts.push(`${ADAPTER_SPECS[id].settingsFile}: ${id} CLI installed, no harnery hooks wired`);
485
+ }
359
486
  const needsManualRepair = drift.some(
360
487
  (d) => d.parseError || d.invalidTopLevelKeys.length > 0 || d.invalidEventKeys.length > 0,
361
488
  );
362
- const hint = needsManualRepair
363
- ? `repair the invalid adapter settings, then run \`${bin} init\` to migrate harnery hooks`
364
- : `run \`${bin} init\` to migrate the hook set (idempotent)`;
489
+ const wireHints = unwired.map((id) => `\`${bin} init --adapter ${id}\``);
490
+ let hint: string;
491
+ if (needsManualRepair) {
492
+ hint = `repair the invalid adapter settings, then run \`${bin} init\` to migrate harnery hooks`;
493
+ } else if (drift.length > 0) {
494
+ hint = `run \`${bin} init\` to migrate the hook set (idempotent)`;
495
+ if (wireHints.length > 0) hint += `; ${wireHints.join(" and ")} to wire the rest`;
496
+ } else {
497
+ hint = `run ${wireHints.join(" and ")} (idempotent, additive)`;
498
+ }
365
499
  return { name: "adapter hooks", severity: "warn", detail: parts.join(" | "), hint };
366
500
  }
367
501
 
@@ -36,12 +36,9 @@ import {
36
36
  applyInstructions,
37
37
  checkInstructions,
38
38
  } from "../lib/instructions/apply.ts";
39
+ import { applyGitHooks, checkGitHooks } from "../lib/instructions/git-hooks.ts";
39
40
  import { HostAddendumError } from "../lib/instructions/host-addendum.ts";
40
41
 
41
- // Re-exported for back-compat: callers (deinit, tests) import these names
42
- // from init.ts. The definitions now live in core/hooks/adapter/wiring.ts.
43
- export type { HookGroup, SettingsFile };
44
-
45
42
  // This file is src/commands/init.ts → harnery package root is two levels up.
46
43
  const HARNERY_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
47
44
 
@@ -85,15 +82,19 @@ export function registerInitCommand(program: Command, emit: EmitContext, binName
85
82
  // ── --check: read-only drift report on the block + skills ──────────────
86
83
  if (opts.check === true) {
87
84
  const { status, issues } = checkInstructions(projectRoot, { binName: bin, adapter });
85
+ const gitHooks = checkGitHooks(projectRoot);
86
+ if (gitHooks.status !== "fresh") issues.push(...gitHooks.issues);
87
+ const merged =
88
+ status === "error" ? "error" : gitHooks.status !== "fresh" ? "drift" : status;
88
89
  const head =
89
- status === "fresh"
90
- ? "harn init --check: instructions + skills are current"
91
- : status === "drift"
90
+ merged === "fresh"
91
+ ? "harn init --check: instructions + skills + git hooks are current"
92
+ : merged === "drift"
92
93
  ? "harn init --check: drift found (re-run `init` to refresh)"
93
94
  : "harn init --check: error";
94
95
  const lines = issues.length ? `\n${issues.map((i) => ` ✗ ${i}`).join("\n")}` : "";
95
96
  emit.text(`${head}${lines}`);
96
- emit.setExitCode(status === "fresh" ? 0 : status === "drift" ? 2 : 1);
97
+ emit.setExitCode(merged === "fresh" ? 0 : merged === "drift" ? 2 : 1);
97
98
  return;
98
99
  }
99
100
 
@@ -174,6 +175,8 @@ export function registerInitCommand(program: Command, emit: EmitContext, binName
174
175
  `${removed} legacy hook(s) in ${rel(projectRoot, settingsPath)} (${already} already present)`,
175
176
  );
176
177
  }
178
+ const authorizationReview = codexHookReviewAction(adapter);
179
+ if (authorizationReview) actions.push(authorizationReview);
177
180
 
178
181
  // ── 3. agent-facing instructions block + skills ────────────────────────
179
182
  // A misconfigured host addendum aborts here rather than half-writing: the
@@ -190,10 +193,26 @@ export function registerInitCommand(program: Command, emit: EmitContext, binName
190
193
  }
191
194
  actions.push(...applied.actions);
192
195
 
193
- emit.text(render(projectRoot, dryRun, actions, applied.warnings));
196
+ // ── 4. git-hook managed regions ────────────────────────────────────────
197
+ // Same lifecycle contract as the instructions block: the coordination
198
+ // content of the consumer's git hooks is machine-owned and versioned;
199
+ // everything else in those files belongs to the host.
200
+ const gitHooks = applyGitHooks(projectRoot, { dryRun });
201
+ actions.push(...gitHooks.actions);
202
+
203
+ emit.text(render(projectRoot, dryRun, actions, [...applied.warnings, ...gitHooks.warnings]));
194
204
  });
195
205
  }
196
206
 
207
+ export function codexHookReviewAction(adapter: AdapterId): string | null {
208
+ if (adapter !== "codex") return null;
209
+ return (
210
+ "! Codex authorizes hook commands separately. Before starting work, review them in " +
211
+ "the terminal UI with `/hooks` or in Codex Desktop under Settings > Hooks. " +
212
+ "After approval, start a fresh task so SessionStart can run."
213
+ );
214
+ }
215
+
197
216
  /**
198
217
  * Merge agent-hook entries into a adapter settings object in place, idempotently.
199
218
  * Preserves every existing hook; rewrites events already wired to