buddy-agent-harness 0.6.0 → 0.7.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 (65) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.cursor-plugin/plugin.json +1 -1
  4. package/bin/buddy-agent-harness.mjs +8 -3
  5. package/dist/cli-Bvnq8zOL.mjs +2122 -0
  6. package/dist/cli-Bvnq8zOL.mjs.map +1 -0
  7. package/dist/cli.d.mts +13 -2
  8. package/dist/cli.d.mts.map +1 -1
  9. package/dist/cli.mjs +2 -20
  10. package/dist/index.d.mts +382 -47
  11. package/dist/index.d.mts.map +1 -1
  12. package/dist/index.mjs +2 -2
  13. package/package.json +3 -2
  14. package/plugin.json +1 -1
  15. package/skills/doctor/SKILL.md +20 -34
  16. package/skills/doctor/references/bridges.md +23 -0
  17. package/skills/doctor/references/configuration.md +12 -0
  18. package/skills/doctor/references/harnesses/claude-code.md +39 -0
  19. package/skills/doctor/references/harnesses/codex.md +37 -0
  20. package/skills/doctor/references/harnesses/copilot-cli.md +39 -0
  21. package/skills/doctor/references/harnesses/cursor.md +40 -0
  22. package/skills/doctor/references/harnesses/devin-desktop.md +20 -0
  23. package/skills/doctor/references/harnesses/gemini-cli.md +38 -0
  24. package/skills/doctor/references/harnesses/windsurf.md +27 -0
  25. package/skills/doctor/references/instructions.md +14 -0
  26. package/skills/doctor/references/mcp.md +31 -0
  27. package/skills/doctor/references/nonstandard.md +27 -0
  28. package/skills/doctor/scripts/doctor.mjs +7 -3
  29. package/skills/enhance/SKILL.md +4 -0
  30. package/skills/init/README.md +1 -1
  31. package/skills/init/SKILL.md +19 -8
  32. package/skills/init/references/detection.md +5 -3
  33. package/skills/init/references/harnesses/cursor.md +9 -2
  34. package/skills/init/scripts/doctor.mjs +14 -0
  35. package/skills/init/scripts/init.mjs +7 -3
  36. package/skills/repair/README.md +21 -0
  37. package/skills/repair/SKILL.md +74 -0
  38. package/skills/repair/references/classes.md +49 -0
  39. package/skills/repair/scripts/doctor.mjs +14 -0
  40. package/src/cli.ts +40 -7
  41. package/src/command-output/command-output.ts +1 -4
  42. package/src/diagnose-bridges/diagnose-bridges.ts +21 -13
  43. package/src/diagnose-bridges/diagnose-instructions.ts +7 -2
  44. package/src/diagnose-bridges/doctor-guidance.ts +636 -90
  45. package/src/diagnose-bridges/doctor.command.ts +55 -28
  46. package/src/diagnose-bridges/git-bridge-state.ts +30 -0
  47. package/src/diagnose-bridges/locator.ts +36 -0
  48. package/src/diagnose-configuration/diagnose-configuration.ts +99 -0
  49. package/src/diagnose-mcp/diagnose-mcp.ts +135 -0
  50. package/src/diagnose-mcp/mcp-baseline.ts +122 -0
  51. package/src/diagnose-mcp/mcp-model.ts +88 -0
  52. package/src/diagnose-mcp/mcp-secrets.ts +101 -0
  53. package/src/diagnose-mcp/mcp-sources.ts +131 -0
  54. package/src/diagnose-nonstandard/diagnose-nonstandard.ts +109 -0
  55. package/src/harness-registry/harness-registry.ts +97 -4
  56. package/src/harness-registry/mcp-config.ts +37 -0
  57. package/src/harness-registry/nonstandard-artifact.ts +39 -0
  58. package/src/index.ts +18 -2
  59. package/src/initialize-harnesses/init.command.ts +13 -18
  60. package/src/initialize-harnesses/initialize-harnesses.ts +6 -3
  61. package/src/is-record/is-record.ts +12 -0
  62. package/src/skill-projection/skill-projection.ts +102 -16
  63. package/dist/cli.mjs.map +0 -1
  64. package/dist/init.command-zbntulIq.mjs +0 -1016
  65. package/dist/init.command-zbntulIq.mjs.map +0 -1
