@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 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 host adapters first and
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 hosts'. A test enforces it. When a skill grows past the
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 host-neutral.** No `${CLAUDE_SKILL_DIR}`, no `$ARGUMENTS`, no
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 host(s) (see Flags below); the same as `apply --yes` |
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: `--host claude-code|codex|copilot|cursor` (repeatable), `--scope
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), and `--no-extra-components` (skip detection of the optional editor
63
- packages).
64
-
65
- `install`/`apply` resolve which host(s) to write into, in order: `--host` if
66
- given; else the hosts a lock file already records (a re-run needs no
67
- question); else, with a terminal attached, an interactive picker; else exit 2
68
- with the four `install --host <h>` commands as `help` — it never guesses and
69
- never hangs. `status` and `uninstall` are unaffected by this and keep
70
- covering every host by default; `plan` writes nothing, so with no `--host` and
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
- `--host`, no existing lock and no terminal (Codex's question tool does not
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 `--host claude-code` (repeatable) to install into one agent,
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, and runs
131
- `apply --yes`. It deliberately restates none of the protocol, because an
132
- installed skill file goes stale and the CLI does not. Anything that needs the
133
- protocol runs:
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. Restart your coding agent afterwards — every host
171
- reads its configuration at startup.
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 hosts', so it is
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 `--host`-scope the run away from it, then re-run `apply`. |
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 [--host <h>] [--scope project|user] [--root <path>] [--no-extra-components]
32
- status [--host <h>] [--scope project|user] [--root <path>]
33
- plan [--host <h>] [--scope project|user] [--root <path>] [--no-extra-components]
34
- apply --yes [--host <h>] [--scope project|user] [--root <path>] [--no-extra-components]
35
- uninstall --yes [--host <h>] [--scope project|user] [--root <path>]
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 host(s), see below.
40
+ it can still ask which coding agent(s), see below.
41
41
 
42
- <h> is one of: claude-code, codex, copilot, cursor. Repeat --host to select
43
- several. install/apply resolve which host(s) to use, in order: --host if
44
- given; else the hosts already recorded in the lock file, if one exists;
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 --host <h>" commands as help. status and uninstall
47
- keep covering every host by default.
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
- Every command prints one JSON object on stdout carrying a next_step. Run
56
- \`sw-ecosystem-agentic-harness guide\` first — it is the install protocol.`;
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 host is planned and applied inside its
4
- // own try, failures accumulate, and the process fails only after every host
5
- // has been attempted. One unwritable host must not abort the others.
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 { HOSTS, buildPlan, detectAll, installedCounts } from './hosts.mjs';
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 { askHosts } from './prompt.mjs';
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 hosts = [];
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 === '--host') {
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 (!HOSTS.includes(value)) {
42
- throw new UsageError(`unknown --host ${value ?? '(missing)'}`,
43
- HOSTS.map((h) => `${RUN} ${command} --host ${h}`));
46
+ if (!AGENTS.includes(value)) {
47
+ throw new UsageError(`unknown --agent ${value ?? '(missing)'}`,
48
+ AGENTS.map((a) => `${RUN} ${command} --agent ${a}`));
44
49
  }
45
- hosts.push(value);
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" — resolveHosts() applies the
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
- hosts: [...new Set(hosts)],
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 host-resolution chain every write command shares:
81
- * 1. `--host` given → use exactly those. No question.
82
- * 2. else a lock already records hosts → reuse them. This is an update;
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 host —
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 resolveHosts(ctx, command) {
94
- if (ctx.hosts.length) return { hosts: [...new Set(ctx.hosts)], chosen: true };
95
- if (command === 'status' || command === 'uninstall') return { hosts: [...HOSTS], chosen: true };
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 lockHosts = ctx.lock?.hosts ? Object.keys(ctx.lock.hosts) : [];
98
- if (lockHosts.length) return { hosts: lockHosts, chosen: true };
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 { hosts: [...HOSTS], chosen: false };
109
+ if (command === 'plan') return { agents: [...AGENTS], chosen: false };
101
110
 
102
111
  if (process.stdin.isTTY) {
103
- const chosen = await askHosts(HOSTS);
104
- if (chosen) return { hosts: chosen, chosen: true };
112
+ const chosen = await askAgents(AGENTS);
113
+ if (chosen) return { agents: chosen, chosen: true };
105
114
  }
106
115
  throw new UsageError(
107
- 'no --host given, this repository has no recorded install, and there is no terminal to ask which host(s) to use',
108
- HOSTS.map((h) => `${RUN} ${command} --host ${h}`),
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 hosts, aggregating per-host failures. */
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.hosts) {
137
+ for (const host of ctx.agents) {
129
138
  try {
130
- const forHost = dedupe(buildPlan({ ...ctx, hosts: [host], selectedHosts: ctx.hosts }));
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
- ({ hosts: ctx.hosts } = await resolveHosts(ctx, 'status'));
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 hosts = detections.map((d) => {
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?.hosts?.[d.id] ? 'drift' : 'not-installed';
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) => hosts.some((h) => h.state === s)) ?? 'absent';
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
- hosts, extra_components: extraComponents, drift,
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 { hosts, chosen } = await resolveHosts(ctx, 'plan');
255
- ctx.hosts = hosts;
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 --host given and no existing install found, so this previews all four hosts. ';
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.hosts.map((h) => `${RUN} apply --yes --host ${h}`)],
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, 'apply');
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
- ({ hosts: ctx.hosts } = await resolveHosts(ctx, label));
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 perHost = new Map(ctx.hosts.map((h) => [h, { id: h, created: 0, updated: 0, unchanged: 0, failed: 0 }]));
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 = perHost.get(action.host) ?? perHost.get(ctx.hosts[0]);
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], { host: action.host === 'shared' ? undefined : action.host });
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 host of ctx.hosts) {
359
- lock = { ...lock, hosts: { ...lock.hosts, [host]: { installed_at: new Date().toISOString(), version: PKG_VERSION } } };
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.hosts.map((h) => RESTART_HELP[h]).filter(Boolean);
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 hosts listed in \`help\` so they pick up the new skills, agents and MCP servers.`
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
- hosts: [...perHost.values()], actions: results, manual, errors,
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
- ({ hosts: ctx.hosts } = await resolveHosts(ctx, 'uninstall'));
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.hosts);
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 === HOSTS.length) {
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 hosts.` + (kept.length ? ` ${kept.length} file(s) were left in place because you edited them — they are listed under \`kept\`.` : ''),
470
- help: [...ctx.hosts.map((h) => RESTART_HELP[h]).filter(Boolean), `${RUN} status`],
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 hosts = { ...lock.hosts };
488
- for (const host of selected) delete hosts[host];
489
- return { ...lock, files, json_pointers, rules, fences, hosts };
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 };