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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/bin/buddy-agent-harness.mjs +8 -3
- package/dist/cli-Bvnq8zOL.mjs +2122 -0
- package/dist/cli-Bvnq8zOL.mjs.map +1 -0
- package/dist/cli.d.mts +13 -2
- package/dist/cli.d.mts.map +1 -1
- package/dist/cli.mjs +2 -20
- package/dist/index.d.mts +382 -47
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/package.json +3 -2
- package/plugin.json +1 -1
- package/skills/doctor/SKILL.md +20 -34
- package/skills/doctor/references/bridges.md +23 -0
- package/skills/doctor/references/configuration.md +12 -0
- package/skills/doctor/references/harnesses/claude-code.md +39 -0
- package/skills/doctor/references/harnesses/codex.md +37 -0
- package/skills/doctor/references/harnesses/copilot-cli.md +39 -0
- package/skills/doctor/references/harnesses/cursor.md +40 -0
- package/skills/doctor/references/harnesses/devin-desktop.md +20 -0
- package/skills/doctor/references/harnesses/gemini-cli.md +38 -0
- package/skills/doctor/references/harnesses/windsurf.md +27 -0
- package/skills/doctor/references/instructions.md +14 -0
- package/skills/doctor/references/mcp.md +31 -0
- package/skills/doctor/references/nonstandard.md +27 -0
- package/skills/doctor/scripts/doctor.mjs +7 -3
- package/skills/enhance/SKILL.md +4 -0
- package/skills/init/README.md +1 -1
- package/skills/init/SKILL.md +19 -8
- package/skills/init/references/detection.md +5 -3
- package/skills/init/references/harnesses/cursor.md +9 -2
- package/skills/init/scripts/doctor.mjs +14 -0
- package/skills/init/scripts/init.mjs +7 -3
- package/skills/repair/README.md +21 -0
- package/skills/repair/SKILL.md +74 -0
- package/skills/repair/references/classes.md +49 -0
- package/skills/repair/scripts/doctor.mjs +14 -0
- package/src/cli.ts +40 -7
- package/src/command-output/command-output.ts +1 -4
- package/src/diagnose-bridges/diagnose-bridges.ts +21 -13
- package/src/diagnose-bridges/diagnose-instructions.ts +7 -2
- package/src/diagnose-bridges/doctor-guidance.ts +636 -90
- package/src/diagnose-bridges/doctor.command.ts +55 -28
- package/src/diagnose-bridges/git-bridge-state.ts +30 -0
- package/src/diagnose-bridges/locator.ts +36 -0
- package/src/diagnose-configuration/diagnose-configuration.ts +99 -0
- package/src/diagnose-mcp/diagnose-mcp.ts +135 -0
- package/src/diagnose-mcp/mcp-baseline.ts +122 -0
- package/src/diagnose-mcp/mcp-model.ts +88 -0
- package/src/diagnose-mcp/mcp-secrets.ts +101 -0
- package/src/diagnose-mcp/mcp-sources.ts +131 -0
- package/src/diagnose-nonstandard/diagnose-nonstandard.ts +109 -0
- package/src/harness-registry/harness-registry.ts +97 -4
- package/src/harness-registry/mcp-config.ts +37 -0
- package/src/harness-registry/nonstandard-artifact.ts +39 -0
- package/src/index.ts +18 -2
- package/src/initialize-harnesses/init.command.ts +13 -18
- package/src/initialize-harnesses/initialize-harnesses.ts +6 -3
- package/src/is-record/is-record.ts +12 -0
- package/src/skill-projection/skill-projection.ts +102 -16
- package/dist/cli.mjs.map +0 -1
- package/dist/init.command-zbntulIq.mjs +0 -1016
- 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/` |
|
|
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`.**
|
|
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
|
-
|
|
10
|
-
|
|
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 {
|
|
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
|
-
|
|
6
|
-
|
|
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
|
|
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
|
|
42
|
+
const code = await app().parse<number | undefined>(argv)
|
|
43
|
+
return typeof code === 'number' ? code : exitCodes.success
|
|
13
44
|
} catch (error) {
|
|
14
|
-
|
|
15
|
-
|
|
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,
|
|
2
|
-
import {
|
|
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
|
-
/**
|
|
24
|
-
repair:
|
|
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
|
|
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({
|
|
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({
|
|
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
|
}
|