buddy-agent-harness 1.0.1 → 1.2.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/CHANGELOG.md +44 -0
- package/dist/cli.mjs +2166 -702
- package/dist/index.d.mts +10 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +597 -431
- package/dist/index.mjs.map +1 -1
- package/dist/skill-scripts/doctor.mjs +74 -67
- package/dist/skill-scripts/init.mjs +74 -67
- package/dist/skill-scripts/mcp.mjs +74 -67
- package/llms.txt +8 -6
- package/package.json +5 -4
- package/plugin.json +1 -1
- package/skills/doctor-buddy-agent-harness/README.md +1 -1
- package/skills/doctor-buddy-agent-harness/SKILL.md +9 -2
- package/skills/doctor-buddy-agent-harness/references/worktrees.md +17 -0
- package/skills/doctor-buddy-agent-harness/scripts/doctor.mjs +74 -67
- package/skills/enhance/SKILL.md +18 -10
- package/skills/enhance/references/agent-worktrees.md +59 -0
- package/skills/enhance/references/list-identifiers.history.md +9 -1
- package/skills/enhance/references/list-identifiers.md +6 -9
- package/skills/enhance/references/references-as-links.history.md +9 -0
- package/skills/enhance/references/references-as-links.md +76 -0
- package/skills/enhance/scripts/doctor.mjs +74 -67
- package/skills/init-buddy-agent-harness/SKILL.md +8 -5
- package/skills/init-buddy-agent-harness/references/agent-worktrees.md +59 -0
- package/skills/init-buddy-agent-harness/scripts/doctor.mjs +74 -67
- package/skills/init-buddy-agent-harness/scripts/init.mjs +74 -67
- package/skills/repair/README.md +2 -2
- package/skills/repair/SKILL.md +3 -3
- package/skills/repair/scripts/doctor.mjs +74 -67
- package/skills/repair/scripts/mcp.mjs +74 -67
- package/src/cli.ts +13 -3
- package/src/command-output/command-output.ts +28 -5
- package/src/dep-plugins/dep-plugins.command.ts +4 -0
- package/src/diagnose-bridges/doctor-guidance.ts +66 -1
- package/src/diagnose-bridges/doctor.command.ts +69 -49
- package/src/diagnose-bridges/git-bridge-state.ts +5 -0
- package/src/diagnose-worktrees/diagnose-worktrees.ts +69 -0
- package/src/governance-overrides/governance.command.ts +13 -8
- package/src/index.ts +1 -0
- package/src/initialize-harnesses/init.command.ts +9 -3
- package/src/project-mcp/mcp.command.ts +15 -9
- package/src/reconcile-mcp/reconcile.command.ts +17 -11
- package/src/references/references.ts +1 -0
package/src/cli.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
2
|
import { cli } from 'clibuilder'
|
|
3
|
-
import { exitCodes } from 'clibuilder/axi'
|
|
3
|
+
import { exitCodes, homeHeader, result } from 'clibuilder/axi'
|
|
4
4
|
import { depPluginsCommand } from './dep-plugins/dep-plugins.command.ts'
|
|
5
|
-
import { doctorCommand } from './diagnose-bridges/doctor.command.ts'
|
|
5
|
+
import { diagnoseReport, doctorCommand } from './diagnose-bridges/doctor.command.ts'
|
|
6
6
|
import { governanceCommand } from './governance-overrides/governance.command.ts'
|
|
7
7
|
import { initCommand } from './initialize-harnesses/init.command.ts'
|
|
8
8
|
import { mcpCommand } from './project-mcp/mcp.command.ts'
|
|
@@ -31,9 +31,19 @@ export const rootCommand: { name: string; description: string; commands: readonl
|
|
|
31
31
|
// would be shared by every later call in the process.
|
|
32
32
|
function app(format: 'toon' | 'text') {
|
|
33
33
|
const [first, ...rest] = rootCommand.commands
|
|
34
|
+
// AXI §8: a bare invocation shows live data, not a manual — the doctor report, under the home header.
|
|
34
35
|
return rest.reduce(
|
|
35
36
|
(builder, command) => builder.command(command),
|
|
36
|
-
cli({ name: rootCommand.name, version, description: rootCommand.description, format })
|
|
37
|
+
cli({ name: rootCommand.name, version, description: rootCommand.description, format })
|
|
38
|
+
.default({
|
|
39
|
+
description: "Report whether this repository's harness bridges resolve (the same as `doctor`).",
|
|
40
|
+
options: doctorCommand.options as NonNullable<typeof doctorCommand.options>,
|
|
41
|
+
async run(args) {
|
|
42
|
+
const { bin: _bin, ...report } = await diagnoseReport({ root: args['root'], harness: args['harness'] })
|
|
43
|
+
return result({ ...homeHeader({ description: rootCommand.description }), ...report })
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
.command(first),
|
|
37
47
|
)
|
|
38
48
|
}
|
|
39
49
|
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { sep } from 'node:path'
|
|
2
2
|
import { type Answer, CliError, exitCodes, type Format, isCliError, printAnswer } from 'clibuilder/axi'
|
|
3
|
+
import { type HarnessName, parseHarnesses } from '../harness-registry/harness-registry.ts'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* AXI §6: a failure is a coded error on stdout, in the caller's format, with a `help` line saying
|
|
7
|
+
* what to do next. A `CliError` the command already built is rethrown untouched; anything else
|
|
8
|
+
* becomes `command-failed`, exit code 1.
|
|
7
9
|
*/
|
|
8
|
-
export function fail(error: unknown, fallback: string): never {
|
|
10
|
+
export function fail(error: unknown, fallback: string, help?: string): never {
|
|
9
11
|
if (isCliError(error)) throw error
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
throw new CliError(error instanceof Error ? error.message : fallback, {
|
|
13
|
+
code: 'command-failed',
|
|
14
|
+
exitCode: exitCodes.error,
|
|
15
|
+
...(help ? { help } : {}),
|
|
16
|
+
cause: error,
|
|
17
|
+
})
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
/**
|
|
@@ -32,3 +38,20 @@ export function collapseHome(home: string, path: string): string {
|
|
|
32
38
|
export function displayBinPath(home: string, executable: string | undefined): string {
|
|
33
39
|
return executable ? collapseHome(home, executable) : 'buddy-agent-harness'
|
|
34
40
|
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `parseHarnesses` for a command's `--harness`: an unsupported name is a usage error, exit code 2.
|
|
44
|
+
* The registry stays free of clibuilder, which the library entry treats as an optional peer.
|
|
45
|
+
*/
|
|
46
|
+
export function parseHarnessOption(value: string | undefined): HarnessName[] {
|
|
47
|
+
try {
|
|
48
|
+
return parseHarnesses(value)
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw new CliError((error as Error).message, {
|
|
51
|
+
code: 'unsupported-harness',
|
|
52
|
+
exitCode: exitCodes.usage,
|
|
53
|
+
help: 'Pass --harness as a comma-separated list of the supported names.',
|
|
54
|
+
cause: error,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -74,6 +74,10 @@ export const depPluginsCommand: cli.Command = command({
|
|
|
74
74
|
type: t.boolean().optional(),
|
|
75
75
|
},
|
|
76
76
|
},
|
|
77
|
+
examples: [
|
|
78
|
+
{ command: 'buddy-agent-harness dep-plugins', description: 'Write the marketplace catalog for dependency plugins' },
|
|
79
|
+
{ command: 'buddy-agent-harness dep-plugins --check', description: 'Fail when the catalog is stale (CI)' },
|
|
80
|
+
],
|
|
77
81
|
run(args) {
|
|
78
82
|
try {
|
|
79
83
|
const root = args.root ?? process.cwd()
|
|
@@ -72,6 +72,12 @@ export type GlobalInstructionProblem =
|
|
|
72
72
|
| 'global-instructions-overridden'
|
|
73
73
|
| 'global-instructions-emptied'
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* What a repository lacks for the git worktrees Claude Code creates under `.claude/worktrees/`;
|
|
77
|
+
* each is an offer `init` and `enhance` make, never a fault in a file the owner wrote.
|
|
78
|
+
*/
|
|
79
|
+
export type WorktreeProblem = 'worktrees-untracked' | 'worktrees-scanned' | 'worktree-base-unset'
|
|
80
|
+
|
|
75
81
|
/** Everything `doctor` can report against, across every section. */
|
|
76
82
|
export type DoctorProblem =
|
|
77
83
|
| BridgeProblem
|
|
@@ -80,6 +86,7 @@ export type DoctorProblem =
|
|
|
80
86
|
| ConfigurationFault
|
|
81
87
|
| McpProblem
|
|
82
88
|
| NonstandardProblem
|
|
89
|
+
| WorktreeProblem
|
|
83
90
|
|
|
84
91
|
/** Alias kept for callers that name the whole set rather than one section. */
|
|
85
92
|
export type ConfigurationProblem = DoctorProblem
|
|
@@ -192,6 +199,7 @@ export const pnpmSkillRef = (invocation: string): string =>
|
|
|
192
199
|
`\`${invocation}\` (pnpm install: \`pnpm-${commandInvocation}-${invocation.slice(invocation.indexOf(':') + 1)}\`)`
|
|
193
200
|
|
|
194
201
|
export const initSkillRef: string = pnpmSkillRef(initSkillInvocation)
|
|
202
|
+
export const enhanceSkillRef: string = pnpmSkillRef('/buddy-agent-harness:enhance')
|
|
195
203
|
export const repairSkillRef: string = pnpmSkillRef(repairSkillInvocation)
|
|
196
204
|
|
|
197
205
|
/** Where the user authors the golden MCP server set, named in every MCP repair that points at it. */
|
|
@@ -589,6 +597,38 @@ const nonstandardTable: Record<NonstandardProblem, RepairRow> = {
|
|
|
589
597
|
|
|
590
598
|
export const nonstandardRepairs: readonly Repair[] = repairsOf(nonstandardTable)
|
|
591
599
|
|
|
600
|
+
/** Who offers every worktree repair, named the same way in each row. */
|
|
601
|
+
const worktreeOffer = `${initSkillRef} offers it, or ${enhanceSkillRef} where the repository already has an AGENTS.md`
|
|
602
|
+
|
|
603
|
+
const worktreeTable: Record<WorktreeProblem, RepairRow> = {
|
|
604
|
+
'worktrees-untracked': {
|
|
605
|
+
detail:
|
|
606
|
+
'.claude/worktrees/ is not git-ignored, so every agent worktree Claude Code creates there shows up as untracked in git status',
|
|
607
|
+
repair: ({ file }) => ({ command: '', instruction: `add \`.claude/worktrees/\` to ${file} — ${worktreeOffer}` }),
|
|
608
|
+
skillRepair: () => `run ${initSkillRef} or ${enhanceSkillRef}, which offers the .gitignore line`,
|
|
609
|
+
},
|
|
610
|
+
'worktrees-scanned': {
|
|
611
|
+
detail:
|
|
612
|
+
'this config does not exclude .claude/worktrees, so the tool walks into each agent worktree, a whole checkout with its own config, and Biome stops on the nested root configuration',
|
|
613
|
+
repair: ({ file }) => ({
|
|
614
|
+
command: '',
|
|
615
|
+
instruction: `add \`"!**/.claude/worktrees"\` to \`files.includes\` in ${file} — ${worktreeOffer}`,
|
|
616
|
+
}),
|
|
617
|
+
skillRepair: () => `run ${initSkillRef} or ${enhanceSkillRef}, which offers the exclusion`,
|
|
618
|
+
},
|
|
619
|
+
'worktree-base-unset': {
|
|
620
|
+
detail:
|
|
621
|
+
'worktree.baseRef is unset, so agent worktrees branch from the default branch rather than the current HEAD — work on a feature branch starts without its commits',
|
|
622
|
+
repair: ({ file }) => ({
|
|
623
|
+
command: '',
|
|
624
|
+
instruction: `decide whether agent worktrees should branch from HEAD, and if so set \`"worktree": { "baseRef": "head" }\` in ${file} — ${worktreeOffer}`,
|
|
625
|
+
}),
|
|
626
|
+
skillRepair: () => `run ${initSkillRef} or ${enhanceSkillRef}, which explains the trade-off and offers the setting`,
|
|
627
|
+
},
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export const worktreeRepairs: readonly Repair[] = repairsOf(worktreeTable)
|
|
631
|
+
|
|
592
632
|
/**
|
|
593
633
|
* Annotated over the whole union rather than inferred, so an added problem fails to compile until
|
|
594
634
|
* its section carries it.
|
|
@@ -600,6 +640,7 @@ const doctorTable: Record<DoctorProblem, RepairRow> = {
|
|
|
600
640
|
...configurationTable,
|
|
601
641
|
...mcpTable,
|
|
602
642
|
...nonstandardTable,
|
|
643
|
+
...worktreeTable,
|
|
603
644
|
}
|
|
604
645
|
|
|
605
646
|
export const doctorRepairs: readonly Repair[] = repairsOf(doctorTable)
|
|
@@ -612,7 +653,7 @@ export function repairFor(problem: DoctorProblem): Repair {
|
|
|
612
653
|
export const doctorSkill = {
|
|
613
654
|
name: 'doctor-buddy-agent-harness',
|
|
614
655
|
description:
|
|
615
|
-
'
|
|
656
|
+
"Checks whether a repository's agent configuration bridges into .claude/skills and the other harness files still resolve — when a repository loads no project skills, when skills are missing after a clone, or when a harness appears to be ignoring AGENTS.md.",
|
|
616
657
|
} as const
|
|
617
658
|
|
|
618
659
|
/** Written into the generated skill so a reader knows not to edit it in place. */
|
|
@@ -699,6 +740,7 @@ Every \`problem\` name routes to exactly one page. Load the page for the finding
|
|
|
699
740
|
| \`references/configuration.md\` | a finding about the configuration around the bridges rather than a bridge |
|
|
700
741
|
| \`references/mcp.md\` | any finding whose path is an MCP locator — **always** before acting on a credential finding |
|
|
701
742
|
| \`references/nonstandard.md\` | a finding about configuration only one harness can read |
|
|
743
|
+
| \`references/worktrees.md\` | a finding about the agent worktrees Claude Code creates under \`.claude/worktrees/\` |
|
|
702
744
|
| \`references/harnesses/<name>.md\` | the paths and files one named harness uses |
|
|
703
745
|
|
|
704
746
|
## Rules
|
|
@@ -709,6 +751,12 @@ Every \`problem\` name routes to exactly one page. Load the page for the finding
|
|
|
709
751
|
- Never repeat a value from a file an \`mcp-literal-secret\` or \`mcp-committed-secret\` finding points at. The report withheld it on purpose, and quoting it back puts it in the transcript anyway.
|
|
710
752
|
- Never write outside the repository. A \`globalInstructions\` repair is a step for the user to take in their own home directory; hand it over exactly as reported.
|
|
711
753
|
- Write instructions in \`AGENTS.md\`, never in \`CLAUDE.md\`. Content written there reaches one harness, drifts from the canonical file, and — because Claude Code prefers it — takes \`AGENTS.md\` out of that harness's context entirely.
|
|
754
|
+
|
|
755
|
+
## Done when
|
|
756
|
+
|
|
757
|
+
- **C1.** The reply names each problem the report found and its cause, or, when the report found none, says the configuration is healthy and calls for no change.
|
|
758
|
+
- **C2.** No bridge, skill, or instruction file changed: no \`init\` ran, and nothing was written to \`CLAUDE.md\`.
|
|
759
|
+
- **C3.** The reply repeats no value that a secret finding withheld.
|
|
712
760
|
`
|
|
713
761
|
}
|
|
714
762
|
|
|
@@ -988,6 +1036,23 @@ ${repairTable(nonstandardRepairs)}
|
|
|
988
1036
|
A harness directory that is a **symlink** is a projection someone already made, not configuration authored here. An artifact is reported whether or not its harness is enabled: a \`.cursorrules\` in a repository nobody opens in Cursor is still instruction content \`AGENTS.md\` does not carry, and filtering by the enabled set would hide exactly the drift worth converting.
|
|
989
1037
|
|
|
990
1038
|
Hooks, LSP settings, and output styles are not covered yet. Their event names and shapes differ by harness with no safe projection, and half-reporting them would be worse than the silence.
|
|
1039
|
+
`,
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
path: 'references/worktrees.md',
|
|
1043
|
+
content: `${generatedSkillWarning}
|
|
1044
|
+
|
|
1045
|
+
# Agent worktree findings
|
|
1046
|
+
|
|
1047
|
+
Claude Code runs a session started with \`--worktree\`, and a subagent isolated in a worktree, in a git worktree under \`.claude/worktrees/<name>/\` inside the repository. That place is by design, and no setting moves it. These findings are what a repository needs so the worktrees there do no harm. \`doctor\` checks them only where a \`.claude\` directory exists.
|
|
1048
|
+
|
|
1049
|
+
${repairTable(worktreeRepairs)}
|
|
1050
|
+
|
|
1051
|
+
None of these is a fault in a file the owner wrote, and none is a \`repair\` correction. Each is an offer the \`init-buddy-agent-harness\` and \`enhance\` skills make, one at a time, and write only on approval.
|
|
1052
|
+
|
|
1053
|
+
\`worktree-base-unset\` is a choice, not an error. The default branch suits agents given unrelated tasks; \`"head"\` suits agents asked to build on the branch in front of them. Put the trade-off to the owner, and report an explicit \`"fresh"\` as their answer rather than as a finding — \`doctor\` already does.
|
|
1054
|
+
|
|
1055
|
+
\`worktrees-scanned\` is checked for Biome only, the one tool with a confirmed failure. A repository whose ESLint, Prettier, knip, or TypeScript configuration walks the tree can hit the same thing; the skills look for those when they make the offer.
|
|
991
1056
|
`,
|
|
992
1057
|
},
|
|
993
1058
|
...harnessRegistry.map((harness) => harnessPage(harness, initReferences.has(harness.name))),
|
|
@@ -3,11 +3,11 @@ import type { cli } from 'clibuilder'
|
|
|
3
3
|
import { command, t } from 'clibuilder'
|
|
4
4
|
import { result } from 'clibuilder/axi'
|
|
5
5
|
import { listReferences, type ReferenceListLayerRow } from 'cyber-agentkit'
|
|
6
|
-
import { collapseHome, displayBinPath, fail } from '../command-output/command-output.ts'
|
|
6
|
+
import { collapseHome, displayBinPath, fail, parseHarnessOption } from '../command-output/command-output.ts'
|
|
7
7
|
import { type ConfigurationFinding, diagnoseConfiguration } from '../diagnose-configuration/diagnose-configuration.ts'
|
|
8
8
|
import { diagnoseMcp } from '../diagnose-mcp/diagnose-mcp.ts'
|
|
9
9
|
import { diagnoseNonstandard } from '../diagnose-nonstandard/diagnose-nonstandard.ts'
|
|
10
|
-
import {
|
|
10
|
+
import { diagnoseWorktrees } from '../diagnose-worktrees/diagnose-worktrees.ts'
|
|
11
11
|
import { referenceLayers } from '../references/references.ts'
|
|
12
12
|
import { type DiagnoseResult, diagnoseBridges } from './diagnose-bridges.ts'
|
|
13
13
|
import { commandInvocation, type DoctorProblem, type RepairAction } from './doctor-guidance.ts'
|
|
@@ -82,56 +82,76 @@ export function buildDoctorReport(
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
options: {
|
|
90
|
-
root: {
|
|
91
|
-
description: 'Repository or package directory. Defaults to the current directory.',
|
|
92
|
-
type: t.string().optional(),
|
|
93
|
-
},
|
|
94
|
-
harness: {
|
|
95
|
-
description: 'Comma-separated harnesses to check in addition to Claude Code and Cursor, e.g. codex,gemini-cli.',
|
|
96
|
-
type: t.string().optional(),
|
|
97
|
-
},
|
|
85
|
+
const doctorOptions = {
|
|
86
|
+
root: {
|
|
87
|
+
description: 'Repository or package directory. Defaults to the current directory.',
|
|
88
|
+
type: t.string().optional(),
|
|
98
89
|
},
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
90
|
+
harness: {
|
|
91
|
+
description: 'Comma-separated harnesses to check in addition to Claude Code and Cursor, e.g. codex,gemini-cli.',
|
|
92
|
+
type: t.string().optional(),
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const doctorDescription =
|
|
97
|
+
"Report whether this repository's harness bridges still resolve — skills into .agents/skills, instructions into AGENTS.md — and whether each harness's user-scope file loads ~/.agents/AGENTS.md. Read-only."
|
|
98
|
+
|
|
99
|
+
/** The live report `doctor` answers with, shared with the bare invocation of the CLI. */
|
|
100
|
+
export async function diagnoseReport(args: {
|
|
101
|
+
root?: string | undefined
|
|
102
|
+
harness?: string | undefined
|
|
103
|
+
}): Promise<DoctorReport> {
|
|
104
|
+
try {
|
|
105
|
+
const harnesses = parseHarnessOption(args.harness)
|
|
106
|
+
const root = args.root ?? process.cwd()
|
|
107
|
+
const home = homedir()
|
|
108
|
+
const diagnosis = diagnoseBridges({
|
|
109
|
+
root,
|
|
110
|
+
...(harnesses.length ? { harnesses } : {}),
|
|
111
|
+
cli: commandInvocation,
|
|
112
|
+
home,
|
|
113
|
+
env: process.env,
|
|
114
|
+
})
|
|
115
|
+
const git = new GitBridgeState(root)
|
|
116
|
+
const configuration = [
|
|
117
|
+
...diagnoseConfiguration({ root, git, cli: commandInvocation }),
|
|
118
|
+
...diagnoseMcp({ root, git, cli: commandInvocation }),
|
|
119
|
+
...diagnoseNonstandard({ root, cli: commandInvocation }),
|
|
120
|
+
...diagnoseWorktrees({ root, git, cli: commandInvocation }),
|
|
121
|
+
]
|
|
122
|
+
const { layers: rows } = listReferences(
|
|
123
|
+
await referenceLayers({
|
|
105
124
|
root,
|
|
106
|
-
...(harnesses.length ? { harnesses } : {}),
|
|
107
|
-
cli: commandInvocation,
|
|
108
125
|
home,
|
|
126
|
+
platform: process.platform,
|
|
109
127
|
env: process.env,
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
128
|
+
}),
|
|
129
|
+
)
|
|
130
|
+
// Resolved across every tier so each status matches `reference list`, but plugin rows are
|
|
131
|
+
// left out: what a plugin ships is its own business, not a repository setting.
|
|
132
|
+
const held = rows
|
|
133
|
+
.filter(({ tier }) => tier !== 'plugin')
|
|
134
|
+
.map(({ name, tier, path, outcome }) => ({ name, tier, path: collapseHome(home, path), status: outcome }))
|
|
135
|
+
return buildDoctorReport(displayBinPath(home, process.argv[1]), diagnosis, configuration, held)
|
|
136
|
+
} catch (error) {
|
|
137
|
+
return fail(error, 'Harness diagnosis failed.')
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const doctorCommand: cli.Command = command({
|
|
142
|
+
name: 'doctor',
|
|
143
|
+
description: doctorDescription,
|
|
144
|
+
options: doctorOptions,
|
|
145
|
+
examples: [
|
|
146
|
+
{ command: 'buddy-agent-harness doctor', description: 'Check the current repository' },
|
|
147
|
+
{
|
|
148
|
+
command: 'buddy-agent-harness doctor --harness codex,gemini-cli',
|
|
149
|
+
description: 'Also check Codex and Gemini CLI',
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
// Exit stays 0 even with findings: a non-zero code reads to an agent as "this command
|
|
153
|
+
// is broken".
|
|
154
|
+
async run(args) {
|
|
155
|
+
return result(await diagnoseReport(args))
|
|
136
156
|
},
|
|
137
157
|
})
|
|
@@ -37,6 +37,11 @@ export class GitBridgeState {
|
|
|
37
37
|
this.repoRootPrefix = git(root, ['rev-parse', '--show-prefix'])?.trim()
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** Whether `root` is inside a git work tree at all; a check that needs git's answer skips when not. */
|
|
41
|
+
isRepository(): boolean {
|
|
42
|
+
return this.repoRootPrefix !== undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
/** The skip-worktree bit is a hint some checkouts clear, so it is verified rather than assumed. */
|
|
41
46
|
trackingOf(path: string): TrackingState {
|
|
42
47
|
if (this.repoRootPrefix === undefined) return 'untracked'
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { type RepairAction, repairFor, type WorktreeProblem } from '../diagnose-bridges/doctor-guidance.ts'
|
|
4
|
+
import type { GitBridgeState } from '../diagnose-bridges/git-bridge-state.ts'
|
|
5
|
+
import { isRecord } from '../is-record/is-record.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The agent-worktree half of `doctor`: what a repository lacks for the git worktrees Claude Code
|
|
9
|
+
* creates inside it under `.claude/worktrees/`. Read-only.
|
|
10
|
+
*/
|
|
11
|
+
export type WorktreeFinding = {
|
|
12
|
+
/** Repository-relative path the finding is about. */
|
|
13
|
+
path: string
|
|
14
|
+
problem: WorktreeProblem
|
|
15
|
+
detail: string
|
|
16
|
+
/** Always an empty `command`: each repair is an offer the owner approves. */
|
|
17
|
+
repair: RepairAction
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type DiagnoseWorktreesOptions = {
|
|
21
|
+
root: string
|
|
22
|
+
git: GitBridgeState
|
|
23
|
+
/** How to name this tool in the repair commands. */
|
|
24
|
+
cli: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The trailing slash matters: `git check-ignore` matches a directory rule only against a directory path. */
|
|
28
|
+
export const worktreeDirectory = '.claude/worktrees/'
|
|
29
|
+
|
|
30
|
+
const biomeConfigs = ['biome.json', 'biome.jsonc']
|
|
31
|
+
const claudeSettings = '.claude/settings.json'
|
|
32
|
+
|
|
33
|
+
/** `undefined` when the file is missing or does not parse; either way Claude Code reads no base from it. */
|
|
34
|
+
function readJson(path: string): unknown {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(readFileSync(path, 'utf8'))
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Checked only where `.claude/` exists, the sign the repository is worked in with Claude Code.
|
|
44
|
+
* Biome is the one tool checked: it is the confirmed failure, and the others are offered by the
|
|
45
|
+
* skills without a finding.
|
|
46
|
+
*/
|
|
47
|
+
export function diagnoseWorktrees({ root, git, cli }: DiagnoseWorktreesOptions): WorktreeFinding[] {
|
|
48
|
+
if (!existsSync(join(root, '.claude'))) return []
|
|
49
|
+
|
|
50
|
+
const findings: WorktreeFinding[] = []
|
|
51
|
+
const add = (path: string, problem: WorktreeProblem) => {
|
|
52
|
+
const { detail, repair } = repairFor(problem)
|
|
53
|
+
findings.push({ path, problem, detail, repair: repair({ file: path }, cli) })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (git.isRepository() && !git.isIgnored(worktreeDirectory)) add('.gitignore', 'worktrees-untracked')
|
|
57
|
+
|
|
58
|
+
for (const config of biomeConfigs) {
|
|
59
|
+
const path = join(root, config)
|
|
60
|
+
if (existsSync(path) && !readFileSync(path, 'utf8').includes('.claude/worktrees')) add(config, 'worktrees-scanned')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const settings = readJson(join(root, claudeSettings))
|
|
64
|
+
const worktree = isRecord(settings) ? settings['worktree'] : undefined
|
|
65
|
+
// An explicit `"fresh"` is the owner's choice; only an unset key is reported.
|
|
66
|
+
if (!isRecord(worktree) || worktree['baseRef'] === undefined) add(claudeSettings, 'worktree-base-unset')
|
|
67
|
+
|
|
68
|
+
return findings
|
|
69
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from 'node:os'
|
|
2
2
|
import type { cli } from 'clibuilder'
|
|
3
3
|
import { command, t } from 'clibuilder'
|
|
4
|
-
import { CliError, document,
|
|
4
|
+
import { CliError, document, ref, result } from 'clibuilder/axi'
|
|
5
5
|
import { collapseHome, fail } from '../command-output/command-output.ts'
|
|
6
6
|
import {
|
|
7
7
|
type GovernanceLayer,
|
|
@@ -67,6 +67,7 @@ export const governanceListCommand: cli.Command = command({
|
|
|
67
67
|
root: rootOption,
|
|
68
68
|
'overrides-only': overridesOnlyOption,
|
|
69
69
|
},
|
|
70
|
+
examples: [{ command: 'buddy-agent-harness governance list', description: 'List the governances the layers hold' }],
|
|
70
71
|
run(args: Args) {
|
|
71
72
|
process.stderr.write(GOVERNANCE_DEPRECATED)
|
|
72
73
|
try {
|
|
@@ -98,6 +99,9 @@ export const governanceShowCommand: cli.Command = command({
|
|
|
98
99
|
root: rootOption,
|
|
99
100
|
'overrides-only': overridesOnlyOption,
|
|
100
101
|
},
|
|
102
|
+
examples: [
|
|
103
|
+
{ command: 'buddy-agent-harness governance show skill-design', description: 'Print one governance document' },
|
|
104
|
+
],
|
|
101
105
|
run(args: Args & { name: string }) {
|
|
102
106
|
const format = this.format
|
|
103
107
|
process.stderr.write(GOVERNANCE_DEPRECATED)
|
|
@@ -105,15 +109,16 @@ export const governanceShowCommand: cli.Command = command({
|
|
|
105
109
|
const name = parseGovernanceName(args.name)
|
|
106
110
|
const found = resolveGovernance(name, layersFor(args))
|
|
107
111
|
if (!found) {
|
|
108
|
-
|
|
109
|
-
// prose
|
|
110
|
-
// mixed in.
|
|
111
|
-
process.stderr.write(
|
|
112
|
+
throw new CliError(
|
|
112
113
|
args['overrides-only']
|
|
113
|
-
? `
|
|
114
|
-
: `
|
|
114
|
+
? `no override for governance "${name}" in the project, user, or machine-wide layer.`
|
|
115
|
+
: `no governance named "${name}" in any layer.`,
|
|
116
|
+
{
|
|
117
|
+
code: 'governance-not-found',
|
|
118
|
+
details: { name },
|
|
119
|
+
help: ref('reference list', {}, 'see what is available'),
|
|
120
|
+
},
|
|
115
121
|
)
|
|
116
|
-
throw CliError.reported(exitCodes.error)
|
|
117
122
|
}
|
|
118
123
|
const report: GovernanceShowReport = {
|
|
119
124
|
name: found.name,
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { cli } from 'clibuilder'
|
|
2
2
|
import { command, t } from 'clibuilder'
|
|
3
3
|
import { result } from 'clibuilder/axi'
|
|
4
|
-
import { fail } from '../command-output/command-output.ts'
|
|
4
|
+
import { fail, parseHarnessOption } from '../command-output/command-output.ts'
|
|
5
5
|
import { doctorCommand } from '../diagnose-bridges/doctor.command.ts'
|
|
6
6
|
import { governanceCommand } from '../governance-overrides/governance.command.ts'
|
|
7
|
-
import { parseHarnesses } from '../harness-registry/harness-registry.ts'
|
|
8
7
|
import { referenceCommand } from '../references/references.ts'
|
|
9
8
|
import { parseForce } from '../skill-projection/skill-projection.ts'
|
|
10
9
|
import { initializeHarnesses } from './initialize-harnesses.ts'
|
|
@@ -31,9 +30,16 @@ export const initCommand: cli.Command = command({
|
|
|
31
30
|
type: t.string().optional(),
|
|
32
31
|
},
|
|
33
32
|
},
|
|
33
|
+
examples: [
|
|
34
|
+
{ command: 'buddy-agent-harness init', description: 'Set up skills for Claude Code and Cursor' },
|
|
35
|
+
{
|
|
36
|
+
command: 'buddy-agent-harness init --harness codex --copy',
|
|
37
|
+
description: 'Also enable Codex, copying skills instead of linking',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
34
40
|
run(args) {
|
|
35
41
|
try {
|
|
36
|
-
const harnesses =
|
|
42
|
+
const harnesses = parseHarnessOption(args.harness)
|
|
37
43
|
return result(
|
|
38
44
|
initializeHarnesses({
|
|
39
45
|
root: args.root ?? process.cwd(),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { cli } from 'clibuilder'
|
|
2
2
|
import { command, t } from 'clibuilder'
|
|
3
|
-
import { CliError,
|
|
3
|
+
import { CliError, empty, ref, result } from 'clibuilder/axi'
|
|
4
4
|
import { fail } from '../command-output/command-output.ts'
|
|
5
5
|
import { GitBridgeState } from '../diagnose-bridges/git-bridge-state.ts'
|
|
6
6
|
import { projectionRecordPath } from '../diagnose-mcp/mcp-baseline.ts'
|
|
@@ -32,6 +32,10 @@ export const mcpProjectCommand: cli.Command = command({
|
|
|
32
32
|
type: t.boolean().optional(),
|
|
33
33
|
},
|
|
34
34
|
},
|
|
35
|
+
examples: [
|
|
36
|
+
{ command: 'buddy-agent-harness mcp project', description: 'Preview what would be projected' },
|
|
37
|
+
{ command: 'buddy-agent-harness mcp project --write', description: 'Project the golden set into each harness' },
|
|
38
|
+
],
|
|
35
39
|
run(args) {
|
|
36
40
|
try {
|
|
37
41
|
const root = args.root ?? process.cwd()
|
|
@@ -40,16 +44,18 @@ export const mcpProjectCommand: cli.Command = command({
|
|
|
40
44
|
if (plan.kind === 'unreadable') {
|
|
41
45
|
// Position only: the parser's message quotes the line, and that line may hold a credential.
|
|
42
46
|
const at = plan.position ? ` at line ${plan.position.line}, column ${plan.position.column}` : ''
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
throw new CliError(`${goldenSetPath} does not parse${at}.`, {
|
|
48
|
+
code: 'golden-set-unreadable',
|
|
49
|
+
help: `Fix the TOML syntax in ${goldenSetPath}, then re-run.`,
|
|
50
|
+
})
|
|
45
51
|
}
|
|
46
52
|
if (plan.kind === 'absent') {
|
|
47
|
-
return
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
+
return empty(`0 servers — no golden set at ${goldenSetPath}`, {
|
|
54
|
+
help: [
|
|
55
|
+
`Create ${goldenSetPath} with the MCP servers to project.`,
|
|
56
|
+
ref('mcp project', {}, 'preview the projection'),
|
|
57
|
+
],
|
|
58
|
+
})
|
|
53
59
|
}
|
|
54
60
|
const report: ProjectionReport = {
|
|
55
61
|
golden: goldenSetPath,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { cli } from 'clibuilder'
|
|
2
2
|
import { command, t } from 'clibuilder'
|
|
3
|
-
import { CliError,
|
|
3
|
+
import { CliError, empty, ref, result } from 'clibuilder/axi'
|
|
4
4
|
import { fail } from '../command-output/command-output.ts'
|
|
5
5
|
import { GitBridgeState } from '../diagnose-bridges/git-bridge-state.ts'
|
|
6
6
|
import { projectionRecordPath } from '../diagnose-mcp/mcp-baseline.ts'
|
|
@@ -30,6 +30,10 @@ export const mcpReconcileCommand: cli.Command = command({
|
|
|
30
30
|
type: t.array(t.string()).optional(),
|
|
31
31
|
},
|
|
32
32
|
},
|
|
33
|
+
examples: [
|
|
34
|
+
{ command: 'buddy-agent-harness mcp reconcile', description: 'List harness-side changes the golden set lacks' },
|
|
35
|
+
{ command: 'buddy-agent-harness mcp reconcile --accept <path>', description: 'Import one approved field' },
|
|
36
|
+
],
|
|
33
37
|
run(args) {
|
|
34
38
|
try {
|
|
35
39
|
const root = args.root ?? process.cwd()
|
|
@@ -38,20 +42,22 @@ export const mcpReconcileCommand: cli.Command = command({
|
|
|
38
42
|
if (plan.kind === 'unreadable') {
|
|
39
43
|
// Position only: the parser's message quotes the line, and that line may hold a credential.
|
|
40
44
|
const at = plan.position ? ` at line ${plan.position.line}, column ${plan.position.column}` : ''
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
throw new CliError(`${goldenSetPath} does not parse${at}.`, {
|
|
46
|
+
code: 'golden-set-unreadable',
|
|
47
|
+
help: `Fix the TOML syntax in ${goldenSetPath}, then re-run.`,
|
|
48
|
+
})
|
|
43
49
|
}
|
|
44
50
|
if (plan.kind === 'rejected') {
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
throw new CliError('nothing written.', {
|
|
52
|
+
code: 'accept-rejected',
|
|
53
|
+
details: { reasons: plan.reasons },
|
|
54
|
+
help: ref('mcp reconcile', {}, 'list the fields that can be approved'),
|
|
55
|
+
})
|
|
47
56
|
}
|
|
48
57
|
if (plan.kind === 'absent') {
|
|
49
|
-
return
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
fields: '0 fields',
|
|
53
|
-
record: 'not written',
|
|
54
|
-
} satisfies ReconcileReport)
|
|
58
|
+
return empty(`0 servers — no golden set at ${goldenSetPath}`, {
|
|
59
|
+
help: `Create ${goldenSetPath} first; there is nothing to reconcile into.`,
|
|
60
|
+
})
|
|
55
61
|
}
|
|
56
62
|
const report: ReconcileReport = {
|
|
57
63
|
golden: goldenSetPath,
|