harnery 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +16 -6
  2. package/dist/commander.d.ts +19 -0
  3. package/dist/commander.d.ts.map +1 -1
  4. package/dist/commander.js +2 -0
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +51 -4
  7. package/dist/commands/deinit.d.ts.map +1 -1
  8. package/dist/commands/deinit.js +4 -0
  9. package/dist/commands/devtools.d.ts +4 -0
  10. package/dist/commands/devtools.d.ts.map +1 -0
  11. package/dist/commands/devtools.js +239 -0
  12. package/dist/commands/docs.d.ts.map +1 -1
  13. package/dist/commands/docs.js +69 -1
  14. package/dist/commands/doctor.js +12 -4
  15. package/dist/commands/env.d.ts.map +1 -1
  16. package/dist/commands/env.js +3 -63
  17. package/dist/commands/init.d.ts +1 -0
  18. package/dist/commands/init.d.ts.map +1 -1
  19. package/dist/commands/init.js +54 -14
  20. package/dist/commands/scratch.js +1 -1
  21. package/dist/commands/tunnel.d.ts.map +1 -1
  22. package/dist/commands/tunnel.js +273 -62
  23. package/dist/commands/web-fetch.js +1 -1
  24. package/dist/core/agents/cli.js +48 -0
  25. package/dist/core/agents/coord-client.d.ts.map +1 -1
  26. package/dist/core/agents/coord-client.js +32 -8
  27. package/dist/core/agents/events/emit.d.ts.map +1 -1
  28. package/dist/core/agents/events/emit.js +4 -0
  29. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  30. package/dist/core/agents/rules/claim-conflict.js +16 -5
  31. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  32. package/dist/core/agents/state/heartbeat-projector.js +10 -3
  33. package/dist/core/config.d.ts +10 -0
  34. package/dist/core/config.d.ts.map +1 -1
  35. package/dist/core/config.js +13 -0
  36. package/dist/core/hooks/cli.js +3 -3
  37. package/dist/core/hooks/effects/index.d.ts +11 -7
  38. package/dist/core/hooks/effects/index.d.ts.map +1 -1
  39. package/dist/core/hooks/effects/index.js +15 -18
  40. package/dist/core/hooks/events/emit.d.ts.map +1 -1
  41. package/dist/core/hooks/events/emit.js +4 -0
  42. package/dist/core/hooks/events/rotate.d.ts +43 -0
  43. package/dist/core/hooks/events/rotate.d.ts.map +1 -0
  44. package/dist/core/hooks/events/rotate.js +142 -0
  45. package/dist/core/hooks/harness/events.d.ts +11 -1
  46. package/dist/core/hooks/harness/events.d.ts.map +1 -1
  47. package/dist/core/hooks/harness/events.js +22 -3
  48. package/dist/core/hooks/harness/wiring.d.ts +8 -0
  49. package/dist/core/hooks/harness/wiring.d.ts.map +1 -1
  50. package/dist/core/hooks/harness/wiring.js +34 -5
  51. package/dist/core/scratch/index.d.ts.map +1 -0
  52. package/dist/{lib → core}/scratch/index.js +2 -2
  53. package/dist/lib/devtools.d.ts +178 -0
  54. package/dist/lib/devtools.d.ts.map +1 -0
  55. package/dist/lib/devtools.js +1328 -0
  56. package/dist/lib/docs-frontmatter-migrate.d.ts +33 -0
  57. package/dist/lib/docs-frontmatter-migrate.d.ts.map +1 -0
  58. package/dist/lib/docs-frontmatter-migrate.js +364 -0
  59. package/dist/lib/docs-frontmatter.d.ts +33 -0
  60. package/dist/lib/docs-frontmatter.d.ts.map +1 -0
  61. package/dist/lib/docs-frontmatter.js +130 -0
  62. package/dist/lib/docs-index.d.ts +1 -0
  63. package/dist/lib/docs-index.d.ts.map +1 -1
  64. package/dist/lib/docs-index.js +4 -5
  65. package/dist/lib/docs-lint.d.ts +2 -0
  66. package/dist/lib/docs-lint.d.ts.map +1 -1
  67. package/dist/lib/docs-lint.js +18 -12
  68. package/dist/lib/docs-meta.d.ts +14 -0
  69. package/dist/lib/docs-meta.d.ts.map +1 -0
  70. package/dist/lib/docs-meta.js +34 -0
  71. package/dist/lib/docs-sweep.d.ts +12 -0
  72. package/dist/lib/docs-sweep.d.ts.map +1 -1
  73. package/dist/lib/docs-sweep.js +98 -103
  74. package/dist/lib/format.js +2 -2
  75. package/dist/lib/http/index.d.ts +1 -0
  76. package/dist/lib/http/index.d.ts.map +1 -1
  77. package/dist/lib/http/index.js +1 -0
  78. package/dist/lib/http/request.d.ts +77 -0
  79. package/dist/lib/http/request.d.ts.map +1 -0
  80. package/dist/lib/http/request.js +105 -0
  81. package/dist/lib/instructions/apply.d.ts +63 -0
  82. package/dist/lib/instructions/apply.d.ts.map +1 -0
  83. package/dist/lib/instructions/apply.js +255 -0
  84. package/dist/lib/instructions/splice.d.ts +73 -0
  85. package/dist/lib/instructions/splice.d.ts.map +1 -0
  86. package/dist/lib/instructions/splice.js +118 -0
  87. package/dist/lib/instructions/templates.d.ts +45 -0
  88. package/dist/lib/instructions/templates.d.ts.map +1 -0
  89. package/dist/lib/instructions/templates.js +258 -0
  90. package/dist/lib/tunnel/gate.d.ts +1 -0
  91. package/dist/lib/tunnel/gate.d.ts.map +1 -1
  92. package/dist/lib/tunnel/gate.js +14 -9
  93. package/dist/lib/tunnel/state.d.ts +11 -1
  94. package/dist/lib/tunnel/state.d.ts.map +1 -1
  95. package/dist/lib/tunnel/state.js +8 -3
  96. package/package.json +7 -6
  97. package/src/commander.ts +23 -0
  98. package/src/commands/agents.ts +50 -3
  99. package/src/commands/deinit.ts +5 -0
  100. package/src/commands/devtools.ts +284 -0
  101. package/src/commands/docs.ts +81 -1
  102. package/src/commands/doctor.ts +13 -4
  103. package/src/commands/env.ts +11 -77
  104. package/src/commands/init.ts +66 -15
  105. package/src/commands/scratch.ts +1 -1
  106. package/src/commands/tunnel.ts +316 -65
  107. package/src/commands/web-fetch.ts +1 -1
  108. package/src/core/agents/cli.ts +55 -0
  109. package/src/core/agents/coord-client.ts +34 -7
  110. package/src/core/agents/events/emit.ts +5 -0
  111. package/src/core/agents/rules/claim-conflict.ts +17 -6
  112. package/src/core/agents/state/heartbeat-projector.ts +11 -3
  113. package/src/core/config.ts +14 -0
  114. package/src/core/hooks/cli.ts +3 -3
  115. package/src/core/hooks/effects/index.ts +23 -17
  116. package/src/core/hooks/events/emit.ts +5 -0
  117. package/src/core/hooks/events/rotate.ts +151 -0
  118. package/src/core/hooks/harness/events.ts +30 -3
  119. package/src/core/hooks/harness/wiring.ts +46 -5
  120. package/src/{lib → core}/scratch/index.ts +2 -2
  121. package/src/lib/devtools.ts +1653 -0
  122. package/src/lib/docs-frontmatter-migrate.ts +427 -0
  123. package/src/lib/docs-frontmatter.ts +151 -0
  124. package/src/lib/docs-index.ts +4 -5
  125. package/src/lib/docs-lint.ts +17 -11
  126. package/src/lib/docs-meta.ts +44 -0
  127. package/src/lib/docs-sweep.ts +104 -102
  128. package/src/lib/format.ts +2 -2
  129. package/src/lib/http/index.ts +1 -0
  130. package/src/lib/http/request.ts +154 -0
  131. package/src/lib/instructions/apply.ts +318 -0
  132. package/src/lib/instructions/splice.ts +148 -0
  133. package/src/lib/instructions/templates.ts +295 -0
  134. package/src/lib/tunnel/gate.ts +14 -9
  135. package/src/lib/tunnel/state.ts +19 -4
  136. package/dist/lib/scratch/index.d.ts.map +0 -1
  137. /package/dist/{lib → core}/scratch/index.d.ts +0 -0
