agentcohort 0.1.1 → 0.3.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/README.md +88 -12
- package/dist/args.d.ts +1 -0
- package/dist/args.js +9 -0
- package/dist/cli.js +45 -2
- package/dist/config.d.ts +32 -0
- package/dist/config.js +100 -0
- package/dist/configCmd.d.ts +27 -0
- package/dist/configCmd.js +54 -0
- package/dist/defaults.d.ts +21 -0
- package/dist/defaults.js +22 -0
- package/dist/diff.d.ts +24 -0
- package/dist/diff.js +64 -0
- package/dist/installer.d.ts +2 -0
- package/dist/installer.js +5 -1
- package/dist/promptModels.d.ts +13 -0
- package/dist/promptModels.js +66 -0
- package/dist/render.d.ts +11 -0
- package/dist/render.js +35 -0
- package/dist/templates/CLAUDE.section.md +49 -1
- package/dist/templates/agents/bug-fixer.md +14 -0
- package/dist/templates/agents/bug-hunter.md +14 -0
- package/dist/templates/agents/dispatcher.md +130 -0
- package/dist/templates/agents/expert-council.md +14 -0
- package/dist/templates/agents/feature-implementer.md +14 -0
- package/dist/templates/agents/feature-planner.md +14 -0
- package/dist/templates/agents/final-reviewer.md +14 -0
- package/dist/templates/agents/perf-optimizer.md +14 -0
- package/dist/templates/agents/perf-reviewer.md +14 -0
- package/dist/templates/agents/performance-hunter.md +14 -0
- package/dist/templates/agents/regression-guard.md +14 -0
- package/dist/templates/agents/repo-scout.md +14 -0
- package/dist/templates/agents/reproduction-engineer.md +14 -0
- package/dist/templates/agents/root-cause-analyst.md +14 -0
- package/dist/templates/agents/solution-architect.md +14 -0
- package/dist/templates/agents/test-verifier.md +14 -0
- package/dist/templates/commands/auto-flow.md +64 -28
- package/dist/templates/commands/quick-feature.md +55 -0
- package/dist/templates/commands/quick-fix.md +53 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -93,6 +93,20 @@ production-grade correctness, no shallow fixes, no fixing without evidence, and
|
|
|
93
93
|
**a bug audit never fixes** — it produces a recommendation and stops at a human
|
|
94
94
|
approval gate.
|
|
95
95
|
|
|
96
|
+
### How agents respect your CLAUDE.md and skills
|
|
97
|
+
|
|
98
|
+
Every installed agent boots by reading your project's `CLAUDE.md`
|
|
99
|
+
content **outside** the `# Agentcohort Routing Rules` section and by
|
|
100
|
+
checking for installed skills that match the current task. The rules:
|
|
101
|
+
|
|
102
|
+
- Your project rules take precedence over an agent's default prompt.
|
|
103
|
+
- An agent invokes a matching skill instead of re-implementing it.
|
|
104
|
+
- Agentcohort's defaults apply only where your project is silent.
|
|
105
|
+
|
|
106
|
+
This means `agentcohort` slots into a project that already has its own
|
|
107
|
+
CLAUDE.md and skills (e.g. `superpowers`) — it does not override what
|
|
108
|
+
you've already set up.
|
|
109
|
+
|
|
96
110
|
## Using the workflow commands (inside Claude Code)
|
|
97
111
|
|
|
98
112
|
| Command | Pipeline | Use it for |
|
|
@@ -113,6 +127,58 @@ approval gate.
|
|
|
113
127
|
- **Sonnet** — implementation, testing, bug & performance hunting.
|
|
114
128
|
- **Opus** — architecture, root-cause analysis, expert council, final review.
|
|
115
129
|
|
|
130
|
+
## Customizing model strategy
|
|
131
|
+
|
|
132
|
+
`agentcohort init` will (interactively) prompt you to either use the
|
|
133
|
+
default Claude model IDs or pick your own for each tier. Your choice is
|
|
134
|
+
saved to `.agentcohort.json` at the project root and reused on later
|
|
135
|
+
runs.
|
|
136
|
+
|
|
137
|
+
To revisit your choice without re-installing everything, run:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
agentcohort config
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
This re-prompts for the three tier model IDs, shows a diff of which
|
|
144
|
+
installed agents would change, and applies the changes with your
|
|
145
|
+
confirmation.
|
|
146
|
+
|
|
147
|
+
To force a re-prompt during install (instead of using the existing
|
|
148
|
+
`.agentcohort.json`), run:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
agentcohort init --reconfigure
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `.agentcohort.json` schema (v1)
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"$schema": "https://raw.githubusercontent.com/Thiendekaco/agentcohort/main/schema/agentcohort-config-v1.json",
|
|
159
|
+
"version": 1,
|
|
160
|
+
"models": {
|
|
161
|
+
"premium": "claude-opus-4-7",
|
|
162
|
+
"mid": "claude-sonnet-4-6",
|
|
163
|
+
"cheap": "claude-haiku-4-5-20251001"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- **premium** — architecture, root-cause analysis, expert council, final
|
|
169
|
+
review.
|
|
170
|
+
- **mid** — implementation, testing, bug & performance hunting.
|
|
171
|
+
- **cheap** — fast read-only repo scout.
|
|
172
|
+
|
|
173
|
+
Hand-editing one of the installed `.claude/agents/*.md` files to use a
|
|
174
|
+
specific model ID is respected: subsequent `agentcohort init` and
|
|
175
|
+
`agentcohort config` runs leave that hand-edit alone (the tool only
|
|
176
|
+
rewrites lines that are still tier aliases or match the previous
|
|
177
|
+
config's IDs).
|
|
178
|
+
|
|
179
|
+
Model IDs are not validated by the tool — if the ID is invalid, Claude
|
|
180
|
+
Code will fail at agent spawn time.
|
|
181
|
+
|
|
116
182
|
## Customizing agents
|
|
117
183
|
|
|
118
184
|
The installed files are plain Markdown and **yours to edit**:
|
|
@@ -146,7 +212,8 @@ before changing them (or back them up with `--backup`).
|
|
|
146
212
|
- **`--dry-run`** performs zero writes and zero backups.
|
|
147
213
|
- Backups are written next to the original as
|
|
148
214
|
`<file>.backup-YYYYMMDD-HHMMSS` and never overwrite an existing backup.
|
|
149
|
-
- Cross-platform (Windows/macOS/Linux)
|
|
215
|
+
- Cross-platform (Windows/macOS/Linux); a single runtime dependency
|
|
216
|
+
(`@inquirer/prompts` for the interactive model-tier prompt), no
|
|
150
217
|
shell-specific behavior.
|
|
151
218
|
|
|
152
219
|
## Development
|
|
@@ -157,27 +224,36 @@ npm run build # tsc -> dist/, then copies templates
|
|
|
157
224
|
npm test # vitest
|
|
158
225
|
```
|
|
159
226
|
|
|
160
|
-
##
|
|
227
|
+
## Branching & releases
|
|
228
|
+
|
|
229
|
+
Two long-lived branches:
|
|
230
|
+
|
|
231
|
+
- **`dev`** — integration / staging. All feature PRs target `dev`. Nothing
|
|
232
|
+
here publishes to npm; this is the place to bundle PRs together, run
|
|
233
|
+
manual smoke tests, and verify the release as a whole.
|
|
234
|
+
- **`main`** — production. Only ever updated by merging `dev` → `main`.
|
|
235
|
+
Every push to `main` triggers the [`Release`](.github/workflows/release.yml)
|
|
236
|
+
workflow.
|
|
161
237
|
|
|
162
|
-
|
|
163
|
-
gets published.** Every push to `main` runs the
|
|
164
|
-
[`Release`](.github/workflows/release.yml) workflow, which:
|
|
238
|
+
The workflow does:
|
|
165
239
|
|
|
166
240
|
1. installs, builds and runs the full test suite;
|
|
167
241
|
2. publishes the **current** `package.json` version to npm —
|
|
168
242
|
https://www.npmjs.com/package/agentcohort (so the very first
|
|
169
|
-
release
|
|
243
|
+
release was exactly `0.1.0`, nothing skipped);
|
|
170
244
|
3. creates the annotated git tag `vX.Y.Z` on the published commit;
|
|
171
245
|
4. bumps to the next dev version (`patch` by default) and pushes a
|
|
172
246
|
`chore(release): published vX.Y.Z, open vX.Y.(Z+1) [skip ci]` commit back
|
|
173
247
|
to `main`.
|
|
174
248
|
|
|
175
|
-
So
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
249
|
+
So the normal release cycle is: open PR → `dev` → review & merge → smoke
|
|
250
|
+
test on `dev` → open PR `dev` → `main` → merge → workflow publishes. To
|
|
251
|
+
ship a `minor`/`major` instead of `patch`, bump `package.json` yourself
|
|
252
|
+
in the PR before merging (or use the *Run workflow* button to control
|
|
253
|
+
how the **next** pending version is opened). If the pending version is
|
|
254
|
+
already on npm, publish is skipped and the job still succeeds (safe
|
|
255
|
+
re-runs). The `[skip ci]` marker stops the release commit from
|
|
256
|
+
re-triggering the workflow (no publish loop).
|
|
181
257
|
|
|
182
258
|
**One-time setup:** add an npm **Automation** access token as the repository
|
|
183
259
|
secret `NPM_TOKEN` (GitHub → Settings → Secrets and variables → Actions →
|
package/dist/args.d.ts
CHANGED
package/dist/args.js
CHANGED
|
@@ -9,6 +9,7 @@ const FLAGS = {
|
|
|
9
9
|
'--dry-run': 'dryRun',
|
|
10
10
|
'--force': 'force',
|
|
11
11
|
'--backup': 'backup',
|
|
12
|
+
'--reconfigure': 'reconfigure',
|
|
12
13
|
'--help': 'help',
|
|
13
14
|
'-h': 'help',
|
|
14
15
|
'--version': 'version',
|
|
@@ -22,6 +23,7 @@ function parseArgs(argv) {
|
|
|
22
23
|
dryRun: false,
|
|
23
24
|
force: false,
|
|
24
25
|
backup: false,
|
|
26
|
+
reconfigure: false,
|
|
25
27
|
help: false,
|
|
26
28
|
version: false,
|
|
27
29
|
unknown: [],
|
|
@@ -56,6 +58,9 @@ ${b('USAGE')}
|
|
|
56
58
|
${b('COMMANDS')}
|
|
57
59
|
init Install agents, workflow commands and routing rules
|
|
58
60
|
into ./.claude and ./CLAUDE.md of the current project.
|
|
61
|
+
config Re-prompt the model-tier strategy, show a diff of
|
|
62
|
+
any pending changes to installed agents, and apply
|
|
63
|
+
them with confirmation.
|
|
59
64
|
|
|
60
65
|
${b('OPTIONS')}
|
|
61
66
|
--yes, -y Non-interactive. Safe defaults: new files created;
|
|
@@ -67,6 +72,9 @@ ${b('OPTIONS')}
|
|
|
67
72
|
section without prompting (no backup unless --backup).
|
|
68
73
|
--backup Always back up a file before overwriting it.
|
|
69
74
|
Backup name: <file>.backup-YYYYMMDD-HHMMSS
|
|
75
|
+
--reconfigure (init only) Re-prompt model-tier strategy even if a
|
|
76
|
+
.agentcohort.json already exists. Requires a TTY;
|
|
77
|
+
not compatible with --yes.
|
|
70
78
|
--help, -h Show this help.
|
|
71
79
|
--version, -v Print the version.
|
|
72
80
|
|
|
@@ -75,6 +83,7 @@ ${b('WHAT GETS INSTALLED')}
|
|
|
75
83
|
reviewer, bug-hunter, root-cause-analyst, ...).
|
|
76
84
|
.claude/commands/ 7 workflow commands.
|
|
77
85
|
CLAUDE.md Appends a "# Agentcohort Routing Rules" section.
|
|
86
|
+
.agentcohort.json (Only when user customizes model-tier strategy.)
|
|
78
87
|
|
|
79
88
|
${b('WORKFLOW COMMANDS (run inside Claude Code)')}
|
|
80
89
|
/auto-flow Classify the task and pick the right workflow.
|
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.main = main;
|
|
5
|
+
const prompts_1 = require("@inquirer/prompts");
|
|
6
|
+
const core_1 = require("@inquirer/core");
|
|
5
7
|
const installer_1 = require("./installer");
|
|
6
8
|
const prompt_1 = require("./prompt");
|
|
9
|
+
const promptModels_1 = require("./promptModels");
|
|
10
|
+
const configCmd_1 = require("./configCmd");
|
|
11
|
+
const config_1 = require("./config");
|
|
7
12
|
const logger_1 = require("./logger");
|
|
8
13
|
const paths_1 = require("./paths");
|
|
9
14
|
const args_1 = require("./args");
|
|
@@ -55,11 +60,15 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
55
60
|
process.stdout.write((0, args_1.helpText)() + '\n');
|
|
56
61
|
return 0;
|
|
57
62
|
}
|
|
58
|
-
if (args.command !== 'init') {
|
|
63
|
+
if (args.command !== 'init' && args.command !== 'config') {
|
|
59
64
|
process.stderr.write((0, logger_1.paint)(`✗ Unknown command: ${args.command}\n`, 'red'));
|
|
60
65
|
process.stdout.write((0, args_1.helpText)() + '\n');
|
|
61
66
|
return 1;
|
|
62
67
|
}
|
|
68
|
+
if (args.reconfigure && (args.yes || args.force)) {
|
|
69
|
+
process.stderr.write((0, logger_1.paint)('✗ --reconfigure requires interactive mode (cannot combine with --yes or --force).\n', 'red'));
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
63
72
|
const stdinTTY = Boolean(process.stdin.isTTY);
|
|
64
73
|
const stdoutTTY = Boolean(process.stdout.isTTY);
|
|
65
74
|
const interactive = !args.yes && !args.force && !args.dryRun && stdinTTY && stdoutTTY;
|
|
@@ -73,10 +82,39 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
73
82
|
logger.info('Non-interactive environment detected — using safe defaults (like --yes).');
|
|
74
83
|
}
|
|
75
84
|
try {
|
|
85
|
+
const cwd = process.cwd();
|
|
86
|
+
if (args.command === 'config') {
|
|
87
|
+
if (!interactive) {
|
|
88
|
+
process.stderr.write((0, logger_1.paint)('✗ `agentcohort config` requires interactive mode (TTY). Edit .agentcohort.json directly to set models non-interactively.\n', 'red'));
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
const result = await (0, configCmd_1.runConfigCmd)({
|
|
92
|
+
cwd,
|
|
93
|
+
promptModelStrategy: promptModels_1.promptModelStrategy,
|
|
94
|
+
confirm: (message) => (0, prompts_1.confirm)({ message, default: true }),
|
|
95
|
+
});
|
|
96
|
+
const msg = {
|
|
97
|
+
'no-changes': 'No changes. Configuration is up to date.',
|
|
98
|
+
'no-agents': 'Config saved. No installed agents found — run `agentcohort init` to install.',
|
|
99
|
+
'cancelled': 'Cancelled. No changes made.',
|
|
100
|
+
'applied': `Applied ${result.changes.length} change(s) to installed agents.`,
|
|
101
|
+
}[result.status];
|
|
102
|
+
process.stdout.write(`${(0, logger_1.paint)('•', 'cyan')} ${msg}\n`);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
// command === 'init'
|
|
106
|
+
let existingConfig = (0, config_1.loadConfig)(cwd);
|
|
107
|
+
let models = (0, config_1.resolveModels)(existingConfig);
|
|
108
|
+
if (interactive && (existingConfig === null || args.reconfigure)) {
|
|
109
|
+
const newModels = await (0, promptModels_1.promptModelStrategy)(existingConfig?.models);
|
|
110
|
+
// Persist config BEFORE install so a partial install can be re-run idempotently.
|
|
111
|
+
(0, config_1.writeConfig)(cwd, { version: 1, models: newModels });
|
|
112
|
+
models = newModels;
|
|
113
|
+
}
|
|
76
114
|
if (interactive)
|
|
77
115
|
resolverHandle = (0, prompt_1.createInteractiveResolver)();
|
|
78
116
|
const result = await (0, installer_1.runInit)({
|
|
79
|
-
cwd
|
|
117
|
+
cwd,
|
|
80
118
|
yes: args.yes,
|
|
81
119
|
dryRun: args.dryRun,
|
|
82
120
|
force: args.force,
|
|
@@ -84,12 +122,17 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
84
122
|
interactive,
|
|
85
123
|
resolver: resolverHandle?.resolve,
|
|
86
124
|
logger,
|
|
125
|
+
models,
|
|
87
126
|
});
|
|
88
127
|
printSummary(result);
|
|
89
128
|
return 0;
|
|
90
129
|
}
|
|
91
130
|
catch (err) {
|
|
92
131
|
const message = err instanceof Error ? err.message : String(err);
|
|
132
|
+
if (err instanceof core_1.ExitPromptError) {
|
|
133
|
+
process.stderr.write((0, logger_1.paint)('\nCancelled. No changes made.\n', 'yellow'));
|
|
134
|
+
return 130;
|
|
135
|
+
}
|
|
93
136
|
process.stderr.write((0, logger_1.paint)(`\n✗ ${message}\n`, 'red'));
|
|
94
137
|
return 1;
|
|
95
138
|
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const CONFIG_FILENAME = ".agentcohort.json";
|
|
2
|
+
export interface ModelsConfig {
|
|
3
|
+
premium: string;
|
|
4
|
+
mid: string;
|
|
5
|
+
cheap: string;
|
|
6
|
+
}
|
|
7
|
+
export interface AgentcohortConfig {
|
|
8
|
+
version: 1;
|
|
9
|
+
models: ModelsConfig;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Validate that `raw` is a well-formed AgentcohortConfig. Returns the
|
|
13
|
+
* parsed config (with unknown top-level keys stripped). Throws on any
|
|
14
|
+
* violation.
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateConfig(raw: unknown): AgentcohortConfig;
|
|
17
|
+
/**
|
|
18
|
+
* Load `.agentcohort.json` from `projectRoot`. Returns null if absent.
|
|
19
|
+
* Throws a structured error if the file exists but is malformed.
|
|
20
|
+
*/
|
|
21
|
+
export declare function loadConfig(projectRoot: string): AgentcohortConfig | null;
|
|
22
|
+
/**
|
|
23
|
+
* Write `cfg` to `.agentcohort.json` in `projectRoot`. Includes a
|
|
24
|
+
* `$schema` field for editor autocomplete. Idempotent at the byte
|
|
25
|
+
* level: writing the same config twice produces identical bytes.
|
|
26
|
+
*/
|
|
27
|
+
export declare function writeConfig(projectRoot: string, cfg: AgentcohortConfig): void;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a ModelsConfig from a possibly-null config. When null,
|
|
30
|
+
* returns DEFAULT_MODELS.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveModels(cfg: AgentcohortConfig | null): ModelsConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CONFIG_FILENAME = void 0;
|
|
4
|
+
exports.validateConfig = validateConfig;
|
|
5
|
+
exports.loadConfig = loadConfig;
|
|
6
|
+
exports.writeConfig = writeConfig;
|
|
7
|
+
exports.resolveModels = resolveModels;
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const defaults_1 = require("./defaults");
|
|
11
|
+
exports.CONFIG_FILENAME = '.agentcohort.json';
|
|
12
|
+
const TIERS = ['premium', 'mid', 'cheap'];
|
|
13
|
+
function fail(reason) {
|
|
14
|
+
throw new Error(`Invalid .agentcohort.json: ${reason}. Expected schema: { version: 1, models: { premium: string, mid: string, cheap: string } }`);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Validate that `raw` is a well-formed AgentcohortConfig. Returns the
|
|
18
|
+
* parsed config (with unknown top-level keys stripped). Throws on any
|
|
19
|
+
* violation.
|
|
20
|
+
*/
|
|
21
|
+
function validateConfig(raw) {
|
|
22
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
23
|
+
fail('not an object');
|
|
24
|
+
}
|
|
25
|
+
const obj = raw;
|
|
26
|
+
if (obj.version !== 1) {
|
|
27
|
+
fail(`unsupported version ${JSON.stringify(obj.version)}; expected 1`);
|
|
28
|
+
}
|
|
29
|
+
const models = obj.models;
|
|
30
|
+
if (models === null || typeof models !== 'object' || Array.isArray(models)) {
|
|
31
|
+
fail('models must be an object');
|
|
32
|
+
}
|
|
33
|
+
const m = models;
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const tier of TIERS) {
|
|
36
|
+
const v = m[tier];
|
|
37
|
+
if (typeof v !== 'string') {
|
|
38
|
+
fail(`models.${tier} must be a string`);
|
|
39
|
+
}
|
|
40
|
+
if (v.trim().length === 0) {
|
|
41
|
+
fail(`models.${tier} must be a non-empty, non-whitespace string`);
|
|
42
|
+
}
|
|
43
|
+
out[tier] = v;
|
|
44
|
+
}
|
|
45
|
+
return { version: 1, models: out };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Load `.agentcohort.json` from `projectRoot`. Returns null if absent.
|
|
49
|
+
* Throws a structured error if the file exists but is malformed.
|
|
50
|
+
*/
|
|
51
|
+
function loadConfig(projectRoot) {
|
|
52
|
+
const path = (0, node_path_1.join)(projectRoot, exports.CONFIG_FILENAME);
|
|
53
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
54
|
+
return null;
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
61
|
+
fail(`malformed JSON (${msg})`);
|
|
62
|
+
}
|
|
63
|
+
return validateConfig(parsed);
|
|
64
|
+
}
|
|
65
|
+
const SCHEMA_URL = 'https://raw.githubusercontent.com/Thiendekaco/agentcohort/main/schema/agentcohort-config-v1.json';
|
|
66
|
+
function serializeConfig(cfg) {
|
|
67
|
+
const ordered = {
|
|
68
|
+
$schema: SCHEMA_URL,
|
|
69
|
+
version: cfg.version,
|
|
70
|
+
models: {
|
|
71
|
+
premium: cfg.models.premium,
|
|
72
|
+
mid: cfg.models.mid,
|
|
73
|
+
cheap: cfg.models.cheap,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
return JSON.stringify(ordered, null, 2) + '\n';
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Write `cfg` to `.agentcohort.json` in `projectRoot`. Includes a
|
|
80
|
+
* `$schema` field for editor autocomplete. Idempotent at the byte
|
|
81
|
+
* level: writing the same config twice produces identical bytes.
|
|
82
|
+
*/
|
|
83
|
+
function writeConfig(projectRoot, cfg) {
|
|
84
|
+
const path = (0, node_path_1.join)(projectRoot, exports.CONFIG_FILENAME);
|
|
85
|
+
(0, node_fs_1.writeFileSync)(path, serializeConfig(cfg), 'utf8');
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resolve a ModelsConfig from a possibly-null config. When null,
|
|
89
|
+
* returns DEFAULT_MODELS.
|
|
90
|
+
*/
|
|
91
|
+
function resolveModels(cfg) {
|
|
92
|
+
if (cfg === null) {
|
|
93
|
+
return {
|
|
94
|
+
premium: defaults_1.DEFAULT_MODELS.premium,
|
|
95
|
+
mid: defaults_1.DEFAULT_MODELS.mid,
|
|
96
|
+
cheap: defaults_1.DEFAULT_MODELS.cheap,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return cfg.models;
|
|
100
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ModelsConfig } from './config';
|
|
2
|
+
import { ModelChange } from './diff';
|
|
3
|
+
export type ConfigCmdStatus = 'no-changes' | 'no-agents' | 'cancelled' | 'applied';
|
|
4
|
+
export interface ConfigCmdResult {
|
|
5
|
+
status: ConfigCmdStatus;
|
|
6
|
+
changes: ModelChange[];
|
|
7
|
+
}
|
|
8
|
+
export interface ConfigCmdOptions {
|
|
9
|
+
cwd: string;
|
|
10
|
+
/** Inject the prompt function — production passes the real TUI, tests pass a mock. */
|
|
11
|
+
promptModelStrategy: (current?: ModelsConfig) => Promise<ModelsConfig>;
|
|
12
|
+
/** Inject the diff-confirm function — same reason. */
|
|
13
|
+
confirm: (message: string) => Promise<boolean>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Run the `agentcohort config` subcommand.
|
|
17
|
+
*
|
|
18
|
+
* 1. Load existing config (or null → defaults).
|
|
19
|
+
* 2. Prompt user for a new ModelsConfig (pre-filled with current).
|
|
20
|
+
* 3. If no models changed: write config (idempotent) → 'no-changes'.
|
|
21
|
+
* 4. Compute the diff of installed agent files.
|
|
22
|
+
* 5. If diff is empty (e.g. no .claude/agents dir): write config → 'no-agents'.
|
|
23
|
+
* 6. Otherwise: confirm with user. Decline → 'cancelled'. Accept →
|
|
24
|
+
* write config + rewrite each affected file's `model:` line in
|
|
25
|
+
* place (preserves the rest byte-for-byte) → 'applied'.
|
|
26
|
+
*/
|
|
27
|
+
export declare function runConfigCmd(opts: ConfigCmdOptions): Promise<ConfigCmdResult>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runConfigCmd = runConfigCmd;
|
|
4
|
+
const node_path_1 = require("node:path");
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
const diff_1 = require("./diff");
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
/**
|
|
9
|
+
* Run the `agentcohort config` subcommand.
|
|
10
|
+
*
|
|
11
|
+
* 1. Load existing config (or null → defaults).
|
|
12
|
+
* 2. Prompt user for a new ModelsConfig (pre-filled with current).
|
|
13
|
+
* 3. If no models changed: write config (idempotent) → 'no-changes'.
|
|
14
|
+
* 4. Compute the diff of installed agent files.
|
|
15
|
+
* 5. If diff is empty (e.g. no .claude/agents dir): write config → 'no-agents'.
|
|
16
|
+
* 6. Otherwise: confirm with user. Decline → 'cancelled'. Accept →
|
|
17
|
+
* write config + rewrite each affected file's `model:` line in
|
|
18
|
+
* place (preserves the rest byte-for-byte) → 'applied'.
|
|
19
|
+
*/
|
|
20
|
+
async function runConfigCmd(opts) {
|
|
21
|
+
const existing = (0, config_1.loadConfig)(opts.cwd);
|
|
22
|
+
const oldModels = (0, config_1.resolveModels)(existing);
|
|
23
|
+
const newModels = await opts.promptModelStrategy(existing?.models);
|
|
24
|
+
const noChange = newModels.premium === oldModels.premium &&
|
|
25
|
+
newModels.mid === oldModels.mid &&
|
|
26
|
+
newModels.cheap === oldModels.cheap;
|
|
27
|
+
const newConfig = { version: 1, models: newModels };
|
|
28
|
+
const agentDir = (0, node_path_1.join)(opts.cwd, '.claude', 'agents');
|
|
29
|
+
if (noChange) {
|
|
30
|
+
(0, config_1.writeConfig)(opts.cwd, newConfig);
|
|
31
|
+
if (!(0, node_fs_1.existsSync)(agentDir)) {
|
|
32
|
+
return { status: 'no-agents', changes: [] };
|
|
33
|
+
}
|
|
34
|
+
return { status: 'no-changes', changes: [] };
|
|
35
|
+
}
|
|
36
|
+
const changes = (0, diff_1.computeFrontmatterModelDiff)(agentDir, oldModels, newModels);
|
|
37
|
+
if (changes.length === 0) {
|
|
38
|
+
(0, config_1.writeConfig)(opts.cwd, newConfig);
|
|
39
|
+
return { status: 'no-agents', changes: [] };
|
|
40
|
+
}
|
|
41
|
+
const message = `Apply ${changes.length} model change${changes.length === 1 ? '' : 's'}?`;
|
|
42
|
+
const accepted = await opts.confirm(message);
|
|
43
|
+
if (!accepted) {
|
|
44
|
+
return { status: 'cancelled', changes };
|
|
45
|
+
}
|
|
46
|
+
(0, config_1.writeConfig)(opts.cwd, newConfig);
|
|
47
|
+
for (const c of changes) {
|
|
48
|
+
const path = (0, node_path_1.join)(agentDir, c.file);
|
|
49
|
+
const current = (0, node_fs_1.readFileSync)(path, 'utf8');
|
|
50
|
+
const rewritten = current.replace(/^model:[ \t]+\S+[ \t]*$/m, `model: ${c.to}`);
|
|
51
|
+
(0, node_fs_1.writeFileSync)(path, rewritten, 'utf8');
|
|
52
|
+
}
|
|
53
|
+
return { status: 'applied', changes };
|
|
54
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for default Claude model IDs and the
|
|
3
|
+
* alias-to-tier mapping the installer uses when rewriting agent
|
|
4
|
+
* templates.
|
|
5
|
+
*
|
|
6
|
+
* When a new Claude model ships, update DEFAULT_MODELS here and the
|
|
7
|
+
* change flows through both the default install path and the prompt
|
|
8
|
+
* defaults.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_MODELS: {
|
|
11
|
+
readonly premium: "claude-opus-4-7";
|
|
12
|
+
readonly mid: "claude-sonnet-4-6";
|
|
13
|
+
readonly cheap: "claude-haiku-4-5-20251001";
|
|
14
|
+
};
|
|
15
|
+
export declare const TIER_ALIASES: {
|
|
16
|
+
readonly opus: "premium";
|
|
17
|
+
readonly sonnet: "mid";
|
|
18
|
+
readonly haiku: "cheap";
|
|
19
|
+
};
|
|
20
|
+
export type Tier = keyof typeof DEFAULT_MODELS;
|
|
21
|
+
export type TierAlias = keyof typeof TIER_ALIASES;
|
package/dist/defaults.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Single source of truth for default Claude model IDs and the
|
|
4
|
+
* alias-to-tier mapping the installer uses when rewriting agent
|
|
5
|
+
* templates.
|
|
6
|
+
*
|
|
7
|
+
* When a new Claude model ships, update DEFAULT_MODELS here and the
|
|
8
|
+
* change flows through both the default install path and the prompt
|
|
9
|
+
* defaults.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.TIER_ALIASES = exports.DEFAULT_MODELS = void 0;
|
|
13
|
+
exports.DEFAULT_MODELS = {
|
|
14
|
+
premium: 'claude-opus-4-7',
|
|
15
|
+
mid: 'claude-sonnet-4-6',
|
|
16
|
+
cheap: 'claude-haiku-4-5-20251001',
|
|
17
|
+
};
|
|
18
|
+
exports.TIER_ALIASES = {
|
|
19
|
+
opus: 'premium',
|
|
20
|
+
sonnet: 'mid',
|
|
21
|
+
haiku: 'cheap',
|
|
22
|
+
};
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ModelsConfig } from './config';
|
|
2
|
+
export interface ModelChange {
|
|
3
|
+
file: string;
|
|
4
|
+
from: string;
|
|
5
|
+
to: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* For each `.md` file in `installedAgentDir`, decide whether changing
|
|
9
|
+
* the model config from `oldModels` to `newModels` would alter the
|
|
10
|
+
* file's frontmatter `model:` line. Returns the list of changes (file
|
|
11
|
+
* name + from/to model IDs).
|
|
12
|
+
*
|
|
13
|
+
* A file is treated three ways:
|
|
14
|
+
* 1. Tier alias in frontmatter (`model: opus|sonnet|haiku`): treat
|
|
15
|
+
* as `oldModels[tier(alias)]` and check whether
|
|
16
|
+
* `newModels[tier(alias)]` differs.
|
|
17
|
+
* 2. Concrete ID matching one of oldModels: that's a previously
|
|
18
|
+
* rendered file; we know its tier, so check whether the new
|
|
19
|
+
* config differs.
|
|
20
|
+
* 3. Concrete ID NOT in oldModels: a hand-edit. Skipped.
|
|
21
|
+
*
|
|
22
|
+
* Returns [] if the dir does not exist.
|
|
23
|
+
*/
|
|
24
|
+
export declare function computeFrontmatterModelDiff(installedAgentDir: string, oldModels: ModelsConfig, newModels: ModelsConfig): ModelChange[];
|
package/dist/diff.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeFrontmatterModelDiff = computeFrontmatterModelDiff;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const defaults_1 = require("./defaults");
|
|
7
|
+
/**
|
|
8
|
+
* For each `.md` file in `installedAgentDir`, decide whether changing
|
|
9
|
+
* the model config from `oldModels` to `newModels` would alter the
|
|
10
|
+
* file's frontmatter `model:` line. Returns the list of changes (file
|
|
11
|
+
* name + from/to model IDs).
|
|
12
|
+
*
|
|
13
|
+
* A file is treated three ways:
|
|
14
|
+
* 1. Tier alias in frontmatter (`model: opus|sonnet|haiku`): treat
|
|
15
|
+
* as `oldModels[tier(alias)]` and check whether
|
|
16
|
+
* `newModels[tier(alias)]` differs.
|
|
17
|
+
* 2. Concrete ID matching one of oldModels: that's a previously
|
|
18
|
+
* rendered file; we know its tier, so check whether the new
|
|
19
|
+
* config differs.
|
|
20
|
+
* 3. Concrete ID NOT in oldModels: a hand-edit. Skipped.
|
|
21
|
+
*
|
|
22
|
+
* Returns [] if the dir does not exist.
|
|
23
|
+
*/
|
|
24
|
+
function computeFrontmatterModelDiff(installedAgentDir, oldModels, newModels) {
|
|
25
|
+
if (!(0, node_fs_1.existsSync)(installedAgentDir))
|
|
26
|
+
return [];
|
|
27
|
+
const oldById = {
|
|
28
|
+
[oldModels.premium]: 'premium',
|
|
29
|
+
[oldModels.mid]: 'mid',
|
|
30
|
+
[oldModels.cheap]: 'cheap',
|
|
31
|
+
};
|
|
32
|
+
const changes = [];
|
|
33
|
+
for (const file of (0, node_fs_1.readdirSync)(installedAgentDir).sort()) {
|
|
34
|
+
if (!file.endsWith('.md'))
|
|
35
|
+
continue;
|
|
36
|
+
const text = (0, node_fs_1.readFileSync)((0, node_path_1.join)(installedAgentDir, file), 'utf8');
|
|
37
|
+
const m = text.match(/^model:[ \t]+(\S+)[ \t]*$/m);
|
|
38
|
+
if (!m?.[1])
|
|
39
|
+
continue;
|
|
40
|
+
const value = m[1];
|
|
41
|
+
let tier;
|
|
42
|
+
let from = '';
|
|
43
|
+
if (value === 'opus' || value === 'sonnet' || value === 'haiku') {
|
|
44
|
+
tier = defaults_1.TIER_ALIASES[value];
|
|
45
|
+
if (!tier)
|
|
46
|
+
continue; // satisfy strict TS
|
|
47
|
+
from = oldModels[tier];
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const tierFromId = oldById[value];
|
|
51
|
+
if (!tierFromId) {
|
|
52
|
+
// hand-edited specific ID → skip
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
tier = tierFromId;
|
|
56
|
+
from = value;
|
|
57
|
+
}
|
|
58
|
+
const to = newModels[tier];
|
|
59
|
+
if (from !== to) {
|
|
60
|
+
changes.push({ file, from, to });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return changes;
|
|
64
|
+
}
|
package/dist/installer.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EntryKind } from './manifest';
|
|
2
2
|
import type { ConflictResolver } from './prompt';
|
|
3
3
|
import type { Logger } from './logger';
|
|
4
|
+
import type { ModelsConfig } from './config';
|
|
4
5
|
export type Disposition = 'created' | 'overwritten' | 'appended-section' | 'replaced-section' | 'skipped' | 'unchanged';
|
|
5
6
|
export interface ActionRecord {
|
|
6
7
|
targetRelPath: string;
|
|
@@ -19,6 +20,7 @@ export interface InitOptions {
|
|
|
19
20
|
backup: boolean;
|
|
20
21
|
/** When false, conflicts are resolved by safe automatic defaults. */
|
|
21
22
|
interactive: boolean;
|
|
23
|
+
models: ModelsConfig;
|
|
22
24
|
resolver?: ConflictResolver;
|
|
23
25
|
now?: () => Date;
|
|
24
26
|
logger?: Logger;
|
package/dist/installer.js
CHANGED
|
@@ -7,6 +7,7 @@ const fileOps_1 = require("./fileOps");
|
|
|
7
7
|
const claudeMd_1 = require("./claudeMd");
|
|
8
8
|
const manifest_1 = require("./manifest");
|
|
9
9
|
const paths_1 = require("./paths");
|
|
10
|
+
const render_1 = require("./render");
|
|
10
11
|
/** Pick a non-clobbering backup path (never overwrite an existing backup). */
|
|
11
12
|
function uniqueBackupPath(target, date) {
|
|
12
13
|
const base = (0, fileOps_1.backupPathFor)(target, date);
|
|
@@ -100,7 +101,10 @@ async function runInit(options) {
|
|
|
100
101
|
return { projectRoot, actions, dryRun: options.dryRun };
|
|
101
102
|
// ---- per-entry handlers (closures over decide/record/doBackup) ----
|
|
102
103
|
async function handleRegular(entry) {
|
|
103
|
-
const
|
|
104
|
+
const rawTemplate = (0, node_fs_1.readFileSync)(entry.templateAbsPath, 'utf8');
|
|
105
|
+
const template = entry.targetRelPath.startsWith('.claude/agents/')
|
|
106
|
+
? (0, render_1.renderAgentTemplate)(rawTemplate, options.models)
|
|
107
|
+
: rawTemplate;
|
|
104
108
|
const existing = (0, fileOps_1.readIfExists)(entry.targetAbsPath);
|
|
105
109
|
if (existing === null) {
|
|
106
110
|
if (!options.dryRun)
|