@execuro-sw-ecosystem/sw-ecosystem-agentic-harness 0.1.5 → 0.1.7
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/CHANGELOG.md +41 -0
- package/CONTRIBUTING.md +13 -4
- package/README.md +40 -23
- package/bin/cli.mjs +17 -13
- package/lib/commands.mjs +71 -67
- package/lib/guide.mjs +28 -20
- package/lib/hosts.mjs +8 -8
- package/lib/lock.mjs +10 -5
- package/lib/out.mjs +27 -6
- package/lib/prompt.mjs +18 -23
- package/lib/render.mjs +364 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/sw-setup/SKILL.md +14 -12
- package/skills/sw-setup/reference/rows.md +20 -14
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,47 @@ release.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.7] - 2026-09-19
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **BREAKING: `install`, `status`, `plan` and `uninstall` print a human
|
|
15
|
+
summary, not JSON.** Typing `install` and getting several hundred lines of
|
|
16
|
+
JSON made success unreadable; the result object is now rendered for a
|
|
17
|
+
person on stdout — what changed, grouped by coding agent, and the restart
|
|
18
|
+
each one needs. A run with nothing to do is one line. Conflicts, drift and
|
|
19
|
+
manual steps are never collapsed: each names its file and its remedy, and
|
|
20
|
+
an agent this project has not installed is never named at all. Pass
|
|
21
|
+
`--json` for the previous object, unchanged apart from the key rename
|
|
22
|
+
below, and `--verbose` for the per-file detail alongside the summary.
|
|
23
|
+
**Every skill, script or agent parsing this output must add `--json`.**
|
|
24
|
+
- **BREAKING: `--host` is now `--agent`, with no alias.** `--host` is
|
|
25
|
+
rejected as an unknown option (exit 2, with runnable `--agent` help), the
|
|
26
|
+
`hosts` key in every result body is now `agents`, and the lock file records
|
|
27
|
+
`agents`. A lock written by an earlier version under `hosts` is still read
|
|
28
|
+
and is rewritten as `agents` on the next run, so an existing install keeps
|
|
29
|
+
its recorded selection. Consistent with the earlier
|
|
30
|
+
`--no-companions` -> `--no-extra-components` break.
|
|
31
|
+
**WARNING — the lock migration is one-way:** writing `agents` drops the
|
|
32
|
+
legacy `hosts` key, so after any run of this version an older pinned CLI
|
|
33
|
+
(`npx …@0.1.6`) exits 2 in that repository with "no --host given, this
|
|
34
|
+
repository has no recorded install". Verified against the published 0.1.6
|
|
35
|
+
tarball.
|
|
36
|
+
- `install` invoked with a bad flag now reports `install`, not `apply`, in
|
|
37
|
+
its usage help.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- `status` no longer labels a conflict as "edited since install". Its
|
|
42
|
+
`drift[]` entries now carry the action's `state`, and the human renderer
|
|
43
|
+
titles conflicts as conflicts.
|
|
44
|
+
|
|
45
|
+
## [0.1.6] - 2026-09-18
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- `README.md` only (commit subject: "Test"). No code change.
|
|
50
|
+
|
|
10
51
|
## [0.1.5] - 2026-09-17
|
|
11
52
|
|
|
12
53
|
### Fixed
|
package/CONTRIBUTING.md
CHANGED
|
@@ -15,7 +15,7 @@ npm run gen:check # generated adapters match their sources
|
|
|
15
15
|
node scripts/check-pack.mjs # the tarball matches the files allow-list
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
If you edited an agent or a skill, regenerate the
|
|
18
|
+
If you edited an agent or a skill, regenerate the per-agent adapters first and
|
|
19
19
|
commit the result:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
@@ -29,14 +29,23 @@ npm run gen
|
|
|
29
29
|
`scripts/gen-copilot-agents.mjs`. Never hand-edit them — run `npm run gen`
|
|
30
30
|
and commit the output. `npm run gen:check` fails the build on drift.
|
|
31
31
|
- **Every `SKILL.md` stays under 8192 bytes**, Codex's effective cap and the
|
|
32
|
-
smallest of the four
|
|
33
|
-
cap, move material into `reference/*.md` beside it — that is
|
|
32
|
+
smallest of the four coding agents'. A test enforces it. When a skill grows
|
|
33
|
+
past the cap, move material into `reference/*.md` beside it — that is
|
|
34
34
|
**reorganisation, never compression**. Audit the split against the
|
|
35
35
|
pre-split file line by line; do not drop meaning to make the number work.
|
|
36
|
-
- **Skills stay
|
|
36
|
+
- **Skills stay agent-neutral.** No `${CLAUDE_SKILL_DIR}`, no `$ARGUMENTS`, no
|
|
37
37
|
`SendMessage` or `subagent_type`, no literal `.claude/…` path. Any prose that
|
|
38
38
|
asks the user a question names both Claude Code's and Codex's question
|
|
39
39
|
tools, not just one.
|
|
40
|
+
- **The result object is the model; `lib/render.mjs` is only a view.** Every
|
|
41
|
+
command builds one body, `--json` prints it verbatim and the human summary
|
|
42
|
+
is rendered from that same body — never assembled beside it. The renderer is
|
|
43
|
+
pure `body -> string` with no I/O, so it is unit-tested in
|
|
44
|
+
`test/render.test.mjs` without a terminal. Human text goes to stdout (it is
|
|
45
|
+
the result); only the interactive picker and warnings go to stderr.
|
|
46
|
+
- **Test suites pass `--json`.** `test/helpers.mjs`'s `run()` appends it for
|
|
47
|
+
every call; only a suite that is about the human output opts out with
|
|
48
|
+
`{ json: false }`.
|
|
40
49
|
- **The installer never prompts, never fetches, and writes atomically.**
|
|
41
50
|
Every file is written to a temporary path and renamed. A change that breaks
|
|
42
51
|
any of those three properties is a change to the contract this package
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ The `sw-*` skills and sub-agents for Shopware 6 projects: they turn a briefing
|
|
|
4
4
|
into a PRD, a PRD into a technical spec, a spec into tested code, and tested
|
|
5
5
|
code into a project wiki page.
|
|
6
6
|
|
|
7
|
+
New line
|
|
8
|
+
|
|
7
9
|
It ships as **one npm package carrying an installer**. The installer copies the
|
|
8
10
|
skills and agents into whichever coding agents your repository uses and
|
|
9
11
|
registers the MCP servers and permission rules they depend on. Nothing is
|
|
@@ -49,26 +51,39 @@ declines is fully set up without them, and `--editor` on `sw-design-requirements
|
|
|
49
51
|
|
|
50
52
|
| Command | What it does |
|
|
51
53
|
| --- | --- |
|
|
52
|
-
| `install` | installs into the resolved
|
|
54
|
+
| `install` | installs into the resolved coding agent(s) (see Flags below); the same as `apply --yes` |
|
|
53
55
|
| `status` | read-only; what is installed, what drifted, which optional extra components are present |
|
|
54
56
|
| `plan` | the exact changes, written as a diff; writes nothing |
|
|
55
57
|
| `apply --yes` | performs them |
|
|
56
58
|
| `uninstall --yes` | removes what the lock file records |
|
|
57
59
|
| `guide` | the install protocol, the single source of truth |
|
|
58
60
|
|
|
59
|
-
Flags: `--
|
|
61
|
+
Flags: `--agent claude-code|codex|copilot|cursor` (repeatable), `--scope
|
|
60
62
|
project|user` (default `project`), `--root <path>` (default the current
|
|
61
63
|
directory), `--yes` (required by `apply` and `uninstall` — `install` implies
|
|
62
|
-
it),
|
|
63
|
-
packages).
|
|
64
|
-
|
|
65
|
-
`install`/`apply` resolve which
|
|
66
|
-
given; else the
|
|
67
|
-
question); else, with a terminal attached, an interactive picker;
|
|
68
|
-
with the four `install --
|
|
69
|
-
never hangs. `status` and `uninstall` are unaffected by this and
|
|
70
|
-
covering every
|
|
71
|
-
no lock it previews all four and says so.
|
|
64
|
+
it), `--no-extra-components` (skip detection of the optional editor
|
|
65
|
+
packages), `--json` and `--verbose` (see Output below).
|
|
66
|
+
|
|
67
|
+
`install`/`apply` resolve which coding agent(s) to write into, in order:
|
|
68
|
+
`--agent` if given; else the agents a lock file already records (a re-run
|
|
69
|
+
needs no question); else, with a terminal attached, an interactive picker;
|
|
70
|
+
else exit 2 with the four `install --agent <a>` commands as `help` — it never
|
|
71
|
+
guesses and never hangs. `status` and `uninstall` are unaffected by this and
|
|
72
|
+
keep covering every agent by default; `plan` writes nothing, so with no
|
|
73
|
+
`--agent` and no lock it previews all four and says so.
|
|
74
|
+
|
|
75
|
+
### Output
|
|
76
|
+
|
|
77
|
+
`install`, `status`, `plan` and `uninstall` print a short human summary on
|
|
78
|
+
stdout — what changed, grouped by coding agent, and the restart each one
|
|
79
|
+
needs. When nothing changed, that is a single line. Conflicts, drift and
|
|
80
|
+
manual steps are always shown in full, with the file and the remedy, because
|
|
81
|
+
those are the only things you have to act on.
|
|
82
|
+
|
|
83
|
+
`--json` prints the result object instead: the same body, with the per-file
|
|
84
|
+
detail, a `next_step` on every success and a `help` array of runnable commands
|
|
85
|
+
on every error. **Anything parsing this output must pass `--json`.**
|
|
86
|
+
`--verbose` keeps the human summary but adds the per-file detail.
|
|
72
87
|
|
|
73
88
|
## Install
|
|
74
89
|
|
|
@@ -84,9 +99,9 @@ The command's output tells you which ones and how.
|
|
|
84
99
|
|
|
85
100
|
`apply` refuses without `--yes`. Neither `apply` nor `install` prompts for
|
|
86
101
|
consent — typing the verb, or passing `--yes`, is the consent — but with no
|
|
87
|
-
`--
|
|
102
|
+
`--agent`, no existing lock and no terminal (Codex's question tool does not
|
|
88
103
|
block outside Plan mode, and CI has no terminal either), it exits 2 rather
|
|
89
|
-
than guess. Add `--
|
|
104
|
+
than guess. Add `--agent claude-code` (repeatable) to install into one agent,
|
|
90
105
|
and `--scope user` to install into your home directory instead of this
|
|
91
106
|
repository. The default scope is the project — nothing is written under `~`
|
|
92
107
|
unless you ask.
|
|
@@ -127,10 +142,11 @@ keeps anything you hand-edited, and tells you what it kept.
|
|
|
127
142
|
|
|
128
143
|
### From a skill
|
|
129
144
|
|
|
130
|
-
`sw-setup` is a stub: it runs `status`, renders the table, asks once,
|
|
131
|
-
`apply --yes`.
|
|
132
|
-
|
|
133
|
-
protocol
|
|
145
|
+
`sw-setup` is a stub: it runs `status --json`, renders the table, asks once,
|
|
146
|
+
and runs `apply --yes --json`. A skill always passes `--json`; the bare
|
|
147
|
+
commands print the human summary. It deliberately restates none of the
|
|
148
|
+
protocol, because an installed skill file goes stale and the CLI does not.
|
|
149
|
+
Anything that needs the protocol runs:
|
|
134
150
|
|
|
135
151
|
```bash
|
|
136
152
|
npx -y @execuro-sw-ecosystem/sw-ecosystem-agentic-harness@latest guide
|
|
@@ -163,12 +179,13 @@ npm i -D @execuro-sw-ecosystem/sw-tender-discovery-tool@0.1.0
|
|
|
163
179
|
|
|
164
180
|
Re-run `install` afterwards so the new skill lands in each host. `status`'s
|
|
165
181
|
`extra_components[]` array reports the pinned `version` and the exact `install`
|
|
166
|
-
command for whichever one is missing.
|
|
182
|
+
command for whichever one is missing (`status --json`).
|
|
167
183
|
|
|
168
184
|
Re-running `install` is safe at any time: it rewrites only what it owns,
|
|
169
185
|
leaves anything you hand-edited alone (reported as drift), and records the new
|
|
170
|
-
version in the lock file.
|
|
171
|
-
reads its configuration at
|
|
186
|
+
version in the lock file. It prints one line when there was nothing to change.
|
|
187
|
+
Restart your coding agent afterwards — every host reads its configuration at
|
|
188
|
+
startup.
|
|
172
189
|
|
|
173
190
|
`@latest` matters. Without it, `npx` can reuse a copy already in its cache or
|
|
174
191
|
in this project's `node_modules`, so you would silently keep running the
|
|
@@ -332,7 +349,7 @@ repository is development-only.**
|
|
|
332
349
|
|
|
333
350
|
### Every `SKILL.md` is capped at 8 KB
|
|
334
351
|
|
|
335
|
-
Codex's effective skill-body limit is the smallest of the four
|
|
352
|
+
Codex's effective skill-body limit is the smallest of the four coding agents', so it is
|
|
336
353
|
the budget every skill is written to. Overflow lives in `reference/*.md` beside
|
|
337
354
|
each `SKILL.md`, loaded on demand. A test enforces the cap, so the gate is
|
|
338
355
|
mechanical rather than a review habit.
|
|
@@ -341,7 +358,7 @@ mechanical rather than a review habit.
|
|
|
341
358
|
|
|
342
359
|
| Symptom | What it means | What to do |
|
|
343
360
|
| --- | --- | --- |
|
|
344
|
-
| `state: "conflict"` | A file we did not install already sits at that path. It is never overwritten. | Move or delete the existing file, or `--
|
|
361
|
+
| `state: "conflict"` | A file we did not install already sits at that path. It is never overwritten. | Move or delete the existing file, or `--agent`-scope the run away from it, then re-run `apply`. |
|
|
345
362
|
| `state: "drift"` | You (or something else) edited a file this installer wrote. It is left alone on every later run. | Nothing to fix — this is by design. Re-run `status` to confirm it is the only thing reported. |
|
|
346
363
|
| A manual step for `.vscode/mcp.json` | That file is JSONC (comments or trailing commas). Rewriting it would destroy your notes, so it is never touched automatically. | Add the printed snippet to `.vscode/mcp.json` yourself, under the `servers` key. |
|
|
347
364
|
| `.codex/` reported read-only | Codex's default workspace-write sandbox makes `.codex/` and `.agents/` read-only from inside a Codex session. | Run the installer from a plain shell, not from inside Codex. |
|
package/bin/cli.mjs
CHANGED
|
@@ -28,23 +28,23 @@ const COMMANDS = {
|
|
|
28
28
|
|
|
29
29
|
const USAGE = `usage: sw-ecosystem-agentic-harness <command> [options]
|
|
30
30
|
|
|
31
|
-
install [--
|
|
32
|
-
status [--
|
|
33
|
-
plan [--
|
|
34
|
-
apply --yes [--
|
|
35
|
-
uninstall --yes [--
|
|
31
|
+
install [--agent <a>] [--scope project|user] [--root <path>] [--no-extra-components]
|
|
32
|
+
status [--agent <a>] [--scope project|user] [--root <path>]
|
|
33
|
+
plan [--agent <a>] [--scope project|user] [--root <path>] [--no-extra-components]
|
|
34
|
+
apply --yes [--agent <a>] [--scope project|user] [--root <path>] [--no-extra-components]
|
|
35
|
+
uninstall --yes [--agent <a>] [--scope project|user] [--root <path>]
|
|
36
36
|
guide print the install protocol
|
|
37
37
|
|
|
38
38
|
install is apply --yes under a friendlier name: the whole install in one
|
|
39
39
|
command, no --yes needed — typing the verb is the consent for that write;
|
|
40
|
-
it can still ask which
|
|
40
|
+
it can still ask which coding agent(s), see below.
|
|
41
41
|
|
|
42
|
-
<
|
|
43
|
-
several. install/apply resolve which
|
|
44
|
-
given; else the
|
|
42
|
+
<a> is one of: claude-code, codex, copilot, cursor. Repeat --agent to select
|
|
43
|
+
several. install/apply resolve which agent(s) to use, in order: --agent if
|
|
44
|
+
given; else the agents already recorded in the lock file, if one exists;
|
|
45
45
|
else, with a terminal attached, an interactive picker; else exit 2 with
|
|
46
|
-
the four "... install --
|
|
47
|
-
keep covering every
|
|
46
|
+
the four "... install --agent <a>" commands as help. status and uninstall
|
|
47
|
+
keep covering every agent by default.
|
|
48
48
|
|
|
49
49
|
--scope defaults to project: nothing is written under your home directory
|
|
50
50
|
unless you ask for it. --root defaults to the current directory.
|
|
@@ -52,8 +52,12 @@ const USAGE = `usage: sw-ecosystem-agentic-harness <command> [options]
|
|
|
52
52
|
--no-extra-components skips the two optional extra-component skills (Specs
|
|
53
53
|
Editor, Tender Discovery Tool) even when the package is present locally.
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
--json print the result object instead of the human summary
|
|
56
|
+
--verbose include the per-file detail --json carries
|
|
57
|
+
|
|
58
|
+
install, status, plan and uninstall print a short human summary on stdout.
|
|
59
|
+
Add --json for the machine-readable object, which always carries a next_step.
|
|
60
|
+
Run \`sw-ecosystem-agentic-harness guide\` first — it is the install protocol.`;
|
|
57
61
|
|
|
58
62
|
const argv = process.argv.slice(2);
|
|
59
63
|
const cmd = argv[0];
|
package/lib/commands.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// status / plan / apply / uninstall.
|
|
2
2
|
//
|
|
3
|
-
// Error aggregation lives here: each
|
|
4
|
-
// own try, failures accumulate, and the process fails only after every
|
|
5
|
-
// has been attempted. One unwritable
|
|
3
|
+
// Error aggregation lives here: each agent is planned and applied inside its
|
|
4
|
+
// own try, failures accumulate, and the process fails only after every agent
|
|
5
|
+
// has been attempted. One unwritable agent must not abort the others.
|
|
6
6
|
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import { resolve } from 'node:path';
|
|
@@ -11,11 +11,12 @@ import {
|
|
|
11
11
|
CONTENT_VERSION, LOCK_FILE, PKG_NAME, PKG_VERSION, agents, skills,
|
|
12
12
|
} from './content.mjs';
|
|
13
13
|
import { exists, isWritable, abs } from './fsx.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import { AGENTS, buildPlan, detectAll, installedCounts } from './hosts.mjs';
|
|
15
15
|
import * as lockfile from './lock.mjs';
|
|
16
16
|
import { dedupe, execute, isWritableState, preview, pruneDirs, revert } from './actions.mjs';
|
|
17
17
|
import { probeAll } from './extra-components.mjs';
|
|
18
|
-
import {
|
|
18
|
+
import { askAgents } from './prompt.mjs';
|
|
19
|
+
import { RESTART } from './render.mjs';
|
|
19
20
|
|
|
20
21
|
const RUN = 'sw-ecosystem-agentic-harness';
|
|
21
22
|
|
|
@@ -25,24 +26,28 @@ class UsageError extends Error {
|
|
|
25
26
|
|
|
26
27
|
/** Parse the flags every command shares. */
|
|
27
28
|
export function buildContext(argv, command) {
|
|
28
|
-
const
|
|
29
|
+
const agents = [];
|
|
29
30
|
let scope = 'project';
|
|
30
31
|
let root = process.cwd();
|
|
31
32
|
let yes = false;
|
|
32
33
|
let projectWiki;
|
|
33
34
|
let extraComponents = true;
|
|
35
|
+
let asJson = false;
|
|
36
|
+
let verbose = false;
|
|
34
37
|
|
|
35
38
|
for (let i = 0; i < argv.length; i += 1) {
|
|
36
39
|
const arg = argv[i];
|
|
37
40
|
if (arg === '--yes' || arg === '-y') { yes = true; continue; }
|
|
38
41
|
if (arg === '--no-extra-components') { extraComponents = false; continue; }
|
|
39
|
-
if (arg === '--
|
|
42
|
+
if (arg === '--json') { asJson = true; continue; }
|
|
43
|
+
if (arg === '--verbose') { verbose = true; continue; }
|
|
44
|
+
if (arg === '--agent') {
|
|
40
45
|
const value = argv[++i];
|
|
41
|
-
if (!
|
|
42
|
-
throw new UsageError(`unknown --
|
|
43
|
-
|
|
46
|
+
if (!AGENTS.includes(value)) {
|
|
47
|
+
throw new UsageError(`unknown --agent ${value ?? '(missing)'}`,
|
|
48
|
+
AGENTS.map((a) => `${RUN} ${command} --agent ${a}`));
|
|
44
49
|
}
|
|
45
|
-
|
|
50
|
+
agents.push(value);
|
|
46
51
|
continue;
|
|
47
52
|
}
|
|
48
53
|
if (arg === '--scope') {
|
|
@@ -59,17 +64,20 @@ export function buildContext(argv, command) {
|
|
|
59
64
|
throw new UsageError(`unknown option ${arg}`, [`${RUN} guide`, `${RUN} ${command}`]);
|
|
60
65
|
}
|
|
61
66
|
|
|
67
|
+
out.setFormat(asJson ? 'json' : 'human');
|
|
68
|
+
out.setVerbose(verbose);
|
|
69
|
+
|
|
62
70
|
// User scope writes under $HOME; project scope stays in the repository.
|
|
63
71
|
if (scope === 'user') root = homedir();
|
|
64
72
|
root = resolve(root);
|
|
65
73
|
if (!exists(root)) throw new UsageError(`--root does not exist: ${root}`, [`${RUN} ${command} --root .`]);
|
|
66
74
|
|
|
67
75
|
return {
|
|
68
|
-
root, scope, yes, projectWiki, extraComponents,
|
|
69
|
-
// Empty means "nothing chosen yet" —
|
|
76
|
+
root, scope, yes, projectWiki, extraComponents, json: asJson, verbose,
|
|
77
|
+
// Empty means "nothing chosen yet" — resolveAgents() applies the
|
|
70
78
|
// per-command fallback once the lock (and, for apply/install, a TTY)
|
|
71
79
|
// is available to consult.
|
|
72
|
-
|
|
80
|
+
agents: [...new Set(agents)],
|
|
73
81
|
platform: process.platform,
|
|
74
82
|
version: PKG_VERSION,
|
|
75
83
|
lock: null,
|
|
@@ -77,35 +85,36 @@ export function buildContext(argv, command) {
|
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
/**
|
|
80
|
-
* The
|
|
81
|
-
* 1. `--
|
|
82
|
-
* 2. else a lock already records
|
|
83
|
-
* the choice was already made. No question.
|
|
88
|
+
* The agent-resolution chain every write command shares:
|
|
89
|
+
* 1. `--agent` given → use exactly those. No question.
|
|
90
|
+
* 2. else a lock already records agents → reuse them. This is an update;
|
|
91
|
+
* the choice was already made. No question. A lock written before the
|
|
92
|
+
* rename records them under `hosts`; lib/lock.mjs reads either.
|
|
84
93
|
* 3. else a TTY is attached (apply/install only) → ask, via lib/prompt.mjs.
|
|
85
94
|
* 4. else → UsageError, exit 2, `help` lists the four runnable commands.
|
|
86
95
|
*
|
|
87
|
-
* `status` and `uninstall` keep their old, ungated default of every
|
|
96
|
+
* `status` and `uninstall` keep their old, ungated default of every agent —
|
|
88
97
|
* reporting and removal are not consent points. `plan` writes nothing, so it
|
|
89
98
|
* never reaches step 3: steps 1-2, then it previews all four and says so.
|
|
90
99
|
*
|
|
91
100
|
* Call after `loadLock(ctx)` so `ctx.lock` is populated for step 2.
|
|
92
101
|
*/
|
|
93
|
-
async function
|
|
94
|
-
if (ctx.
|
|
95
|
-
if (command === 'status' || command === 'uninstall') return {
|
|
102
|
+
async function resolveAgents(ctx, command) {
|
|
103
|
+
if (ctx.agents.length) return { agents: [...new Set(ctx.agents)], chosen: true };
|
|
104
|
+
if (command === 'status' || command === 'uninstall') return { agents: [...AGENTS], chosen: true };
|
|
96
105
|
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
106
|
+
const recorded = ctx.lock?.agents ? Object.keys(ctx.lock.agents) : [];
|
|
107
|
+
if (recorded.length) return { agents: recorded, chosen: true };
|
|
99
108
|
|
|
100
|
-
if (command === 'plan') return {
|
|
109
|
+
if (command === 'plan') return { agents: [...AGENTS], chosen: false };
|
|
101
110
|
|
|
102
111
|
if (process.stdin.isTTY) {
|
|
103
|
-
const chosen = await
|
|
104
|
-
if (chosen) return {
|
|
112
|
+
const chosen = await askAgents(AGENTS);
|
|
113
|
+
if (chosen) return { agents: chosen, chosen: true };
|
|
105
114
|
}
|
|
106
115
|
throw new UsageError(
|
|
107
|
-
'no --
|
|
108
|
-
|
|
116
|
+
'no --agent given, this repository has no recorded install, and there is no terminal to ask which coding agent(s) to use',
|
|
117
|
+
AGENTS.map((a) => `${RUN} ${command} --agent ${a}`),
|
|
109
118
|
);
|
|
110
119
|
}
|
|
111
120
|
|
|
@@ -121,13 +130,13 @@ function loadLock(ctx) {
|
|
|
121
130
|
return ctx.lock;
|
|
122
131
|
}
|
|
123
132
|
|
|
124
|
-
/** Preview every action for the selected
|
|
133
|
+
/** Preview every action for the selected agents, aggregating per-agent failures. */
|
|
125
134
|
function previewAll(ctx) {
|
|
126
135
|
const errors = [];
|
|
127
136
|
const actions = [];
|
|
128
|
-
for (const host of ctx.
|
|
137
|
+
for (const host of ctx.agents) {
|
|
129
138
|
try {
|
|
130
|
-
const forHost = dedupe(buildPlan({ ...ctx,
|
|
139
|
+
const forHost = dedupe(buildPlan({ ...ctx, agents: [host], selectedAgents: ctx.agents }));
|
|
131
140
|
for (const action of forHost) {
|
|
132
141
|
try {
|
|
133
142
|
actions.push(preview(action, ctx));
|
|
@@ -175,13 +184,13 @@ export async function status(argv) {
|
|
|
175
184
|
}
|
|
176
185
|
throw e;
|
|
177
186
|
}
|
|
178
|
-
({
|
|
187
|
+
({ agents: ctx.agents } = await resolveAgents(ctx, 'status'));
|
|
179
188
|
|
|
180
189
|
const detections = await detectAll(ctx);
|
|
181
190
|
const { actions, errors } = previewAll(ctx);
|
|
182
191
|
const byHost = new Map(detections.map((d) => [d.id, d]));
|
|
183
192
|
|
|
184
|
-
const
|
|
193
|
+
const agentStates = detections.map((d) => {
|
|
185
194
|
const mine = actions.filter((a) => a.host === d.id || a.host === 'shared');
|
|
186
195
|
let state = 'absent';
|
|
187
196
|
if (d.detected) {
|
|
@@ -189,7 +198,7 @@ export async function status(argv) {
|
|
|
189
198
|
const bad = mine.filter((a) => a.state === 'conflict' || a.state === 'drift');
|
|
190
199
|
if (bad.length) state = bad.some((a) => a.state === 'conflict') ? 'conflict' : 'drift';
|
|
191
200
|
else if (pending.length === 0) state = 'ok';
|
|
192
|
-
else state = lock?.
|
|
201
|
+
else state = lock?.agents?.[d.id] ? 'drift' : 'not-installed';
|
|
193
202
|
}
|
|
194
203
|
return {
|
|
195
204
|
id: d.id, detected: d.detected, marker: d.marker, writable: d.writable,
|
|
@@ -197,14 +206,16 @@ export async function status(argv) {
|
|
|
197
206
|
};
|
|
198
207
|
});
|
|
199
208
|
|
|
209
|
+
// `drift[]` carries conflicts too; `state` says which, so a file someone
|
|
210
|
+
// else wrote is not reported back to the user as their own edit.
|
|
200
211
|
const drift = actions.filter((a) => a.state === 'drift' || a.state === 'conflict').map((a) => ({
|
|
201
|
-
target: a.target, expected: a.hash ?? null, actual: a.prior ?? null,
|
|
212
|
+
target: a.target, state: a.state, expected: a.hash ?? null, actual: a.prior ?? null,
|
|
202
213
|
reason: a.reason, remedy: a.remedy,
|
|
203
214
|
}));
|
|
204
215
|
|
|
205
216
|
const pending = actions.filter((a) => isWritableState(a.state)).length;
|
|
206
217
|
const worst = ['conflict', 'drift', 'not-installed', 'ok', 'absent']
|
|
207
|
-
.find((s) =>
|
|
218
|
+
.find((s) => agentStates.some((a) => a.state === s)) ?? 'absent';
|
|
208
219
|
|
|
209
220
|
const extraComponents = ctx.extraComponents === false ? [] : probeAll({ platform: ctx.platform }).map((p) => ({
|
|
210
221
|
id: p.id, package: p.pkg, version: p.version, label: p.label,
|
|
@@ -231,7 +242,7 @@ export async function status(argv) {
|
|
|
231
242
|
next_step: next,
|
|
232
243
|
help: [`${RUN} plan`, `${RUN} apply --yes`, `${RUN} guide`],
|
|
233
244
|
inventory: { skills: skills().length, agents: agents().length },
|
|
234
|
-
|
|
245
|
+
agents: agentStates, extra_components: extraComponents, drift,
|
|
235
246
|
declined: lock?.declined ?? [],
|
|
236
247
|
manual: lock?.manual ?? [],
|
|
237
248
|
errors,
|
|
@@ -251,13 +262,13 @@ export async function plan(argv) {
|
|
|
251
262
|
}
|
|
252
263
|
throw e;
|
|
253
264
|
}
|
|
254
|
-
const {
|
|
255
|
-
ctx.
|
|
265
|
+
const { agents: chosenAgents, chosen } = await resolveAgents(ctx, 'plan');
|
|
266
|
+
ctx.agents = chosenAgents;
|
|
256
267
|
const { actions, errors } = previewAll(ctx);
|
|
257
268
|
const summary = summarise(actions);
|
|
258
269
|
const changes = summary.create + summary.update;
|
|
259
270
|
|
|
260
|
-
let next = chosen ? '' : 'No --
|
|
271
|
+
let next = chosen ? '' : 'No --agent given and no existing install found, so this previews all four agents. ';
|
|
261
272
|
next += changes
|
|
262
273
|
? `Nothing was written. Run \`${RUN} apply --yes\` to perform the ${changes} change(s).`
|
|
263
274
|
+ (summary.conflict || summary.manual
|
|
@@ -271,7 +282,7 @@ export async function plan(argv) {
|
|
|
271
282
|
|
|
272
283
|
return out.ok('plan', head(ctx, {
|
|
273
284
|
summary, next_step: next,
|
|
274
|
-
help: [`${RUN} apply --yes`, ...ctx.
|
|
285
|
+
help: [`${RUN} apply --yes`, ...ctx.agents.map((a) => `${RUN} apply --yes --agent ${a}`)],
|
|
275
286
|
actions: actions.map(publicAction),
|
|
276
287
|
errors,
|
|
277
288
|
}));
|
|
@@ -279,15 +290,8 @@ export async function plan(argv) {
|
|
|
279
290
|
|
|
280
291
|
// ------------------------------------------------------------------- apply
|
|
281
292
|
|
|
282
|
-
const RESTART_HELP = {
|
|
283
|
-
'claude-code': 'Restart Claude Code, or run `/mcp` in the running session',
|
|
284
|
-
codex: 'Restart `codex` — .codex/config.toml is read once at startup',
|
|
285
|
-
copilot: 'Reload the VS Code window (Developer: Reload Window) for .vscode/mcp.json',
|
|
286
|
-
cursor: 'Restart Cursor so it re-reads .cursor/mcp.json',
|
|
287
|
-
};
|
|
288
|
-
|
|
289
293
|
export async function apply(argv, label = 'apply') {
|
|
290
|
-
const ctx = buildContext(argv,
|
|
294
|
+
const ctx = buildContext(argv, label);
|
|
291
295
|
if (!ctx.yes) {
|
|
292
296
|
return out.usage(label, 'apply refuses to write without --yes',
|
|
293
297
|
[`${RUN} plan`, `${RUN} apply --yes`]);
|
|
@@ -303,7 +307,7 @@ export async function apply(argv, label = 'apply') {
|
|
|
303
307
|
}
|
|
304
308
|
throw e;
|
|
305
309
|
}
|
|
306
|
-
({
|
|
310
|
+
({ agents: ctx.agents } = await resolveAgents(ctx, label));
|
|
307
311
|
|
|
308
312
|
// An apply we cannot record is an apply we cannot undo. Refuse up front
|
|
309
313
|
// rather than leave the user with files and no way back.
|
|
@@ -316,11 +320,11 @@ export async function apply(argv, label = 'apply') {
|
|
|
316
320
|
|
|
317
321
|
const { actions, errors } = previewAll(ctx);
|
|
318
322
|
const results = [];
|
|
319
|
-
const
|
|
323
|
+
const perAgent = new Map(ctx.agents.map((a) => [a, { id: a, created: 0, updated: 0, unchanged: 0, failed: 0 }]));
|
|
320
324
|
const manual = [];
|
|
321
325
|
|
|
322
326
|
for (const action of actions) {
|
|
323
|
-
const bucket =
|
|
327
|
+
const bucket = perAgent.get(action.host) ?? perAgent.get(ctx.agents[0]);
|
|
324
328
|
if (action.state === 'manual') {
|
|
325
329
|
manual.push({ target: action.target, reason: action.reason, remedy: action.remedy });
|
|
326
330
|
results.push({ ...publicAction(action), state: 'manual' });
|
|
@@ -333,7 +337,7 @@ export async function apply(argv, label = 'apply') {
|
|
|
333
337
|
}
|
|
334
338
|
try {
|
|
335
339
|
const entry = execute(action, ctx);
|
|
336
|
-
lock = lockfile.record(lock, [entry], {
|
|
340
|
+
lock = lockfile.record(lock, [entry], { agent: action.host === 'shared' ? undefined : action.host });
|
|
337
341
|
if (bucket) bucket[action.state === 'create' ? 'created' : 'updated'] += 1;
|
|
338
342
|
results.push({ ...publicAction(action), state: action.state === 'create' ? 'created' : 'updated' });
|
|
339
343
|
} catch (e) {
|
|
@@ -355,8 +359,8 @@ export async function apply(argv, label = 'apply') {
|
|
|
355
359
|
for (const entry of manual) {
|
|
356
360
|
lock = lockfile.record(lock, [{ kind: 'manual', target: entry.target, remedy: entry.remedy }]);
|
|
357
361
|
}
|
|
358
|
-
for (const
|
|
359
|
-
lock = { ...lock,
|
|
362
|
+
for (const agent of ctx.agents) {
|
|
363
|
+
lock = { ...lock, agents: { ...lock.agents, [agent]: { installed_at: new Date().toISOString(), version: PKG_VERSION } } };
|
|
360
364
|
}
|
|
361
365
|
lockfile.write(ctx.root, lock);
|
|
362
366
|
|
|
@@ -371,12 +375,12 @@ export async function apply(argv, label = 'apply') {
|
|
|
371
375
|
failed: results.filter((r) => r.state === 'failed').length,
|
|
372
376
|
};
|
|
373
377
|
|
|
374
|
-
const help = ctx.
|
|
378
|
+
const help = ctx.agents.map((a) => RESTART[a]).filter(Boolean);
|
|
375
379
|
help.push(`${RUN} status`);
|
|
376
380
|
|
|
377
381
|
let next = summary.failed
|
|
378
382
|
? `${summary.failed} change(s) failed — see \`errors\`. Fix those, then re-run \`${RUN} apply --yes\`.`
|
|
379
|
-
: `Restart the
|
|
383
|
+
: `Restart the coding agents listed in \`help\` so they pick up the new skills, sub-agents and MCP servers.`
|
|
380
384
|
+ (manual.length ? ` Then finish the ${manual.length} manual step(s) in \`manual\`.` : '');
|
|
381
385
|
const installs = skippedInstalls(results);
|
|
382
386
|
if (installs.length) {
|
|
@@ -385,7 +389,7 @@ export async function apply(argv, label = 'apply') {
|
|
|
385
389
|
|
|
386
390
|
const body = head(ctx, {
|
|
387
391
|
summary, lock_file: LOCK_FILE, next_step: next, help,
|
|
388
|
-
|
|
392
|
+
agents: [...perAgent.values()], actions: results, manual, errors,
|
|
389
393
|
});
|
|
390
394
|
return summary.failed ? out.failure(label, `${summary.failed} change(s) failed`, body) : out.ok(label, body);
|
|
391
395
|
}
|
|
@@ -412,7 +416,7 @@ export async function uninstall(argv) {
|
|
|
412
416
|
}
|
|
413
417
|
throw e;
|
|
414
418
|
}
|
|
415
|
-
({
|
|
419
|
+
({ agents: ctx.agents } = await resolveAgents(ctx, 'uninstall'));
|
|
416
420
|
|
|
417
421
|
if (!lock) {
|
|
418
422
|
// No record means no evidence of what we own. Guessing here would delete
|
|
@@ -425,7 +429,7 @@ export async function uninstall(argv) {
|
|
|
425
429
|
}));
|
|
426
430
|
}
|
|
427
431
|
|
|
428
|
-
const selected = new Set(ctx.
|
|
432
|
+
const selected = new Set(ctx.agents);
|
|
429
433
|
const all = lockfile.entries(lock);
|
|
430
434
|
const mine = all.filter((e) => !e.host || selected.has(e.host) || e.host === 'shared');
|
|
431
435
|
|
|
@@ -452,7 +456,7 @@ export async function uninstall(argv) {
|
|
|
452
456
|
const prunedDirs = pruneDirs(lock.dirs ?? [], ctx);
|
|
453
457
|
const failed = errors.length;
|
|
454
458
|
|
|
455
|
-
if (failed === 0 && survivors.length === 0 && selected.size ===
|
|
459
|
+
if (failed === 0 && survivors.length === 0 && selected.size === AGENTS.length) {
|
|
456
460
|
// Everything we recorded is gone; the lock has nothing left to describe.
|
|
457
461
|
const { removeFile } = await import('./fsx.mjs');
|
|
458
462
|
removeFile(lockfile.lockPath(ctx.root));
|
|
@@ -466,8 +470,8 @@ export async function uninstall(argv) {
|
|
|
466
470
|
summary: { removed, reverted, kept: kept.length, missing: missing.length, failed },
|
|
467
471
|
next_step: failed
|
|
468
472
|
? `${failed} item(s) could not be removed — see \`errors\`.`
|
|
469
|
-
: `Restart your
|
|
470
|
-
help: [...ctx.
|
|
473
|
+
: `Restart your coding agent(s).` + (kept.length ? ` ${kept.length} file(s) were left in place because you edited them — they are listed under \`kept\`.` : ''),
|
|
474
|
+
help: [...ctx.agents.map((a) => RESTART[a]).filter(Boolean), `${RUN} status`],
|
|
471
475
|
kept, missing, pruned_dirs: prunedDirs, errors,
|
|
472
476
|
});
|
|
473
477
|
return failed ? out.failure('uninstall', `${failed} item(s) could not be removed`, body) : out.ok('uninstall', body);
|
|
@@ -484,9 +488,9 @@ function rebuild(lock, survivors, selected) {
|
|
|
484
488
|
else if (entry.kind === 'json-merge') json_pointers.push(entry);
|
|
485
489
|
else fences.push(entry);
|
|
486
490
|
}
|
|
487
|
-
const
|
|
488
|
-
for (const
|
|
489
|
-
return { ...lock, files, json_pointers, rules, fences,
|
|
491
|
+
const agents = { ...lock.agents };
|
|
492
|
+
for (const agent of selected) delete agents[agent];
|
|
493
|
+
return { ...lock, files, json_pointers, rules, fences, agents };
|
|
490
494
|
}
|
|
491
495
|
|
|
492
496
|
export { UsageError };
|