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.
Files changed (47) 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/CHANGELOG.md +44 -0
  5. package/dist/cli.mjs +2166 -702
  6. package/dist/index.d.mts +10 -3
  7. package/dist/index.d.mts.map +1 -1
  8. package/dist/index.mjs +597 -431
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/skill-scripts/doctor.mjs +74 -67
  11. package/dist/skill-scripts/init.mjs +74 -67
  12. package/dist/skill-scripts/mcp.mjs +74 -67
  13. package/llms.txt +8 -6
  14. package/package.json +5 -4
  15. package/plugin.json +1 -1
  16. package/skills/doctor-buddy-agent-harness/README.md +1 -1
  17. package/skills/doctor-buddy-agent-harness/SKILL.md +9 -2
  18. package/skills/doctor-buddy-agent-harness/references/worktrees.md +17 -0
  19. package/skills/doctor-buddy-agent-harness/scripts/doctor.mjs +74 -67
  20. package/skills/enhance/SKILL.md +18 -10
  21. package/skills/enhance/references/agent-worktrees.md +59 -0
  22. package/skills/enhance/references/list-identifiers.history.md +9 -1
  23. package/skills/enhance/references/list-identifiers.md +6 -9
  24. package/skills/enhance/references/references-as-links.history.md +9 -0
  25. package/skills/enhance/references/references-as-links.md +76 -0
  26. package/skills/enhance/scripts/doctor.mjs +74 -67
  27. package/skills/init-buddy-agent-harness/SKILL.md +8 -5
  28. package/skills/init-buddy-agent-harness/references/agent-worktrees.md +59 -0
  29. package/skills/init-buddy-agent-harness/scripts/doctor.mjs +74 -67
  30. package/skills/init-buddy-agent-harness/scripts/init.mjs +74 -67
  31. package/skills/repair/README.md +2 -2
  32. package/skills/repair/SKILL.md +3 -3
  33. package/skills/repair/scripts/doctor.mjs +74 -67
  34. package/skills/repair/scripts/mcp.mjs +74 -67
  35. package/src/cli.ts +13 -3
  36. package/src/command-output/command-output.ts +28 -5
  37. package/src/dep-plugins/dep-plugins.command.ts +4 -0
  38. package/src/diagnose-bridges/doctor-guidance.ts +66 -1
  39. package/src/diagnose-bridges/doctor.command.ts +69 -49
  40. package/src/diagnose-bridges/git-bridge-state.ts +5 -0
  41. package/src/diagnose-worktrees/diagnose-worktrees.ts +69 -0
  42. package/src/governance-overrides/governance.command.ts +13 -8
  43. package/src/index.ts +1 -0
  44. package/src/initialize-harnesses/init.command.ts +9 -3
  45. package/src/project-mcp/mcp.command.ts +15 -9
  46. package/src/reconcile-mcp/reconcile.command.ts +17 -11
  47. 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 }).command(first),
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
- * Reports a failure on stderr, not stdout — stdout carries what an agent parses — and fails the cli
6
- * with the exit code. A `CliError` the cli already reports is rethrown untouched.
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
- process.stderr.write(`error: ${error instanceof Error ? error.message : fallback}\n`)
11
- throw CliError.reported(exitCodes.error)
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
- 'Use this skill when a repository loads no project skills, when skills are missing after a clone, when a harness appears to be ignoring AGENTS.md, or when checking whether the agent configuration bridges into .claude/skills and the other harness files still resolve.',
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 { parseHarnesses } from '../harness-registry/harness-registry.ts'
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
- export const doctorCommand: cli.Command = command({
86
- name: 'doctor',
87
- description:
88
- "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.",
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
- async run(args) {
100
- try {
101
- const harnesses = parseHarnesses(args.harness)
102
- const root = args.root ?? process.cwd()
103
- const home = homedir()
104
- const diagnosis = diagnoseBridges({
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
- const git = new GitBridgeState(root)
112
- const configuration = [
113
- ...diagnoseConfiguration({ root, git, cli: commandInvocation }),
114
- ...diagnoseMcp({ root, git, cli: commandInvocation }),
115
- ...diagnoseNonstandard({ root, cli: commandInvocation }),
116
- ]
117
- const { layers: rows } = listReferences(
118
- await referenceLayers({
119
- root,
120
- home,
121
- platform: process.platform,
122
- env: process.env,
123
- }),
124
- )
125
- // Resolved across every tier so each status matches `reference list`, but plugin rows are
126
- // left out: what a plugin ships is its own business, not a repository setting.
127
- const held = rows
128
- .filter(({ tier }) => tier !== 'plugin')
129
- .map(({ name, tier, path, outcome }) => ({ name, tier, path: collapseHome(home, path), status: outcome }))
130
- // Exit stays 0 even with findings: a non-zero code reads to an agent as "this command
131
- // is broken".
132
- return result(buildDoctorReport(displayBinPath(home, process.argv[1]), diagnosis, configuration, held))
133
- } catch (error) {
134
- return fail(error, 'Harness diagnosis failed.')
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, exitCodes, result } from 'clibuilder/axi'
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
- // stderr, not stdout — a caller piping the document off stdout must never see this
109
- // prose
110
- // mixed in.
111
- process.stderr.write(
112
+ throw new CliError(
112
113
  args['overrides-only']
113
- ? `error: no override for governance "${name}" in the project, user, or machine-wide layer.\n`
114
- : `error: no governance named "${name}" in any layer.\n`,
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
@@ -56,6 +56,7 @@ export type {
56
56
  InstructionProblem,
57
57
  McpProblem,
58
58
  Repair,
59
+ WorktreeProblem,
59
60
  } from './diagnose-bridges/doctor-guidance.ts'
60
61
  export {
61
62
  bridgeRepairs,
@@ -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 = parseHarnesses(args.harness)
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, exitCodes, result } from 'clibuilder/axi'
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
- process.stderr.write(`error: ${goldenSetPath} does not parse${at}.\n`)
44
- throw CliError.reported(exitCodes.error)
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 result({
48
- golden: `0 servers — no golden set at ${goldenSetPath}`,
49
- mode: 'nothing to project',
50
- actions: '0 changes',
51
- record: 'not written',
52
- } satisfies ProjectionReport)
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, exitCodes, result } from 'clibuilder/axi'
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
- process.stderr.write(`error: ${goldenSetPath} does not parse${at}.\n`)
42
- throw CliError.reported(exitCodes.error)
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
- process.stderr.write(`error: nothing written.\n${plan.reasons.map((reason) => `- ${reason}\n`).join('')}`)
46
- throw CliError.reported(exitCodes.error)
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 result({
50
- golden: `0 servers — no golden set at ${goldenSetPath}`,
51
- mode: 'nothing to reconcile into',
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,
@@ -25,4 +25,5 @@ export function referenceLayers({
25
25
 
26
26
  export const referenceCommand: cli.Command = createReferenceCommand({
27
27
  plugin: { name: PACKAGE_PLUGIN, root: dirname(packageGovernancesDir()) },
28
+ path: 'buddy-agent-harness reference',
28
29
  })