package/README.md CHANGED
@@ -7,23 +7,24 @@
7
7
 
8
8
  # Harnery
9
9
 
10
- > Multi-agent coordination + harness adapters + portable CLI utilities for Claude Code / Cursor / Codex.
10
+ > Multi-agent coordination for AI coding agents — Claude Code, Cursor, and Codex.
11
11
 
12
12
  [![CI](https://github.com/ryanjkelly/harnery/actions/workflows/ci.yml/badge.svg)](https://github.com/ryanjkelly/harnery/actions/workflows/ci.yml)
13
13
  [![npm version](https://img.shields.io/npm/v/harnery.svg)](https://www.npmjs.com/package/harnery)
14
14
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
15
15
 
16
- > ⚠️ **Pre-1.0.** API surface is still settling. Pin a specific minor version (`harnery@^0.1.0`) and read the [CHANGELOG](CHANGELOG.md) before each upgrade.
16
+ > ⚠️ **Pre-1.0.** API surface is still settling. Pin a specific minor version (`harnery@^0.7.0`) and read the [CHANGELOG](CHANGELOG.md) before each upgrade.
17
17
 
18
18
  ## What it is
19
19
 
20
- `harnery` is a utility layer extracted from years of building agent tooling across a multi-project monorepo. It bundles:
20
+ `harnery` keeps multiple AI coding agents from stepping on each other in a shared checkout. It was extracted from years of running several Claude Code / Cursor / Codex sessions at once against the same monorepo:
21
21
 
22
- - **Multi-agent coordination:** per-agent heartbeats in `.harnery/active/`, claim-time and commit-time guards, the canonical event stream, harness adapters for Claude Code / Cursor / Codex.
23
- - **Portable CLI utilities:** `tokens`, `eml`, `env`, `grep`, `docs`, `repo`, `wip`, `share`, `browse`, `fetch`, `read`, and more. Cross-platform and dependency-light, with sensible defaults out of the box.
24
- - **Standalone web UI:** `harn web up` boots a local Next.js dashboard for the coord layer, councils, and per-project state. Ships with the git clone, not the npm package (see [Install](#install)).
22
+ - **Multi-agent coordination:** per-agent heartbeats in `.harnery/active/`, claim-time and commit-time guards, the canonical event stream, councils, a decision docket, per-agent scratchpads, and harness adapters for Claude Code / Cursor / Codex.
23
+ - **Standalone web UI:** `harn web up` boots a local Next.js dashboard for the coord layer, councils, per-project state, and a local coding-agent status view (Claude Code / Codex / Cursor usage, backed by `harn devtools`). Ships with the git clone, not the npm package (see [Install](#install)).
25
24
  - **Backup + sync:** `harn backup` snapshots `.harnery/` via [restic](https://restic.net/); `harn sync` keeps a curated subset live across machines via [rclone](https://rclone.org/) (Google Drive or any rclone remote).
26
25
 
26
+ The CLI also ships batteries: portable utility commands (`tokens`, `eml`, `env`, `grep`, `docs`, `browse`, `fetch`, `read`, `devtools`, and more) plus the library toolkit they're built from. Useful, cross-platform, dependency-light — and deliberately not the headline. Coordination is why harnery exists; the toolkit exists because the CLI needed it (see [Public surface tiers](#public-surface-tiers)).
27
+
27
28
  ## Install
28
29
 
29
30
  ```bash
@@ -80,6 +81,15 @@ await program.parseAsync(process.argv);
80
81
 
81
82
  `mycli agents status` then resolves to **the same code** as `harn agents status`, loaded as a library. See [examples/extending-with-commander.ts](examples/extending-with-commander.ts) for the full pattern.
82
83
 
84
+ ### Public surface tiers
85
+
86
+ The exports map draws the line between what harnery *is* and what it *ships with*:
87
+
88
+ - **Product tier** — `harnery`, `harnery/commander`, `harnery/core/*`: the coordination layer and CLI composition. This is the API to build against, and the reason to install harnery.
89
+ - **Toolkit tier** — every `harnery/lib/*` subpath (`http`, `cookies`, `format`, `readability`, `browser`, `machine`, …): the supporting utilities harnery's own CLI is built from, exposed for embedding hosts that want to lean on them. Supported, but secondary: it can evolve faster than the product tier, and it isn't a reason to adopt harnery on its own.
90
+
91
+ The boundary is enforced, not aspirational: CI verifies that no `harnery/lib/*` export imports the coordination core, directly or transitively (`scripts/check-layering.ts`), so pulling a toolkit module never drags in coordination state. Details: [Embedding + surface tiers](https://harnery.com/concepts/embedding/).
92
+
83
93
  ## Documentation
84
94
 
85
95
  Full docs at **[harnery.com](https://harnery.com)**:
@@ -42,6 +42,18 @@ export interface HarneryContextOpts {
42
42
  */
43
43
  skipCommands?: readonly string[];
44
44
  }
45
+ /** One row in a `harn env` section report. */
46
+ export interface EnvCheck {
47
+ label: string;
48
+ value: string;
49
+ status?: "ok" | "missing" | "warn" | "info";
50
+ }
51
+ /**
52
+ * A `harn env` section: an async probe returning its rows. Hosts register extra
53
+ * sections (e.g. cloud-provider connectivity) via `context.envSections`; harnery
54
+ * core ships only the generic sections (runtimes, docker, git).
55
+ */
56
+ export type EnvSection = () => Promise<EnvCheck[]>;
45
57
  export interface HarneryProgramContext {
46
58
  /** Project name (e.g., "my-monorepo"). Used in user-facing log lines + telemetry tags. */
47
59
  projectName?: string;
@@ -59,6 +71,13 @@ export interface HarneryProgramContext {
59
71
  * submodule row.
60
72
  */
61
73
  submodules?: readonly string[];
74
+ /**
75
+ * Extra `harn env` sections keyed by name (e.g. `{ gcp, bq }`). Merged into
76
+ * the built-in generic sections (runtimes, docker, git), so a host can expose
77
+ * `harn env <name>` for its own environment probes without harnery core
78
+ * carrying provider-specific checks. harn standalone ships none.
79
+ */
80
+ envSections?: Record<string, EnvSection>;
62
81
  /**
63
82
  * Optional callback that returns extra HTTP headers to attach to outbound
64
83
  * `fetch` calls based on the target URL. Useful for consumers that need
@@ -1 +1 @@
1
- {"version":3,"file":"commander.d.ts","sourceRoot":"","sources":["../src/commander.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgCpC,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAEhC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,WAAW,CAAC;IAEnB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,qBAAqB;IACpC,0FAA0F;IAC1F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvD;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE;QACvB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,KAAK,MAAM,GAAG,SAAS,CAAC;IACzB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACvE;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACjE;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,IAAI,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACxC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7B,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC;IAC5C,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC3D,KAAK,CAAC,GAAG,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,GAAG,OAAO,GAAG,IAAI,CAAC;IACrF,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IACpE,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,eAAO,MAAM,WAAW,EAAE,WAkCzB,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,IAAI,GAAE,kBAAuB,GAAG,OAAO,CA4C3E"}
1
+ {"version":3,"file":"commander.d.ts","sourceRoot":"","sources":["../src/commander.ts"],"names":[],"mappings":"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"}
package/dist/commander.js CHANGED
@@ -28,6 +28,7 @@ import { registerContextCommand } from "./commands/context.js";
28
28
  import { registerCookiesCommand } from "./commands/cookies.js";
29
29
  import { registerDecisionCommand } from "./commands/decision.js";
30
30
  import { registerDeinitCommand } from "./commands/deinit.js";
31
+ import { registerDevtoolsCommand } from "./commands/devtools.js";
31
32
  import { registerDocsCommand } from "./commands/docs.js";
32
33
  import { registerDoctorCommand } from "./commands/doctor.js";
33
34
  import { registerEditBatchCommand } from "./commands/edit-batch.js";
@@ -112,6 +113,7 @@ export function createHarneryProgram(opts = {}) {
112
113
  registerContextCommand(program, emit, opts.context);
113
114
  registerScratchCommand(program, emit);
114
115
  registerDecisionCommand(program, emit);
116
+ registerDevtoolsCommand(program, emit);
115
117
  registerTunnelCommand(program, emit, opts.context);
116
118
  registerDocsCommand(program, emit, opts.context);
117
119
  registerAgentsCommand(program, emit);
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/commands/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAeH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAuFnD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,GAAG,IAAI,CA6OpF"}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/commands/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAeH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAwFnD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,GAAG,IAAI,CA4OpF"}
@@ -17,16 +17,16 @@ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync,
17
17
  import { homedir, tmpdir } from "node:os";
18
18
  import { basename, join, resolve } from "node:path";
19
19
  import { readStreamTailBounded } from "../core/agents/events/consume.js";
20
- import { emitCanonical, monorepoRoot, normalizeHarness, readHeartbeat, resolveOwner, resolveOwnerWithSource, } from "../core/agents/index.js";
20
+ import { emitCanonical, monorepoRoot, normalizeHarness, readHeartbeat, resolveOwner, resolveOwnerBySessionEnv, resolveOwnerWithSource, } from "../core/agents/index.js";
21
21
  import { resolveBinName } from "../core/config.js";
22
22
  /** Cap for CLI scans of the unbounded event ledger (`trace` / `health`). Well
23
23
  * under V8's ~512MB max string length so a `readFileSync` of the whole file can
24
24
  * never throw; covers ample recent history for a diagnostic scan. */
25
25
  const STREAM_SCAN_CAP_BYTES = 128 * 1024 * 1024; // 128 MiB
26
26
  import { parsePsChainLine } from "../core/hooks/resolve/anchor.js";
27
+ import { appendEntry, resolveOwnerByName } from "../core/scratch/index.js";
27
28
  import { buildCouncilId, buildInviteMarkdown, COUNCIL_SCHEMA_VERSION, contributorsInRound, councilBodyDir, councilsArchiveDir, deleteArchivedCouncil, effectiveSteward, findManifestByPartialId, listKnownAgents, listManifests, moveFromArchive, moveToArchive, normalizeAgentName, pendingCouncilsForMember, readArchivedManifest, readManifest, readRoundPrompts, roundDir, setCouncilSteward, writeContribution, writeManifest, writePrompt, } from "../lib/council/index.js";
28
29
  import { displayName as displayAgentName, ensureIdentity, listIdentities, lookupById as lookupIdentityById, lookupByName as lookupIdentityByName, } from "../lib/identities/index.js";
29
- import { appendEntry, resolveOwnerByName } from "../lib/scratch/index.js";
30
30
  const FRESHNESS_SECS = 600; // 10-minute heartbeat-freshness window.
31
31
  const SUBAGENT_NOTE = "Bash identity is process-level in v1; if you're running inside a subagent, " +
32
32
  "this resolves to the parent group's name, not the subagent's. A subagent-aware " +
@@ -80,8 +80,7 @@ export function registerAgentsCommand(program, emitParam) {
80
80
  });
81
81
  cmd
82
82
  .command("show <name>")
83
- .description("Deep-dive on one peer agent: registry state (files held, last tool, task) " +
84
- "plus claude-sessions history (latest title, recent prompts, recent tools, tool-usage tallies). " +
83
+ .description("Deep-dive on one peer agent: registry state (files held, last tool, task, turn summary). " +
85
84
  "Disambiguates name → instance_id via prefix match.")
86
85
  .option("--json", "JSON envelope output")
87
86
  .action(async (name, opts) => {
@@ -435,6 +434,7 @@ function runWhoami(opts) {
435
434
  });
436
435
  process.exit(1);
437
436
  }
437
+ ensureCursorSession(root);
438
438
  const resolved = resolveOwnerWithSource();
439
439
  const myOwner = resolved.owner;
440
440
  if (!myOwner) {
@@ -873,6 +873,49 @@ function formatLocalShort(iso) {
873
873
  timeZone: "America/Chicago",
874
874
  }).format(d);
875
875
  }
876
+ function cursorEnvSessionId() {
877
+ const raw = process.env.CURSOR_SESSION_ID?.trim() || process.env.CURSOR_CONVERSATION_ID?.trim();
878
+ if (!raw)
879
+ return null;
880
+ return raw.startsWith("bc-") && raw.length > 3 ? raw.slice(3) : raw;
881
+ }
882
+ function shouldBootstrapCursorSession() {
883
+ return process.env.CURSOR_AGENT === "1" && cursorEnvSessionId() !== null;
884
+ }
885
+ function ensureCursorSession(root) {
886
+ if (!shouldBootstrapCursorSession())
887
+ return;
888
+ if (resolveOwnerBySessionEnv(root))
889
+ return;
890
+ const sessionId = cursorEnvSessionId();
891
+ if (!sessionId)
892
+ return;
893
+ const agentHook = resolve(root, "harnery", "bin", "agent-hook");
894
+ if (!existsSync(agentHook))
895
+ return;
896
+ const payload = JSON.stringify({
897
+ conversation_id: sessionId,
898
+ session_id: sessionId,
899
+ hook_event_name: "sessionStart",
900
+ workspace_roots: [root],
901
+ cwd: root,
902
+ composer_mode: "agent",
903
+ is_background_agent: false,
904
+ });
905
+ spawnSync("bash", [agentHook, "session-start", "--harness", "cursor"], {
906
+ input: payload,
907
+ cwd: root,
908
+ encoding: "utf8",
909
+ timeout: 3000,
910
+ env: {
911
+ ...process.env,
912
+ HARNERY_AGENT_COORD_PLATFORM: "cursor",
913
+ HARNERY_COORD_ROOT_OVERRIDE: root,
914
+ CURSOR_SESSION_ID: sessionId,
915
+ CURSOR_CONVERSATION_ID: sessionId,
916
+ },
917
+ });
918
+ }
876
919
  function runReleaseClaim(path) {
877
920
  const root = monorepoRoot();
878
921
  if (!root) {
@@ -920,6 +963,8 @@ function runSetTask(task, opts) {
920
963
  // Identity: prefer explicit --session-id (the ppid-walk-free escape hatch,
921
964
  // mirrors `status`), fall back to the ppid walk. Cursor shell tool calls
922
965
  // don't descend from a pid-map-registered anchor, so the walk can miss there.
966
+ if (!opts?.sessionId)
967
+ ensureCursorSession(root);
923
968
  const myOwner = opts?.sessionId ?? resolveOwner();
924
969
  if (!myOwner) {
925
970
  emit.error({
@@ -966,6 +1011,8 @@ function runStatus(opts) {
966
1011
  }
967
1012
  // Identity resolution: prefer explicit --session-id (hook-friendly), fall
968
1013
  // back to ppid walk for interactive shell usage.
1014
+ if (!opts.sessionId)
1015
+ ensureCursorSession(root);
969
1016
  const myOwner = opts.sessionId ?? resolveOwner();
970
1017
  if (!myOwner) {
971
1018
  emit.error({
@@ -1 +1 @@
1
- {"version":3,"file":"deinit.d.ts","sourceRoot":"","sources":["../../src/commands/deinit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAYnD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CA2GjG;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE;IACtC,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;CACtB,GAAG,OAAO,CAEV;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAM1C"}
1
+ {"version":3,"file":"deinit.d.ts","sourceRoot":"","sources":["../../src/commands/deinit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAanD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CA+GjG;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE;IACtC,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;CACtB,GAAG,OAAO,CAEV;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAM1C"}
@@ -31,6 +31,7 @@ import { relative, resolve } from "node:path";
31
31
  import { createInterface } from "node:readline";
32
32
  import { DEFAULT_BIN_NAME } from "../core/config.js";
33
33
  import { HARNESS_SPECS } from "../core/hooks/harness/events.js";
34
+ import { removeInstructions } from "../lib/instructions/apply.js";
34
35
  import { unwireHooks } from "./init.js";
35
36
  export function registerDeinitCommand(program, emit, binName) {
36
37
  // The interactive prompt + engine-removal hint are standalone-harn niceties.
@@ -113,6 +114,9 @@ export function registerDeinitCommand(program, emit, binName) {
113
114
  `(${remaining} other hook(s) kept)`);
114
115
  }
115
116
  }
117
+ // ── 1b. agent-facing instructions block + skills ───────────────────────
118
+ const removed = removeInstructions(projectRoot, { harness, dryRun });
119
+ actions.push(...removed.actions, ...removed.warnings.map((w) => `! ${w}`));
116
120
  // ── 2. coord root (opt-in; destructive) ────────────────────────────────
117
121
  if (purgeState) {
118
122
  if (!coordExists) {
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ import type { EmitContext } from "../commander.js";
3
+ export declare function registerDevtoolsCommand(program: Command, emit: EmitContext): void;
4
+ //# sourceMappingURL=devtools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devtools.d.ts","sourceRoot":"","sources":["../../src/commands/devtools.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAkCnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAsDjF"}
@@ -0,0 +1,239 @@
1
+ import { chmodSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { cursorApiKeyPath, enrichFromApi, probeEndpoints, readDevtools, resolveCursorApiKey, } from "../lib/devtools.js";
4
+ /**
5
+ * `devtools`: report the local state of the AI coding agents harnery supports
6
+ * — Claude Code, Codex, and Cursor — in one place: logged-in status, plan/seat
7
+ * tier, auth expiry, session counts, and (where the tool keeps them locally)
8
+ * rate-limit / quota windows.
9
+ *
10
+ * Reads files on disk by default (no network). When a Cursor API key is
11
+ * configured (`devtools cursor-key set`), it additionally verifies the key and
12
+ * pulls Cloud Agent activity. `--usage` adds an opt-in windowed scan of local
13
+ * transcripts for approximate token totals.
14
+ */
15
+ const VALID = ["claude-code", "codex", "cursor"];
16
+ export function registerDevtoolsCommand(program, emit) {
17
+ const cmd = program
18
+ .command("devtools")
19
+ .description("Local status of the AI coding agents (Claude Code, Codex, Cursor)")
20
+ .option("--tool <name>", "Restrict to a tool (repeatable): claude-code | codex | cursor", collect, [])
21
+ .option("--usage", "Also scan local transcripts for approximate token totals (slower)")
22
+ .option("--window-days <n>", "With --usage: only count transcripts modified within N days", (v) => Number.parseInt(v, 10), 7)
23
+ .option("--no-api", "Skip the Cursor API enrichment even when a key is configured")
24
+ .option("--format <type>", "Output format: table, json", "table")
25
+ .action(async (opts) => {
26
+ const only = (opts.tool ?? []).filter((t) => VALID.includes(t));
27
+ const bad = (opts.tool ?? []).filter((t) => !VALID.includes(t));
28
+ if (bad.length) {
29
+ emit.error({
30
+ code: "bad_tool",
31
+ message: `unknown --tool: ${bad.join(", ")}`,
32
+ hint: `valid: ${VALID.join(", ")}`,
33
+ });
34
+ return;
35
+ }
36
+ const report = readDevtools({
37
+ usage: opts.usage,
38
+ windowDays: opts.windowDays,
39
+ only: only.length ? only : undefined,
40
+ });
41
+ // Auto-enrich when a Cursor key is configured (the user opted in by
42
+ // storing one); --no-api forces pure-local.
43
+ if (opts.noApi !== true)
44
+ await enrichFromApi(report);
45
+ if (opts.format === "json") {
46
+ emit.config({ format: "json" });
47
+ emit.data({ ok: true, ...report });
48
+ return;
49
+ }
50
+ emit.text(renderTable(report.tools));
51
+ });
52
+ registerCursorKeyCommand(cmd, emit);
53
+ registerDoctorCommand(cmd, emit);
54
+ }
55
+ /**
56
+ * `devtools doctor` — one live call per usage endpoint to check the integrations
57
+ * still work. Surfaces `auth_rejected` (our headers/token stopped being
58
+ * accepted) and `shape_changed` (response schema drifted) distinctly from a
59
+ * mere rate limit. Bypasses the cache; run occasionally, by hand.
60
+ */
61
+ function registerDoctorCommand(parent, emit) {
62
+ parent
63
+ .command("doctor")
64
+ .description("Probe the usage endpoints once each to detect header/schema drift")
65
+ .option("--tool <name>", "Restrict to a tool (repeatable): claude-code | cursor", collect, [])
66
+ .option("--format <type>", "Output format: table, json", "table")
67
+ .action(async (opts) => {
68
+ const only = (opts.tool ?? []).filter((t) => VALID.includes(t));
69
+ const results = await probeEndpoints({ only: only.length ? only : undefined });
70
+ if (opts.format === "json") {
71
+ emit.config({ format: "json" });
72
+ emit.data({
73
+ ok: results.every((r) => r.outcome === "ok" || r.outcome === "rate_limited"),
74
+ results,
75
+ });
76
+ return;
77
+ }
78
+ emit.text(renderDoctor(results));
79
+ });
80
+ }
81
+ const DOCTOR_MARK = {
82
+ ok: "✓",
83
+ rate_limited: "~",
84
+ no_credential: "-",
85
+ auth_rejected: "✗",
86
+ shape_changed: "✗",
87
+ unreachable: "✗",
88
+ };
89
+ function renderDoctor(results) {
90
+ if (!results.length)
91
+ return "no probeable endpoints (Codex is local-only)";
92
+ const lines = [];
93
+ for (const r of results) {
94
+ lines.push(`${DOCTOR_MARK[r.outcome]} ${r.tool.padEnd(12)} ${r.outcome.padEnd(14)} ${r.detail}`);
95
+ lines.push(` ${r.endpoint}${r.clientVersion ? ` (client ${r.clientVersion})` : ""}`);
96
+ }
97
+ lines.push("");
98
+ lines.push("✗ auth_rejected / shape_changed = the client's request contract likely changed — review the headers/parser.");
99
+ return lines.join("\n");
100
+ }
101
+ /** `devtools cursor-key set|clear|status` — store the machine-local Cursor API key. */
102
+ function registerCursorKeyCommand(parent, emit) {
103
+ const key = parent
104
+ .command("cursor-key")
105
+ .description("Manage the Cursor API key used for the Cloud Agent enrichment");
106
+ key
107
+ .command("set [value]")
108
+ .description("Store a Cursor API key (reads stdin when [value] is omitted)")
109
+ .action(async (value) => {
110
+ const raw = value ?? (await readStdin());
111
+ const trimmed = raw.trim();
112
+ if (!trimmed) {
113
+ emit.error({ code: "empty_key", message: "no key provided (arg or stdin)" });
114
+ return;
115
+ }
116
+ const path = cursorApiKeyPath();
117
+ mkdirSync(dirname(path), { recursive: true });
118
+ writeFileSync(path, trimmed, { mode: 0o600 });
119
+ chmodSync(path, 0o600);
120
+ emit.file(path, { stored: true, length: trimmed.length });
121
+ });
122
+ key
123
+ .command("clear")
124
+ .description("Remove the stored Cursor API key")
125
+ .action(() => {
126
+ try {
127
+ rmSync(cursorApiKeyPath());
128
+ }
129
+ catch {
130
+ // already absent
131
+ }
132
+ emit.data({ ok: true, cleared: true });
133
+ });
134
+ key
135
+ .command("status")
136
+ .description("Report whether a Cursor key is configured (env or file)")
137
+ .action(() => {
138
+ const fromEnv = Boolean(process.env.CURSOR_API_KEY?.trim());
139
+ const resolved = resolveCursorApiKey();
140
+ emit.data({
141
+ ok: true,
142
+ configured: Boolean(resolved),
143
+ source: fromEnv ? "env" : resolved ? "file" : null,
144
+ path: cursorApiKeyPath(),
145
+ });
146
+ });
147
+ }
148
+ function readStdin() {
149
+ return new Promise((resolve) => {
150
+ let data = "";
151
+ if (process.stdin.isTTY) {
152
+ resolve("");
153
+ return;
154
+ }
155
+ process.stdin.setEncoding("utf8");
156
+ process.stdin.on("data", (c) => {
157
+ data += c;
158
+ });
159
+ process.stdin.on("end", () => resolve(data));
160
+ });
161
+ }
162
+ function collect(value, prev) {
163
+ return [...prev, value];
164
+ }
165
+ function renderTable(tools) {
166
+ const lines = [];
167
+ for (const t of tools) {
168
+ lines.push(`── ${t.tool} ${"─".repeat(Math.max(0, 40 - t.tool.length))}`);
169
+ if (!t.installed) {
170
+ lines.push(" not installed");
171
+ lines.push("");
172
+ continue;
173
+ }
174
+ lines.push(` logged in ${fmtBool(t.loggedIn)}`);
175
+ if (t.account)
176
+ lines.push(` account ${t.account}`);
177
+ if (t.plan)
178
+ lines.push(` plan ${t.plan}`);
179
+ if (t.rateLimitTier)
180
+ lines.push(` rate tier ${t.rateLimitTier}`);
181
+ if (t.authExpiresAt)
182
+ lines.push(` auth expires ${fmtDate(t.authExpiresAt)}`);
183
+ if (t.sessions != null)
184
+ lines.push(` sessions ${t.sessions}`);
185
+ if (t.lastActivity)
186
+ lines.push(` last active ${fmtDate(t.lastActivity)}`);
187
+ if (t.quota?.length) {
188
+ for (const q of t.quota) {
189
+ const pct = q.usedPercent != null ? `${q.usedPercent}% used` : "usage unknown";
190
+ const reset = q.resetsAt ? `resets ${fmtDate(q.resetsAt)}` : "";
191
+ lines.push(` quota (${q.window}) ${pct}${reset ? ` · ${reset}` : ""}`);
192
+ }
193
+ }
194
+ if (t.tokensUsed != null) {
195
+ lines.push(` tokens ${t.tokensUsed.toLocaleString()}`);
196
+ }
197
+ if (t.usage) {
198
+ const u = t.usage;
199
+ if (u.cycleEnd) {
200
+ const days = Math.round((Date.parse(u.cycleEnd) - Date.now()) / 86_400_000);
201
+ lines.push(` plan resets ${fmtDate(u.cycleEnd)} (${days}d)`);
202
+ }
203
+ const pct = (label, v) => (v != null ? `${label} ${v}%` : null);
204
+ const bars = [
205
+ pct("total", u.totalPercentUsed),
206
+ pct("api", u.apiPercentUsed),
207
+ pct("first-party", u.firstPartyPercentUsed),
208
+ ].filter((s) => s != null);
209
+ if (bars.length)
210
+ lines.push(` usage ${bars.join(" · ")}`);
211
+ }
212
+ if (t.spend?.limitCents != null) {
213
+ const label = t.spend.label.toLowerCase().padEnd(12).slice(0, 12);
214
+ lines.push(` ${label} ${fmtUsd(t.spend.usedCents ?? 0)} / ${fmtUsd(t.spend.limitCents)}`);
215
+ }
216
+ if (t.api?.ok && t.api.cloudAgents) {
217
+ lines.push(` cloud agents ${t.api.cloudAgents.total} (${t.api.cloudAgents.active} active)`);
218
+ }
219
+ for (const n of t.notes)
220
+ lines.push(` · ${n}`);
221
+ lines.push("");
222
+ }
223
+ return lines.join("\n").trimEnd();
224
+ }
225
+ function fmtBool(v) {
226
+ if (v === true)
227
+ return "yes";
228
+ if (v === false)
229
+ return "no";
230
+ return "unknown";
231
+ }
232
+ function fmtDate(iso) {
233
+ // Keep the machine-readable ISO but drop milliseconds for readability.
234
+ return iso.replace(/\.\d{3}Z$/, "Z");
235
+ }
236
+ /** Cents → "$X.XX". */
237
+ function fmtUsd(cents) {
238
+ return `$${(cents / 100).toFixed(2)}`;
239
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/commands/docs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AA2B1E,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,WAAW,EACtB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CA+EN"}
1
+ {"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/commands/docs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAiC1E,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,WAAW,EACtB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CA8GN"}
@@ -1,6 +1,8 @@
1
1
  import { initDocsContext as initDocs, scanDocs } from "../lib/docs.js";
2
+ import { initDocsMigrationContext, runFrontmatterMigration, } from "../lib/docs-frontmatter-migrate.js";
2
3
  import { initDocsContext as initDocsIndex, runIndex } from "../lib/docs-index.js";
3
4
  import { initDocsContext as initDocsLint, runLint } from "../lib/docs-lint.js";
5
+ import { readDocsMetadata, readDocsMetadataKey } from "../lib/docs-meta.js";
4
6
  import { countColdHandoffs, initDocsContext as initDocsSweep, runSweep, } from "../lib/docs-sweep.js";
5
7
  function ensureContext(context) {
6
8
  if (!context?.repoRoot || !context?.submodules) {
@@ -8,6 +10,7 @@ function ensureContext(context) {
8
10
  }
9
11
  const opts = { repoRoot: context.repoRoot, submodules: context.submodules };
10
12
  initDocs(opts);
13
+ initDocsMigrationContext(opts);
11
14
  initDocsIndex(opts);
12
15
  initDocsLint({
13
16
  ...opts,
@@ -21,7 +24,7 @@ export function registerDocsCommand(program, emitParam, context) {
21
24
  emit = emitParam;
22
25
  const docs = program
23
26
  .command("docs")
24
- .description("Documentation tooling: freshness report, lint, sweep, index")
27
+ .description("Documentation tooling: freshness report, metadata, lint, sweep, index")
25
28
  // Options on the group itself back the default (no-subcommand) behavior.
26
29
  // See handleDocs below.
27
30
  .option("--stale <days>", "Only show files not committed in N+ days", Number.parseInt)
@@ -39,6 +42,37 @@ export function registerDocsCommand(program, emitParam, context) {
39
42
  emit.error({ code: "docs_error", message: msg });
40
43
  }
41
44
  });
45
+ docs
46
+ .command("meta")
47
+ .description("Read YAML frontmatter from a documentation file")
48
+ .argument("<path>", "Markdown file path, relative to the project root or absolute")
49
+ .argument("[key]", "Optional top-level frontmatter key")
50
+ .option("--json", "Emit a requested key as JSON even in an interactive terminal")
51
+ .action(async (path, key, opts) => {
52
+ try {
53
+ ensureContext(context);
54
+ handleMeta(context.repoRoot, path, key, opts);
55
+ }
56
+ catch (err) {
57
+ const msg = err instanceof Error ? err.message : String(err);
58
+ emit.error({ code: "docs_error", message: msg });
59
+ }
60
+ });
61
+ docs
62
+ .command("frontmatter-migrate")
63
+ .description("Convert lifecycle docs from bold metadata headers to YAML frontmatter")
64
+ .option("--repo <name>", "Limit to one submodule or '.' for parent")
65
+ .option("--yes", "Apply changes; without this flag the command is a dry-run")
66
+ .action(async (opts) => {
67
+ try {
68
+ ensureContext(context);
69
+ handleFrontmatterMigration(opts);
70
+ }
71
+ catch (err) {
72
+ const msg = err instanceof Error ? err.message : String(err);
73
+ emit.error({ code: "docs_error", message: msg });
74
+ }
75
+ });
42
76
  docs
43
77
  .command("lint")
44
78
  .description("Verify every repo matches the documentation contract (directory layout + naming rules)")
@@ -86,6 +120,40 @@ export function registerDocsCommand(program, emitParam, context) {
86
120
  }
87
121
  });
88
122
  }
123
+ // --- `harn docs frontmatter-migrate` ---
124
+ function handleFrontmatterMigration(opts) {
125
+ const rows = runFrontmatterMigration({ repo: opts.repo, apply: !!opts.yes });
126
+ const counts = {
127
+ would_update: rows.filter((row) => row.status === "would-update").length,
128
+ updated: rows.filter((row) => row.status === "updated").length,
129
+ skipped: rows.filter((row) => row.status === "skipped").length,
130
+ errors: rows.filter((row) => row.status === "error").length,
131
+ };
132
+ emit.data({
133
+ dry_run: !opts.yes,
134
+ applied: !!opts.yes && counts.errors === 0,
135
+ aborted: !!opts.yes && counts.errors > 0,
136
+ repo: opts.repo ?? null,
137
+ counts,
138
+ rows,
139
+ });
140
+ if (counts.errors > 0)
141
+ emit.setExitCode(1);
142
+ }
143
+ // --- `harn docs meta` ---
144
+ function handleMeta(repoRoot, path, key, opts) {
145
+ const metadata = readDocsMetadata(repoRoot, path).data;
146
+ if (!key) {
147
+ emit.data(metadata);
148
+ return;
149
+ }
150
+ const value = readDocsMetadataKey(metadata, key, path);
151
+ if (opts.json || !process.stdout.isTTY || typeof value === "object") {
152
+ emit.data(value);
153
+ return;
154
+ }
155
+ emit.text(String(value));
156
+ }
89
157
  // --- Default `harn docs` (freshness report) ---
90
158
  async function handleDocs(opts) {
91
159
  if (opts.format === "json")
@@ -220,17 +220,25 @@ function checkHarnessHooks() {
220
220
  const bin = resolveBinName(root);
221
221
  const parts = drift.map((d) => {
222
222
  const bits = [];
223
+ if (d.parseError)
224
+ bits.push(`invalid JSON (${d.parseError})`);
223
225
  if (d.missing.length > 0) {
224
226
  bits.push(`${d.missing.length} missing (${d.missing.map((m) => m.subcommand).join(", ")})`);
225
227
  }
226
228
  if (d.orphans.length > 0)
227
229
  bits.push(`${d.orphans.length} orphaned (${d.orphans.join(", ")})`);
230
+ if (d.invalidTopLevelKeys.length > 0) {
231
+ bits.push(`invalid fields (${d.invalidTopLevelKeys.join(", ")})`);
232
+ }
233
+ if (d.invalidEventKeys.length > 0) {
234
+ bits.push(`unsupported events (${d.invalidEventKeys.join(", ")})`);
235
+ }
228
236
  return `${d.settingsFile}: ${bits.join("; ")}`;
229
237
  });
230
- const hasOrphans = drift.some((d) => d.orphans.length > 0);
231
- const hint = hasOrphans
232
- ? `run \`${bin} init\` to wire missing hooks; remove orphaned entries (renamed/dropped events) with \`${bin} deinit\` then \`${bin} init\``
233
- : `run \`${bin} init\` to wire the new hook(s) (idempotent)`;
238
+ const needsManualRepair = drift.some((d) => d.parseError || d.invalidTopLevelKeys.length > 0 || d.invalidEventKeys.length > 0);
239
+ const hint = needsManualRepair
240
+ ? `repair the invalid harness settings, then run \`${bin} init\` to migrate harnery hooks`
241
+ : `run \`${bin} init\` to migrate the hook set (idempotent)`;
234
242
  return { name: "harness hooks", severity: "warn", detail: parts.join(" | "), hint };
235
243
  }
236
244
  function macOrLinux(mac, linux) {
@@ -1 +1 @@
1
- {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/commands/env.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAqB1E,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,WAAW,EACjB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CAaN"}
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/commands/env.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAwB,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAqBhG,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,WAAW,EACjB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CAaN"}