@@ -8,11 +8,13 @@ What to look for during the survey, and where each finding belongs.
8
8
  | Nested instructions | `**/AGENTS.md` below the root, and any `**/CLAUDE.md` beside one | canonical and scoped — report, never merge upward. Bridging is per directory and per approval; see `agents-md.md` |
9
9
  | Skills | `.claude/skills/`, `.cursor/skills/`, `.codex/skills/`, `.github/skills/`, `.windsurf/skills/`, `.gemini/skills/` | move to `.agents/skills/`, fix frontmatter, then link back |
10
10
  | Commands | `.claude/commands/*.md`, `.cursor/commands/*.md` | move to `.agents/skills/<name>/SKILL.md`. Claude Code has merged commands into skills, so this follows the harness rather than fighting it |
11
- | Subagents | `.claude/agents/` | canonical-only — no cross-harness format exists. Report; leave in place |
12
- | Rules | `.cursor/rules/**.mdc`, `.claude/rules/`, `.windsurf/rules/` | canonical-only — `.mdc` and `.md` are not interchangeable, and path-scoping syntax differs. Report |
13
- | MCP servers | `.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`, `.claude/settings.json` | canonical-only. Report; never convert between formats |
11
+ | Subagents | `.claude/agents/` | canonical-only — no cross-harness format exists. Report as having no candidate at all, so the list is not read as pending work; leave in place |
12
+ | Rules | `.cursor/rules/**.mdc`, `.claude/rules/`, `.windsurf/rules/` | offer a skill where the paths a rule names are incidental to what it says; leave it where the path scoping is the point. `.mdc` and `.md` are not interchangeable and path-scoping has no `AGENTS.md` equivalent, so the choice is the owner's — ask per rule |
13
+ | MCP servers | `.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`, `.claude/settings.json` | canonical-only. Report; never convert between formats. A mapping between them exists but is not lossless, and applying it means inventing fields the source did not carry |
14
14
  | Hooks, LSP, output styles | hook blocks in settings files, `.claude/output-styles/` | canonical-only. Hook event names differ by case across harnesses, so no safe projection exists. Report |
15
15
 
16
+ `doctor` reports every artifact only one harness can read, each with the canonical form it is a candidate for. That report is the list — the table above says what each class *is*, and the command says what this repository actually has. Do not derive a second list by walking these paths yourself.
17
+
16
18
  For every entry, record which of three states it is in:
17
19
 
18
20
  - a real file or directory the user authored,
@@ -14,10 +14,17 @@ Tell the user: a repository that consolidates into `AGENTS.md` keeps its instruc
14
14
 
15
15
  ## Do not
16
16
 
17
- - **Do not generate `.cursor/rules/*.mdc` from `AGENTS.md`.** Rules are canonical-only — `.mdc` and `.md` are not interchangeable and path-scoping has no `AGENTS.md` equivalent.
18
- - **Do not delete or rewrite `.cursorrules` or `.cursor/rules/**` on the assumption that `AGENTS.md` covers them.** In Chat and Composer it does not.
17
+ - **Do not generate `.cursor/rules/*.mdc` from `AGENTS.md`.** `.mdc` and `.md` are not interchangeable and path-scoping has no `AGENTS.md` equivalent, so a generated rule would be inventing scope the source never carried.
18
+ - **Do not delete or rewrite `.cursorrules` or `.cursor/rules/**` on the assumption that `AGENTS.md` covers them.** In Chat and Composer it does not. This is the reason a migration is *offered* rather than applied: the owner is agreeing to a trade, and they cannot agree to one nobody described.
19
19
  - Do not assert whether Cursor's `.agents/skills` discovery recurses into nested subdirectories. It is untested.
20
20
 
21
+ ## Migrating a rule, when the owner asks for it
22
+
23
+ Offer, never assume, and offer the whole thing:
24
+
25
+ - **A rule whose paths are incidental** — guidance that happens to name files but says something generally true — becomes a skill under `.agents/skills/`, which every harness reads. A rule whose scoping *is* the point has no equivalent; say so and leave it.
26
+ - **Consolidating `.cursorrules` into `AGENTS.md` narrows who reads it** unless a copy stays behind for Chat and Composer. Offer the consolidation and the copy together. Never offer the consolidation alone, and never delete the file because `AGENTS.md` now carries the words.
27
+
21
28
  ## Frontmatter
22
29
 
23
30
  `paths`, `disable-model-invocation`, and legacy `globs` are Cursor-recognized. Other harnesses drop them — keep load-bearing behavior in the body, per `frontmatter.md`.
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ // Generated from src/diagnose-bridges/doctor-guidance.ts by scripts/generate-skills.ts. Do not edit by hand.
3
+ import { dirname, join } from 'node:path'
4
+ import { fileURLToPath, pathToFileURL } from 'node:url'
5
+
6
+ // <package>/skills/<skill>/scripts/doctor.mjs: four levels up is the package root.
7
+ const packageRoot = dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url)))))
8
+ const { run } = await import(pathToFileURL(join(packageRoot, 'dist', 'cli.mjs')).href)
9
+
10
+ // The subcommand is composed into a fresh argv rather than spliced into the global one, so nothing
11
+ // outside this file observes the rewrite. Applied only when non-zero, so a usage code clibuilder
12
+ // recorded itself is not overwritten by the zero `run` returns on that path.
13
+ const code = await run([...process.argv.slice(0, 2), 'doctor', ...process.argv.slice(2)])
14
+ if (code !== 0) process.exitCode = code
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  // Generated from src/diagnose-bridges/doctor-guidance.ts by scripts/generate-skills.ts. Do not edit by hand.
3
3
  import { dirname, join } from 'node:path'
4
- import { fileURLToPath } from 'node:url'
4
+ import { fileURLToPath, pathToFileURL } from 'node:url'
5
5
 
6
6
  // <package>/skills/<skill>/scripts/init.mjs: four levels up is the package root.
7
7
  const packageRoot = dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url)))))
8
+ const { run } = await import(pathToFileURL(join(packageRoot, 'dist', 'cli.mjs')).href)
8
9
 
9
- process.argv.splice(2, 0, 'init')
10
- await import(join(packageRoot, 'bin', 'buddy-agent-harness.mjs'))
10
+ // The subcommand is composed into a fresh argv rather than spliced into the global one, so nothing
11
+ // outside this file observes the rewrite. Applied only when non-zero, so a usage code clibuilder
12
+ // recorded itself is not overwritten by the zero `run` returns on that path.
13
+ const code = await run([...process.argv.slice(0, 2), 'init', ...process.argv.slice(2)])
14
+ if (code !== 0) process.exitCode = code
@@ -0,0 +1,21 @@
1
+ # Harness Repair
2
+
3
+ Correct agent configuration a repository already has that is wrong or outdated.
4
+
5
+ ## What it does
6
+
7
+ `init` consolidates what a repository has, `enhance` offers what it is missing, and `doctor` reports what is wrong. Configuration that is present but **wrong** falls between all three, and that is what this skill takes: a harness name the registry retired, a git-ignored bridge, an `AGENTS.local.md` nothing reads, a skill whose frontmatter makes a harness skip it.
8
+
9
+ Each correction is offered with the file as it stands and as it would read, and written only on approval.
10
+
11
+ ## Why a fourth skill rather than a `--fix` flag
12
+
13
+ `doctor` stays read-only. Its whole safety property is that it never writes, which is what makes it safe to run from a session-start hook — a repair flag would forfeit that for every caller. Its **bridge** findings are all repairable with existing `init` flags, so a flag there would duplicate `init` rather than add anything; its **configuration** findings need judgment about files you wrote, which is a skill's job and not a flag's.
14
+
15
+ That split also defines this skill's remit. `repair` owns what `init` deliberately refuses. Every bridge finding — including an instruction bridge that was never completed — is reported and handed to `init`, which is what writes bridges in the first place.
16
+
17
+ ## The line it does not cross
18
+
19
+ `repair` corrects what the **tooling** decides is wrong, never what the project means. The discriminator is the one `init` already applies: content that would stop being true if this tool's output were removed is non-material, and non-material content is all this skill may correct. A statement about how the repository is worked in stays the user's, even when it is out of date — `repair` reports it and offers no write.
20
+
21
+ `references/classes.md` carries the detection, the correction, and the stopping point for each class.
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: repair
3
+ description: Use this skill when a repository's agent configuration is present but wrong or outdated — a retired harness name, a git-ignored bridge, an AGENTS.local.md nothing reads, or a skill a harness refuses to load. Runs doctor to find them, then offers each correction with its before and after and writes only what is approved.
4
+ ---
5
+
6
+ # Harness Repair
7
+
8
+ `init` consolidates what a repository has. `enhance` proposes what it does not. `doctor` reports what is wrong. `repair` corrects it.
9
+
10
+ Each of the other three refuses this work deliberately. `init` has to run anywhere and invent nothing, so it never clobbers a file the user wrote. `enhance` only offers what is **missing**. `doctor` never writes, which is what makes it safe in a session-start hook. Configuration that is present but wrong falls between them.
11
+
12
+ **`doctor` detects; this skill repairs.** Detection has one home, so the two cannot drift and the finding set can grow without touching this file. Do not detect anything yourself — run the command and repair what it reports.
13
+
14
+ ## 1. Diagnose
15
+
16
+ ```sh
17
+ node scripts/doctor.mjs
18
+ ```
19
+
20
+ That path is relative to this skill's own directory. The launcher runs the CLI that shipped beside it against the current working directory, so nothing is downloaded. Fall back to `npx -y buddy-agent-harness@^0.5.0 doctor` when the launcher cannot be resolved or run.
21
+
22
+ The default output is TOON, which is what you parse. Each entry in `findings` carries three fields: `problem` (the finding's name), `path` (what it is about), and `detail` (what is wrong, in prose). The repairs are not on the row — they are lifted into `help`, which follows `findings` in order and collapses two entries only where the repair is word-for-word identical. Read a finding's repair off the matching position and confirm it by the `path` it names; where two findings sit at one path, what each repair says to do is what tells them apart.
23
+
24
+ **Route on `problem`, never on `detail`.** The prose exists to be read by a person, and rewording it must not change what you do.
25
+
26
+ If `findings` reports zero problems, say so and stop. There is nothing to repair.
27
+
28
+ ## 2. Sort what it found
29
+
30
+ **Route on `problem`.** `references/classes.md` carries one section per fault this skill corrects, keyed by the `problem` name. A `problem` with a section there is yours; a `problem` without one is not. Nothing else decides it.
31
+
32
+ That rule holds for every finding `doctor` reports today and for the ones it reports next year. A fault this skill has no correction for is not one it can repair, so what `classes.md` covers and what this skill owns are the same fact — and a finding added tomorrow gets reported rather than improvised on.
33
+
34
+ **Do not read ownership off a skill name in `help`.** Most repairs name no skill — some are a shell invocation, some a `git` command, some a reconciliation by hand — and only the instruction and configuration families name one at all. An owner is not something the report can always be asked for.
35
+
36
+ - **Findings that are yours.** Look the `problem` up in `references/classes.md`, which carries the correction and the stopping point for each.
37
+ - **Findings that are not yours.** Report each one and pass on the repair `doctor` states for it, unchanged. **Who to hand it to is read off that repair, and the question is whether it names `init`.** Some name the `/buddy-agent-harness:init` skill and some name a `buddy-agent-harness init` command line; both mean the same thing — the finding is `init`'s, because `init` writes both kinds of bridge in the first place, and it writes the `CLAUDE.md` stub *without* asking, where everything here needs approval, and one write cannot have two homes and two contradictory approval rules. Hand it to the **skill** and never run the command yourself: rebuilding a projection can relocate skills a user wrote, and on the Windows case the naive repair is to recreate the link, the operation that already failed on that machine. A repair naming `init` in **neither** form — a `git` invocation, a reconciliation by hand, every MCP finding — is work for a person; say that rather than inferring an owner, and never rebuild over a two-sided divergence, which discards whichever side holds the newer edit.
38
+
39
+ ## 3. Draw the line at material content
40
+
41
+ You correct what the **tooling** decides is wrong, never what the repository means.
42
+
43
+ `../init/references/agents-md.md` draws the line: content that would stop being true if this tool's output were removed is **non-material**, and non-material content is all you may correct. A statement about how the repository is worked in is the user's, even when it is out of date — report it and offer no write.
44
+
45
+ The clearest case is `unloadable-skill`. A `description` broken by an unquoted colon is a quoting fault — correct it. A `description` that is **missing** cannot be written without asserting what a skill you did not author does, and that claim holds whether or not this tool ever ran. Report it and ask.
46
+
47
+ ## 4. Offer
48
+
49
+ Show the file **as it stands and as it would read**. Not a summary — the actual before and after, so the user is reading the change rather than your description of it.
50
+
51
+ Where more than one correction is valid, present the options and ask which. Never pick for the user. `unread-local-override` is always this case, and its options come from `references/classes.md` — `doctor` names one repair per finding and has not read the file's contents.
52
+
53
+ Do not argue past one sentence. The diff is the argument.
54
+
55
+ ## 5. Apply and re-run
56
+
57
+ Write only what was approved, preserving the rest of the file exactly.
58
+
59
+ Then **run `doctor` again**. A correction can fail to hold — a second `.gitignore` rule matching the same path, a frontmatter fix that leaves another fault behind. Re-running is how you know, and it costs nothing. When a finding survives, say it is still open rather than reporting it landed.
60
+
61
+ ## 6. Report
62
+
63
+ Report every run, whichever way it went. One row per finding, naming its **path**, what was wrong, and its **outcome** — corrected, declined, handed to `init`, reported as material, or still open.
64
+
65
+ A run that finds nothing still reports. "Nothing found" without saying `doctor` ran clean is indistinguishable from not having looked.
66
+
67
+ ## Rules
68
+
69
+ - **Never write without approval.** There is no flag that skips it and none should be added.
70
+ - **Never detect.** `doctor` owns detection. A check written here is a second home for it, and two homes drift.
71
+ - **Never rebuild a bridge.** That is `doctor`'s diagnosis and `init`'s repair.
72
+ - **Never consolidate.** Moving content into `AGENTS.md` is `init`'s and has one home.
73
+ - **Correct only non-material content.** Project policy is the user's, even when it is wrong.
74
+ - Local agent configuration only. Do not change workflows, GitHub Actions, repository settings, security scanning, branch rules, or unrelated project files — a retired harness name in a workflow file is not yours to rename.
@@ -0,0 +1,49 @@
1
+ # Correction classes
2
+
3
+ One row per fault `doctor` reports here, with the correction and where it stops.
4
+
5
+ **This file carries no detection.** `doctor` decides what is wrong; you decide how to correct it. If you find yourself checking whether a fault is present, you are doing the command's job — run it instead.
6
+
7
+ Every correction below is non-material: it would stop being true if this tool's output were removed. That is what makes it correctable at all. Where a finding's correction would change what the repository *means*, report it and stop; `../../init/references/agents-md.md` draws the line.
8
+
9
+ ## `deprecated-harness`
10
+
11
+ **Correct.** The replacement harness reads `.agents/skills/` natively and needs no projection at all, so the correction is to **delete** the reported projection — not to rename it, and not to create a new one under the new name. This is the one correction that removes a path rather than editing a file, so show what will be deleted and what will remain.
12
+
13
+ **Stops at.** Everything under that harness's directory that is not the skills projection. `.windsurfrules` and any settings file are instruction artifacts; consolidating them is `init`'s. A harness name in a workflow, a README, or a comment is prose, not configuration — leave it.
14
+
15
+ ## `ignored-bridge`
16
+
17
+ **Correct.** Narrow or remove the reported `.gitignore` rule so the bridge is tracked. Where the rule exists for something else under that directory, the correction is a negation (`!.claude/skills`) rather than a deletion — that keeps the rest of the rule doing its job.
18
+
19
+ **Stops at.** Every rule that does not match a bridge path. `.gitignore` is a project file, and only the part swallowing a bridge is this tool's business.
20
+
21
+ ## `unread-local-override`
22
+
23
+ **More than one correction is valid, so this fault always presents options and never picks.** The options come from this file, not from the report: `doctor` names one repair per finding, and choosing among these is a judgment about content it has not read.
24
+
25
+ Read the file to describe the options, then offer all three:
26
+
27
+ - move it to `CLAUDE.local.md` and add that to `.gitignore`, where the content is personal and Claude Code is the reader that matters;
28
+ - hand it to `init` to consolidate into `AGENTS.md`, where the content is project guidance — that move is material, and `init`'s;
29
+ - delete it, where it is dead.
30
+
31
+ **Stops at.** The content. Read it to describe the options; never rewrite it.
32
+
33
+ ## `unloadable-skill`
34
+
35
+ Two faults arrive under this name, and only one of them is correctable.
36
+
37
+ **An unquoted colon in `description`.** Correctable: quote the value. The YAML then parses and the skill loads.
38
+
39
+ **A missing or empty `description`.** **Not correctable — report it and ask.** Writing one would mean inventing a claim about what a skill you did not author does, and that claim is material: it stays true whether or not this tool ever ran. Name the skill, say why it will not load, and stop.
40
+
41
+ **Stops at.** Everything below the frontmatter. The skill body is the author's. A `name` that mismatches its directory is not reported by `doctor` at all — it is a warning and the skill still loads.
42
+
43
+ ## Not yours: every `problem` with no section above
44
+
45
+ `doctor` reports far more than this file covers: skills bridges that no longer resolve, instruction bridges that were never completed, and drift between the golden MCP server set and a harness's copy of it. None of them has a section here, and that is what says they are not yours.
46
+
47
+ **Who to hand one to is read off the repair `doctor` gave it, and the question is whether it names `init`.** It names it one of two ways — the `/buddy-agent-harness:init` skill, or a `buddy-agent-harness init` command line — and both mean the finding is `init`'s, which writes both kinds of bridge in the first place and writes a `CLAUDE.md` stub *without* asking, where every correction here needs approval. Hand it to the skill; never run the command. A repair naming `init` in **neither** form is work for a person: `doctor` states it in full, and passing it on is the whole of what you do with it. Every MCP finding is that second case, as is every bridge finding a rebuild would not fix.
48
+
49
+ Never infer an owner for a finding that names none. The wrong guess is always `init`, and on a two-sided divergence rebuilding is precisely what discards whichever side holds the newer edit.
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ // Generated from src/diagnose-bridges/doctor-guidance.ts by scripts/generate-skills.ts. Do not edit by hand.
3
+ import { dirname, join } from 'node:path'
4
+ import { fileURLToPath, pathToFileURL } from 'node:url'
5
+
6
+ // <package>/skills/<skill>/scripts/doctor.mjs: four levels up is the package root.
7
+ const packageRoot = dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url)))))
8
+ const { run } = await import(pathToFileURL(join(packageRoot, 'dist', 'cli.mjs')).href)
9
+
10
+ // The subcommand is composed into a fresh argv rather than spliced into the global one, so nothing
11
+ // outside this file observes the rewrite. Applied only when non-zero, so a usage code clibuilder
12
+ // recorded itself is not overwritten by the zero `run` returns on that path.
13
+ const code = await run([...process.argv.slice(0, 2), 'doctor', ...process.argv.slice(2)])
14
+ if (code !== 0) process.exitCode = code
package/src/cli.ts CHANGED
@@ -1,17 +1,50 @@
1
- import { cli } from 'clibuilder'
1
+ import { readFileSync } from 'node:fs'
2
+ import { cli, exitCodes } from 'clibuilder'
2
3
  import { doctorCommand } from './diagnose-bridges/doctor.command.ts'
3
4
  import { initCommand } from './initialize-harnesses/init.command.ts'
4
5
 
5
- export async function main(): Promise<void> {
6
- const app = cli({
6
+ /**
7
+ * Read from the manifest rather than restated here: a version written into this call is a second
8
+ * home for a number that already has one, and it stayed at `0.1.0` for five minor releases.
9
+ *
10
+ * `../package.json` resolves from `src/cli.ts` and from the bundled `dist/cli.mjs` alike — both sit
11
+ * one directory under the package root.
12
+ */
13
+ const version = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version as string
14
+
15
+ /**
16
+ * A factory rather than a module-level constant: `cli()` builds state, and state built at import
17
+ * time is shared by every later call in the process.
18
+ */
19
+ function app() {
20
+ return cli({
7
21
  name: 'buddy-agent-harness',
8
- version: '0.1.0',
22
+ version,
9
23
  description: 'Initialize agent harness skill compatibility in consumer repositories.',
10
24
  })
25
+ .command(initCommand)
26
+ .command(doctorCommand)
27
+ }
28
+
29
+ /**
30
+ * The application boundary: argv in, exit code out, nothing read from or written to the process.
31
+ *
32
+ * The exit code is returned rather than written so that a caller which is not the process — a skill
33
+ * launcher, a test — can run a command and learn how it went. `bin/buddy-agent-harness.mjs` is the
34
+ * only place that turns the returned code back into a process outcome.
35
+ *
36
+ * One exit path this return cannot carry: `clibuilder` answers an unknown option or an unknown
37
+ * command by printing help and writing `process.exitCode` itself, returning nothing. `run` reports
38
+ * `0` there, which is why `bin` and the launchers apply the returned code only when it is non-zero.
39
+ */
40
+ export async function run(argv: string[]): Promise<number> {
11
41
  try {
12
- await app.command(initCommand).command(doctorCommand).parse(process.argv)
42
+ const code = await app().parse<number | undefined>(argv)
43
+ return typeof code === 'number' ? code : exitCodes.success
13
44
  } catch (error) {
14
- process.stdout.write(`error: ${error instanceof Error ? error.message : 'Invalid command.'}\n`)
15
- process.exitCode = 2
45
+ // stderr, not stdout: `doctor`'s default output is TOON that an agent parses, and an error
46
+ // line on that stream lands in the middle of what it is parsing.
47
+ process.stderr.write(`error: ${error instanceof Error ? error.message : 'Invalid command.'}\n`)
48
+ return exitCodes.usage
16
49
  }
17
50
  }
@@ -1,5 +1,6 @@
1
1
  import { sep } from 'node:path'
2
2
  import { encode } from '@toon-format/toon'
3
+ import { isRecord } from '../is-record/is-record.ts'
3
4
 
4
5
  export type OutputFormat = 'json' | 'toon' | 'text'
5
6
 
@@ -21,10 +22,6 @@ function encodeResult(value: object, format: OutputFormat): string {
21
22
  return format === 'text' ? renderText(value) : encode(value)
22
23
  }
23
24
 
24
- function isRecord(value: unknown): value is Record<string, unknown> {
25
- return typeof value === 'object' && value !== null && !Array.isArray(value)
26
- }
27
-
28
25
  function cell(value: unknown): string {
29
26
  return value === undefined ? '' : String(value)
30
27
  }
@@ -1,9 +1,10 @@
1
- import { lstatSync, readFileSync, readlinkSync, statSync } from 'node:fs'
2
- import { dirname, join, relative } from 'node:path'
1
+ import { lstatSync, readFileSync, statSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
3
  import { type HarnessName, selectHarnesses } from '../harness-registry/harness-registry.ts'
4
+ import { linksTo } from '../skill-projection/skill-projection.ts'
4
5
  import { diagnoseInstructions, type InstructionReport } from './diagnose-instructions.ts'
5
6
  import { filesUnder } from './directory-files.ts'
6
- import { type BridgeProblem, repairFor } from './doctor-guidance.ts'
7
+ import { type BridgeProblem, type DoctorProblem, type RepairAction, repairFor } from './doctor-guidance.ts'
7
8
  import { type DivergenceDirection, GitBridgeState } from './git-bridge-state.ts'
8
9
 
9
10
  export type BridgeKind = 'symlink' | 'copy' | 'file' | 'none'
@@ -19,9 +20,14 @@ export type BridgeReport = {
19
20
 
20
21
  export type BridgeFinding = {
21
22
  path: string
23
+ /**
24
+ * Which problem this is. Emitted so a caller routes on a name rather than by matching `detail`
25
+ * prose, which would break the moment the wording is improved.
26
+ */
27
+ problem: DoctorProblem
22
28
  detail: string
23
- /** The command that repairs this finding, already carrying the bridge path. */
24
- repair: string
29
+ /** What repairs this finding, already carrying the bridge path. Empty `command` means judgment. */
30
+ repair: RepairAction
25
31
  }
26
32
 
27
33
  export type DivergenceReport = {
@@ -67,11 +73,6 @@ function isDirectory(path: string): boolean {
67
73
  }
68
74
  }
69
75
 
70
- /** The same test `init` uses to decide a projection is already correct, plus that it still resolves. */
71
- function resolvesToCanonical(target: string, canonical: string): boolean {
72
- return readlinkSync(target) === relative(dirname(target), canonical) && isDirectory(target)
73
- }
74
-
75
76
  function sameContent(left: string, right: string): boolean {
76
77
  const leftFiles = filesUnder(left)
77
78
  const rightFiles = filesUnder(right)
@@ -99,8 +100,10 @@ function inspect(target: string, path: string, canonical: string, git: GitBridge
99
100
  // regular file holding the target path, and the harness silently loads nothing.
100
101
  case 'file':
101
102
  return { kind: 'file', status: 'degraded', problem: 'degraded' }
103
+ // The very test `init` uses to decide a projection is already correct, so a bridge `init` would
104
+ // leave alone never reports as one `doctor` wants rebuilt.
102
105
  case 'symlink':
103
- return resolvesToCanonical(target, canonical)
106
+ return linksTo(target, canonical)
104
107
  ? { kind: 'symlink', status: 'ok' }
105
108
  : { kind: 'symlink', status: 'stale', problem: 'stale' }
106
109
  default: {
@@ -136,7 +139,12 @@ export function diagnoseBridges({ root, harnesses: preferred = [], cli }: Diagno
136
139
 
137
140
  if (!isDirectory(canonical)) {
138
141
  const { detail, repair } = repairFor('no-canonical')
139
- findings.push({ path: canonicalPath, detail, repair: repair(canonicalPath, cli) })
142
+ findings.push({
143
+ path: canonicalPath,
144
+ problem: 'no-canonical',
145
+ detail,
146
+ repair: repair({ file: canonicalPath }, cli),
147
+ })
140
148
  }
141
149
 
142
150
  for (const harness of bridged) {
@@ -146,7 +154,7 @@ export function diagnoseBridges({ root, harnesses: preferred = [], cli }: Diagno
146
154
  if (inspection.direction) divergence.push({ path, direction: inspection.direction })
147
155
  if (inspection.problem) {
148
156
  const { detail, repair } = repairFor(inspection.problem)
149
- findings.push({ path, detail, repair: repair(path, cli) })
157
+ findings.push({ path, problem: inspection.problem, detail, repair: repair({ file: path }, cli) })
150
158
  }
151
159
  }
152
160
 
@@ -122,7 +122,12 @@ export function diagnoseInstructions(
122
122
  // lies, so its absence is a repository that has not run `init` rather than a broken bridge.
123
123
  if (!agentsFileDirectories(root).includes('')) {
124
124
  const { detail, repair } = repairFor('no-instructions')
125
- findings.push({ path: canonicalInstructions, detail, repair: repair(canonicalInstructions, cli) })
125
+ findings.push({
126
+ path: canonicalInstructions,
127
+ problem: 'no-instructions',
128
+ detail,
129
+ repair: repair({ file: canonicalInstructions }, cli),
130
+ })
126
131
  }
127
132
 
128
133
  for (const { name, bridge } of bridged) {
@@ -131,7 +136,7 @@ export function diagnoseInstructions(
131
136
  instructions.push({ harness: name, path, kind: inspection.kind, status: inspection.status })
132
137
  if (inspection.problem) {
133
138
  const { detail, repair } = repairFor(inspection.problem)
134
- findings.push({ path, detail, repair: repair(path, cli) })
139
+ findings.push({ path, problem: inspection.problem, detail, repair: repair({ file: path }, cli) })
135
140
  }
136
141
  }
137
142
  }