@execuro-sw-ecosystem/sw-ecosystem-agentic-harness 0.1.12 → 0.1.13

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,19 @@ release.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.13] - 2026-10-21
11
+
12
+ ### Changed
13
+
14
+ - **The installer sets up only the coding agents a project targets.** The
15
+ target agents are anchored in `sw-ecosystem-agentic-harness.json`, next to
16
+ the lock file, by `agents --agent <a>` or by the first `install` with
17
+ `--agent` or the picker. Marker directories such as a bare `.vscode/` only
18
+ produce candidates and never make an agent a target. Once anchored,
19
+ `status` judges only those agents (the others report `not-targeted`),
20
+ `apply --yes` runs without `--agent`, and `uninstall` prunes what it removes
21
+ from the targets. `sw-setup` and its rows reflect the new `agents` command.
22
+
10
23
  ## [0.1.12] - 2026-09-25
11
24
 
12
25
  ### Changed
package/README.md CHANGED
@@ -56,7 +56,8 @@ stops with one line saying so.
56
56
  | Command | What it does |
57
57
  | --- | --- |
58
58
  | `install` | installs into the resolved coding agent(s) (see Flags below); the same as `apply --yes` |
59
- | `status` | read-only; what is installed, what drifted, and each agent's `skills_dir` |
59
+ | `agents` | shows, or with `--agent` anchors, the coding agents this project targets |
60
+ | `status` | read-only; the target agents, what is installed, what drifted, and each agent's `skills_dir` |
60
61
  | `plan` | the exact changes, written as a diff; writes nothing |
61
62
  | `apply --yes` | performs them |
62
63
  | `uninstall --yes` | removes what the lock file records |
@@ -68,12 +69,21 @@ directory), `--yes` (required by `apply` and `uninstall` — `install` implies
68
69
  it), `--json` and `--verbose` (see Output below).
69
70
 
70
71
  `install`/`apply` resolve which coding agent(s) to write into, in order:
71
- `--agent` if given; else the agents a lock file already records (a re-run
72
- needs no question); else, with a terminal attached, an interactive picker;
73
- else exit 2 with the four `install --agent <a>` commands as `help` — it never
74
- guesses and never hangs. `status` and `uninstall` are unaffected by this and
75
- keep covering every agent by default; `plan` writes nothing, so with no
76
- `--agent` and no lock it previews all four and says so.
72
+ `--agent` if given; else the anchored target agents; else the agents a lock
73
+ file already records (a re-run needs no question); else, with a terminal
74
+ attached, an interactive picker; else exit 2 with the four
75
+ `install --agent <a>` commands as `help` — it never guesses and never hangs.
76
+ `plan` writes nothing, so with none of these it previews all four and says so.
77
+
78
+ The anchored target agents live in `sw-ecosystem-agentic-harness.json`, next
79
+ to the lock file (`var/sw-ai-sdk/`, else `.sw-ai-sdk/`). Marker directories
80
+ such as a bare `.vscode/` only produce `candidates`; they never make an agent
81
+ a target. `agents --agent <a>` (repeatable) anchors exactly those agents and
82
+ writes nothing else; the first `install` with `--agent` or the picker anchors
83
+ its choice too. Once anchored, `status` judges only those agents (the others
84
+ report `not-targeted`) and `apply --yes` runs without `--agent`. `uninstall`
85
+ keeps covering every agent by default and prunes what it removes from the
86
+ targets.
77
87
 
78
88
  ### Output
79
89
 
@@ -109,7 +119,7 @@ row is ticked.
109
119
 
110
120
  `apply` refuses without `--yes`. Neither `apply` nor `install` prompts for
111
121
  consent — typing the verb, or passing `--yes`, is the consent — but with no
112
- `--agent`, no existing lock and no terminal (Codex's question tool does not
122
+ `--agent`, no anchored targets, no existing lock and no terminal (Codex's question tool does not
113
123
  block outside Plan mode, and CI has no terminal either), it exits 2 rather
114
124
  than guess. Add `--agent claude-code` (repeatable) to install into one agent,
115
125
  and `--scope user` to install into your home directory instead of this
package/bin/cli.mjs CHANGED
@@ -23,6 +23,7 @@ const COMMANDS = {
23
23
  apply: async () => (await import('../lib/commands.mjs')).apply,
24
24
  install: async () => (await import('../lib/commands.mjs')).install,
25
25
  uninstall: async () => (await import('../lib/commands.mjs')).uninstall,
26
+ agents: async () => (await import('../lib/commands.mjs')).agentsCommand,
26
27
  guide: async () => (await import('../lib/guide.mjs')).main,
27
28
  };
28
29
 
@@ -33,6 +34,8 @@ const USAGE = `usage: sw-ecosystem-agentic-harness <command> [options]
33
34
  plan [--agent <a>] [--scope project|user] [--root <path>]
34
35
  apply --yes [--agent <a>] [--scope project|user] [--root <path>]
35
36
  uninstall --yes [--agent <a>] [--scope project|user] [--root <path>]
37
+ agents [--agent <a> ...] [--scope project|user] [--root <path>]
38
+ show, or with --agent anchor, the project's target agents
36
39
  guide print the install protocol
37
40
 
38
41
  install is apply --yes under a friendlier name: the whole install in one
@@ -41,10 +44,13 @@ const USAGE = `usage: sw-ecosystem-agentic-harness <command> [options]
41
44
 
42
45
  <a> is one of: claude-code, codex, copilot, cursor. Repeat --agent to select
43
46
  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
- else, with a terminal attached, an interactive picker; else exit 2 with
46
- the four "... install --agent <a>" commands as help. status and uninstall
47
- keep covering every agent by default.
47
+ given; else the anchored target agents (\`agents --agent <a>\`, stored next
48
+ to the lock file); else the agents already recorded in the lock file, if
49
+ one exists; else, with a terminal attached, an interactive picker; else
50
+ exit 2 with the four "... install --agent <a>" commands as help. An
51
+ explicit choice is anchored on the first install. status reports only the
52
+ anchored agents when there are any; uninstall keeps covering every agent
53
+ by default and prunes the agents it removes from the anchored targets.
48
54
 
49
55
  --scope defaults to project: nothing is written under your home directory
50
56
  unless you ask for it. --root defaults to the current directory.
package/lib/commands.mjs CHANGED
@@ -17,6 +17,7 @@ import * as lockfile from './lock.mjs';
17
17
  import { dedupe, execute, isWritableState, preview, pruneDirs, revert } from './actions.mjs';
18
18
  import { removeDirIfEmpty, removeFile } from './fsx.mjs';
19
19
  import { askAgents } from './prompt.mjs';
20
+ import * as settings from './settings.mjs';
20
21
  import { RESTART } from './render.mjs';
21
22
 
22
23
  const RUN = 'sw-ecosystem-agentic-harness';
@@ -86,33 +87,40 @@ export function buildContext(argv, command) {
86
87
  /**
87
88
  * The agent-resolution chain every write command shares:
88
89
  * 1. `--agent` given → use exactly those. No question.
89
- * 2. else a lock already records agents → reuse them. This is an update;
90
- * the choice was already made. No question. A lock written before the
91
- * rename records them under `hosts`; lib/lock.mjs reads either.
92
- * 3. else a TTY is attached (apply/install only) → ask, via lib/prompt.mjs.
93
- * 4. else → UsageError, exit 2, `help` lists the four runnable commands.
90
+ * 2. else agents are anchored (lib/settings.mjs) → use them. The choice was
91
+ * already made. No question.
92
+ * 3. else a lock already records agents → reuse them. A legacy install; a
93
+ * lock written before the rename records them under `hosts`, which
94
+ * lib/lock.mjs reads either way.
95
+ * 4. else a TTY is attached (apply/install only) → ask, via lib/prompt.mjs.
96
+ * 5. else → UsageError, exit 2, `help` lists the four runnable commands.
94
97
  *
95
- * `status` and `uninstall` keep their old, ungated default of every agent —
96
- * reporting and removal are not consent points. `plan` writes nothing, so it
97
- * never reaches step 3: steps 1-2, then it previews all four and says so.
98
+ * `source` says which step answered; apply anchors the answer of 1 and 4.
99
+ * `status` takes steps 1-2, then every agent; `uninstall` takes step 1, then
100
+ * every agent — reporting and removal are not consent points. `plan` writes
101
+ * nothing, so it never reaches step 4: steps 1-3, then it previews all four.
98
102
  *
99
103
  * Call after `loadLock(ctx)` so `ctx.lock` is populated for step 2.
100
104
  */
101
105
  async function resolveAgents(ctx, command) {
102
- if (ctx.agents.length) return { agents: [...new Set(ctx.agents)], chosen: true };
103
- if (command === 'status' || command === 'uninstall') return { agents: [...AGENTS], chosen: true };
106
+ if (ctx.agents.length) return { agents: [...new Set(ctx.agents)], chosen: true, source: 'flag' };
107
+ if (command === 'uninstall') return { agents: [...AGENTS], chosen: true, source: 'all' };
108
+
109
+ const anchored = settings.read(ctx.root)?.agents ?? [];
110
+ if (anchored.length) return { agents: anchored, chosen: true, source: 'settings' };
111
+ if (command === 'status') return { agents: [...AGENTS], chosen: true, source: 'all' };
104
112
 
105
113
  const recorded = ctx.lock?.agents ? Object.keys(ctx.lock.agents) : [];
106
- if (recorded.length) return { agents: recorded, chosen: true };
114
+ if (recorded.length) return { agents: recorded, chosen: true, source: 'lock' };
107
115
 
108
- if (command === 'plan') return { agents: [...AGENTS], chosen: false };
116
+ if (command === 'plan') return { agents: [...AGENTS], chosen: false, source: 'all' };
109
117
 
110
118
  if (process.stdin.isTTY) {
111
119
  const chosen = await askAgents(AGENTS);
112
- if (chosen) return { agents: chosen, chosen: true };
120
+ if (chosen) return { agents: chosen, chosen: true, source: 'picker' };
113
121
  }
114
122
  throw new UsageError(
115
- 'no --agent given, this repository has no recorded install, and there is no terminal to ask which coding agent(s) to use',
123
+ 'no --agent given, no anchored target agents, this repository has no recorded install, and there is no terminal to ask which coding agent(s) to use',
116
124
  AGENTS.map((a) => `${RUN} ${command} --agent ${a}`),
117
125
  );
118
126
  }
@@ -162,6 +170,39 @@ function publicAction(a) {
162
170
  return rest;
163
171
  }
164
172
 
173
+ /** The anchored targets as every result reports them. */
174
+ function targetsOf(root, anchored) {
175
+ return {
176
+ agents: anchored?.agents ?? [],
177
+ source: anchored ? 'settings' : 'none',
178
+ settings_file: settings.settingsFile(root),
179
+ };
180
+ }
181
+
182
+ // ------------------------------------------------------------------ agents
183
+
184
+ /**
185
+ * Read or anchor the target agents. Detection only yields `candidates`; the
186
+ * write touches the settings file and nothing else.
187
+ */
188
+ export async function agentsCommand(argv) {
189
+ const ctx = buildContext(argv, 'agents');
190
+ const candidates = (await detectAll(ctx)).filter((d) => d.detected).map((d) => d.id);
191
+ let anchored = settings.read(ctx.root);
192
+ if (ctx.agents.length) {
193
+ const same = ctx.agents.length === candidates.length && ctx.agents.every((a) => candidates.includes(a));
194
+ anchored = settings.write(ctx.root, ctx.agents, same ? 'detected' : 'chosen');
195
+ }
196
+ return out.ok('agents', head(ctx, {
197
+ next_step: anchored
198
+ ? `Target agents: ${anchored.agents.join(', ')}. Run \`${RUN} status\` to check them.`
199
+ : `Nothing is anchored — run \`${RUN} agents --agent <id>\` (candidates: ${candidates.join(', ') || 'none'}).`,
200
+ help: [`${RUN} status`, ...AGENTS.map((a) => `${RUN} agents --agent ${a}`)],
201
+ targets: targetsOf(ctx.root, anchored),
202
+ candidates,
203
+ }));
204
+ }
205
+
165
206
  // ------------------------------------------------------------------ status
166
207
 
167
208
  export async function status(argv) {
@@ -176,7 +217,11 @@ export async function status(argv) {
176
217
  }
177
218
  throw e;
178
219
  }
179
- ({ agents: ctx.agents } = await resolveAgents(ctx, 'status'));
220
+ const resolved = await resolveAgents(ctx, 'status');
221
+ ctx.agents = resolved.agents;
222
+ // Untargeted only means something once a choice exists (`--agent` or anchored).
223
+ const narrowed = resolved.source === 'flag' || resolved.source === 'settings';
224
+ const anchored = settings.read(ctx.root);
180
225
 
181
226
  const detections = await detectAll(ctx);
182
227
  const { actions, errors } = previewAll(ctx);
@@ -184,8 +229,10 @@ export async function status(argv) {
184
229
 
185
230
  const agentStates = detections.map((d) => {
186
231
  const mine = actions.filter((a) => a.host === d.id || a.host === 'shared');
232
+ const targeted = !narrowed || ctx.agents.includes(d.id);
187
233
  let state = 'absent';
188
- if (d.detected) {
234
+ if (!targeted) state = 'not-targeted';
235
+ else if (d.detected) {
189
236
  const pending = mine.filter((a) => isWritableState(a.state));
190
237
  const bad = mine.filter((a) => a.state === 'conflict' || a.state === 'drift');
191
238
  if (bad.length) state = bad.some((a) => a.state === 'conflict') ? 'conflict' : 'drift';
@@ -193,7 +240,7 @@ export async function status(argv) {
193
240
  else state = lock?.agents?.[d.id] ? 'drift' : 'not-installed';
194
241
  }
195
242
  return {
196
- id: d.id, detected: d.detected, marker: d.marker, writable: d.writable,
243
+ id: d.id, detected: d.detected, targeted, marker: d.marker, writable: d.writable,
197
244
  skills_dir: skillsDir(d.id, ctx),
198
245
  installed: installedCounts(d.id, ctx), state, notes: d.notes,
199
246
  };
@@ -209,15 +256,22 @@ export async function status(argv) {
209
256
  const pending = actions.filter((a) => isWritableState(a.state)).length;
210
257
  const worst = ['conflict', 'drift', 'not-installed', 'ok', 'absent']
211
258
  .find((s) => agentStates.some((a) => a.state === s)) ?? 'absent';
259
+ const candidates = detections.filter((d) => d.detected).map((d) => d.id);
212
260
 
213
261
  let next = pending
214
262
  ? `Run \`${RUN} plan\` to see the ${pending} change(s) needed, then \`${RUN} apply --yes\`.`
215
263
  : `Nothing to do — the harness is installed and up to date. Run \`${RUN} plan\` any time to re-check.`;
264
+ if (!anchored) {
265
+ next = `No target agents are anchored yet — run \`${RUN} agents --agent <id>\` (candidates: ${candidates.join(', ') || 'none'}). ${next}`;
266
+ }
216
267
 
217
268
  return out.ok('status', head(ctx, {
218
269
  installed_version: lock?.version ?? null,
219
270
  state: worst,
220
271
  next_step: next,
272
+ targets: targetsOf(ctx.root, anchored),
273
+ candidates,
274
+ lock_file: lockFile(ctx.root),
221
275
  help: [`${RUN} plan`, `${RUN} apply --yes`, `${RUN} guide`],
222
276
  inventory: { skills: skills().length, agents: agents().length },
223
277
  agents: agentStates, drift,
@@ -281,7 +335,8 @@ export async function apply(argv, label = 'apply') {
281
335
  }
282
336
  throw e;
283
337
  }
284
- ({ agents: ctx.agents } = await resolveAgents(ctx, label));
338
+ const resolved = await resolveAgents(ctx, label);
339
+ ctx.agents = resolved.agents;
285
340
 
286
341
  // An apply we cannot record is an apply we cannot undo. Refuse up front
287
342
  // rather than leave the user with files and no way back.
@@ -359,6 +414,11 @@ export async function apply(argv, label = 'apply') {
359
414
  lock = { ...lock, agents: { ...lock.agents, [agent]: { installed_at: new Date().toISOString(), version: PKG_VERSION } } };
360
415
  }
361
416
  lockfile.write(ctx.root, lock);
417
+ settings.migrate(ctx.root);
418
+ // The first explicit choice becomes the project's target agents.
419
+ if ((resolved.source === 'flag' && !settings.read(ctx.root)) || resolved.source === 'picker') {
420
+ settings.write(ctx.root, ctx.agents, 'chosen');
421
+ }
362
422
 
363
423
  const summary = {
364
424
  created: results.filter((r) => r.state === 'created').length,
@@ -411,6 +471,7 @@ export async function uninstall(argv) {
411
471
  ({ agents: ctx.agents } = await resolveAgents(ctx, 'uninstall'));
412
472
 
413
473
  if (!lock) {
474
+ settings.prune(ctx.root, ctx.agents);
414
475
  // No record means no evidence of what we own. Guessing here would delete
415
476
  // someone else's files, so it does nothing and says so.
416
477
  return out.ok('uninstall', head(ctx, {
@@ -447,6 +508,7 @@ export async function uninstall(argv) {
447
508
 
448
509
  const prunedDirs = pruneDirs(lock.dirs ?? [], ctx);
449
510
  const failed = errors.length;
511
+ if (!failed) settings.prune(ctx.root, [...selected]);
450
512
 
451
513
  if (failed === 0 && survivors.length === 0 && selected.size === AGENTS.length) {
452
514
  // Everything we recorded is gone; the lock has nothing left to describe.
package/lib/guide.mjs CHANGED
@@ -21,26 +21,34 @@ registers the MCP servers and permission rules they depend on.
21
21
  The whole install in one command. It never prompts for consent — typing
22
22
  the verb is the consent — but it does still need to know **which** coding
23
23
  agent(s) to write into, resolved in order: \`--agent\`, if given; else the
24
- agents a lock file already records, if this is a re-run; else, with a real
24
+ anchored target agents (step 2); else the agents a lock file already
25
+ records, if this is a re-run; else, with a real
25
26
  terminal attached, an interactive picker; else it exits 2 with the four
26
27
  \`${RUN} install --agent <a>\` commands as \`help\` rather than guess or
27
28
  hang — Codex's question tool does not block outside Plan mode, and CI has
28
29
  no terminal, so a skill invoking this must always pass \`--agent\`.
29
30
 
30
- 2. \`${RUN} status\`
31
- Read-only, optional. Reports which agents are present, what is already
31
+ 2. \`${RUN} agents [--agent <a> ...]\`
32
+ Reads, or with \`--agent\` anchors, the coding agents this project
33
+ targets, in \`sw-ecosystem-agentic-harness.json\` next to the lock file.
34
+ Marker directories only produce \`candidates\`; an explicit first
35
+ \`--agent\` choice on \`install\` is anchored too. Once anchored, \`status\`
36
+ judges only those agents and \`install\` needs no \`--agent\`.
37
+
38
+ 3. \`${RUN} status\`
39
+ Read-only, optional. Reports the target agents, what is already
32
40
  installed, and anything that has drifted since it was installed. Always safe.
33
41
 
34
- 3. \`${RUN} plan\`
42
+ 4. \`${RUN} plan\`
35
43
  Prints the exact set of changes and writes nothing. Every entry says what it
36
44
  would do and why. Show this to the user before applying.
37
45
 
38
- 4. \`${RUN} apply --yes\`
46
+ 5. \`${RUN} apply --yes\`
39
47
  The scriptable name for the same write \`install\` performs — use it for
40
48
  re-runs and repairs. It refuses without \`--yes\`, and resolves which
41
49
  agent(s) to touch the same way \`install\` does (see step 1).
42
50
 
43
- 5. \`${RUN} uninstall --yes\`
51
+ 6. \`${RUN} uninstall --yes\`
44
52
  Removes what the lock file records and reverts the managed config blocks.
45
53
 
46
54
  ## Flags
@@ -0,0 +1,77 @@
1
+ // The anchored target agents: `sw-ecosystem-agentic-harness.json`, next to the lock (see
2
+ // `lockDir()` in content.mjs).
3
+ //
4
+ // The lock records what was *installed*; this records what the project
5
+ // *targets*. Marker directories only ever produce candidates — a stray empty
6
+ // `.vscode/` must not make Copilot a target — so every command that has to
7
+ // pick agents reads this file first. Only the CLI writes it, per checkout: it
8
+ // lives in the ignored state directory, so each machine anchors once.
9
+
10
+ import { dirname } from 'node:path';
11
+ import { LOCK_DIR, lockDir } from './content.mjs';
12
+ import {
13
+ abs, ensureDir, exists, readTextMaybe, removeDirIfEmpty, removeFile, writeAtomic,
14
+ } from './fsx.mjs';
15
+ import { AGENTS } from './hosts.mjs';
16
+
17
+ const NAME = 'sw-ecosystem-agentic-harness.json';
18
+
19
+ export function settingsFile(root) {
20
+ return `${lockDir(root)}/${NAME}`;
21
+ }
22
+
23
+ /** The pre-`var/` location; read only so an anchored project survives the move. */
24
+ function legacyPath(root) {
25
+ return abs(root, `${LOCK_DIR}/${NAME}`);
26
+ }
27
+
28
+ /** The anchored agents, or null when nothing is anchored (or the file is unusable). */
29
+ export function read(root) {
30
+ const found = readTextMaybe(abs(root, settingsFile(root))) ?? readTextMaybe(legacyPath(root));
31
+ if (found === null) return null;
32
+ let parsed;
33
+ try { parsed = JSON.parse(found.text); } catch { return null; }
34
+ const agents = (Array.isArray(parsed?.agents) ? parsed.agents : []).filter((a) => AGENTS.includes(a));
35
+ return agents.length ? { ...parsed, agents: [...new Set(agents)] } : null;
36
+ }
37
+
38
+ export function write(root, agents, source) {
39
+ const settings = {
40
+ schema: 1, agents: [...new Set(agents)], anchored_at: new Date().toISOString(), source,
41
+ };
42
+ const target = abs(root, settingsFile(root));
43
+ ensureDir(dirname(target));
44
+ writeAtomic(target, `${JSON.stringify(settings, null, 2)}\n`);
45
+ dropLegacy(root, target);
46
+ return settings;
47
+ }
48
+
49
+ function dropLegacy(root, target) {
50
+ const legacy = legacyPath(root);
51
+ if (legacy !== target && exists(legacy)) {
52
+ removeFile(legacy);
53
+ removeDirIfEmpty(dirname(legacy));
54
+ }
55
+ }
56
+
57
+ /** Move a settings file left at the old path to the current one. */
58
+ export function migrate(root) {
59
+ const current = read(root);
60
+ if (current && legacyPath(root) !== abs(root, settingsFile(root)) && exists(legacyPath(root))) write(root, current.agents, current.source ?? 'chosen');
61
+ }
62
+
63
+ /** Drop agents from the targets; the file goes when none are left. */
64
+ export function prune(root, removed) {
65
+ const current = read(root);
66
+ if (!current) return;
67
+ const left = current.agents.filter((a) => !removed.includes(a));
68
+ if (left.length === current.agents.length) return;
69
+ if (left.length) {
70
+ write(root, left, current.source ?? 'chosen');
71
+ return;
72
+ }
73
+ const target = abs(root, settingsFile(root));
74
+ removeFile(target);
75
+ removeDirIfEmpty(dirname(target));
76
+ dropLegacy(root, target);
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@execuro-sw-ecosystem/sw-ecosystem-agentic-harness",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Shopware 6 agentic harness: sw-* skills and sub-agents, plus an installer that writes them into Claude Code, Codex, GitHub Copilot and Cursor.",
5
5
  "license": "MIT",
6
6
  "author": "Execuro UG (haftungsbeschränkt)",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "sw-ecosystem-agentic-harness",
4
- "version": "0.1.12",
4
+ "version": "0.1.13",
5
5
  "description": "Shopware 6 agentic harness: sw-* skills and sub-agents for requirements, specs, implementation, verification and documentation, with an installer CLI that writes them into Claude Code, Codex, GitHub Copilot and Cursor. The knowledge-base MCP and the editor tools are separate npm packages.",
6
6
  "author": {
7
7
  "name": "Execuro UG (haftungsbeschränkt)",
@@ -50,10 +50,9 @@ npx -y @execuro-sw-ecosystem/sw-ecosystem-agentic-harness@latest status --json
50
50
 
51
51
  `--json` is mandatory for every call this skill makes: without it the CLI
52
52
  prints a human summary, not the object below. It prints one JSON object
53
- (`ok`, `next_step`, `help`, and its own `agents[]`/`drift[]` configuration
54
- state). Alongside it, run every
55
- environment row's check from `reference/rows.md` in this skill's directory —
56
- read-only, seconds each: file/directory existence, one version command, one
53
+ (`ok`, `next_step`, `help`, `targets`, `agents[]`, `drift[]`). First settle
54
+ the target agents (`reference/rows.md`, Target agents); alongside, run every
55
+ environment row's check from `reference/rows.md` — read-only, seconds each: file/directory existence, one version command, one
57
56
  `kb_status` call, one `npm view` registry lookup (Package updates row) — the
58
57
  one part of step 1 that touches the network, so it is the slow part.
59
58
  Never write, never install, never ask a question in this step.
@@ -74,7 +73,8 @@ one line per configuration item the CLI's `agents[]`/`drift[]` reports:
74
73
  - [x] Plugin tests — AhCheckout: phpunit + jest ok
75
74
  - [ ] Project wiki — docs/project-wiki/ missing
76
75
  - [ ] Package updates — harness 0.1.0 → 0.1.3
77
- - [ ] Installer config — .mcp.json entry: drift (from CLI status)
76
+ - [x] Target agents — claude-code (anchored)
77
+ - [ ] Installer config — .mcp.json entry: drift
78
78
  ```
79
79
 
80
80
  Per-plugin rows print one line per directory under `custom/plugins/`, e.g.
@@ -87,8 +87,7 @@ Print one closing line: `Environment ready.` Stop. No question.
87
87
  ### 3. Missing rows
88
88
 
89
89
  For a missing environment row, research only what it needs: the guideline
90
- sections `reference/rows.md` cites for that row
91
- (`shopware-project-qa-guidelines.md` §3.3–§3.7, §6.1), `compose.yaml` for the
90
+ sections `reference/rows.md` cites for that row, `compose.yaml` for the
92
91
  service name, `custom/plugins/*` names for per-plugin rows.
93
92
 
94
93
  For a missing/drifted configuration row, do not research or plan the fix
@@ -11,16 +11,25 @@ Every installer-CLI call this skill makes passes `--json`. Without it the CLI
11
11
  prints a short human summary instead of the object these rows read — that
12
12
  summary is for the person typing the command, never for a skill parsing it.
13
13
 
14
- When that `status --json` call reports no install at all (no lock file, so no
15
- recorded agents), the installer CLI's `apply --yes`/`install` has no `--agent`
16
- to reuse and, run from this skill, no terminal to ask on — it would exit 2.
17
- Do not pass a guessed `--agent`. Report this to the user instead: the first
18
- install must be run by them directly in a terminal, e.g.
19
- `npx -y @execuro-sw-ecosystem/sw-ecosystem-agentic-harness@latest install`,
20
- which then either takes their `--agent` or prompts them interactively. Re-run
21
- this skill's `status` check afterwards — every later `apply --yes --json`
22
- reuses the agents that first run recorded, and this skill can call it
23
- directly again. Expect `agents[]` in every output this skill reads.
14
+ ## Target agents
15
+
16
+ Every row below is judged against the **anchored target agents** only, never
17
+ against marker directories: a stray `.vscode/` makes Copilot a candidate, not a
18
+ target. Read `targets` and `candidates` from the step-1 `status --json`.
19
+
20
+ - `targets.source` is `settings`: use `targets.agents`. Ask nothing.
21
+ - `none` and exactly one candidate: anchor it with
22
+ `npx -y @execuro-sw-ecosystem/sw-ecosystem-agentic-harness@latest agents --agent <id> --json`
23
+ and say so in one line. This is the one write allowed before the question.
24
+ - `none` and zero or several candidates: ask one multi-select question listing
25
+ the four agents (`claude-code`, `codex`, `copilot`, `cursor`), detected ones
26
+ marked, then anchor the answer the same way (one `--agent` per pick). Ask
27
+ with the structured question tool as in `SKILL.md` step 3.
28
+ - A later change of mind goes through the same `agents --agent …` call.
29
+
30
+ Re-run `status --json` after anchoring and read `agents[]` from that. An agent
31
+ whose `state` is `not-targeted` is never printed or acted on. Print
32
+ `- [x] Target agents — <ids> (anchored)` as the first configuration line.
24
33
 
25
34
  ## 1. vendor/
26
35
 
@@ -69,7 +78,7 @@ So this row never blocks readiness, and it never probes. It asks.
69
78
 
70
79
  - **Check:** read `agents[]` from the `status --json` output step 1 already
71
80
  ran. Each entry carries `id` and `skills_dir` — the directory that agent
72
- actually reads skills from. For each agent whose `state` is not `absent`,
81
+ actually reads skills from. For each target agent (`targeted` is true),
73
82
  list `<skills_dir>/sw-specs-editor/` and
74
83
  `<skills_dir>/sw-tender-discovery-tool/`: a `SKILL.md` there means that
75
84
  editor is installed for that agent. That directory read is the whole check —
@@ -78,12 +87,12 @@ So this row never blocks readiness, and it never probes. It asks.
78
87
  state, not something to go looking for. Also read
79
88
  `sw-setup-editors.json` from the installer's own state directory if it
80
89
  exists — this skill's note of what the user has already declined
81
- (`{"sw-specs-editor": "declined"}`). That directory is whatever `lock_file`
82
- in the step-1 `status --json` output names, minus the file name — normally
90
+ (`{"sw-specs-editor": "declined"}`). That directory is the one
91
+ `settings_file` in `targets` names, minus the file name — normally
83
92
  `var/sw-ai-sdk/`, which a Shopware project already ignores. Read the path;
84
93
  never hard-code it.
85
94
  - **Ticked:** `[x]` when the editor's `SKILL.md` is present in every
86
- non-`absent` agent's `skills_dir`; `[-]` when the note records a decline, or
95
+ target agent's `skills_dir`; `[-]` when the note records a decline, or
87
96
  when it is installed for some agents but not all (say which). `[ ]` only for
88
97
  an editor that is neither installed nor declined — and even then it does not
89
98
  hold the table open: report it as optional and move on.
@@ -97,7 +106,7 @@ So this row never blocks readiness, and it never probes. It asks.
97
106
  `--editor` and the `.xlsx` import on `sw-discover-tender`. Install?"
98
107
 
99
108
  On yes, for each accepted editor, run its own installer once per
100
- non-`absent` agent, passing that agent's `skills_dir` from `status --json`:
109
+ target agent, passing that agent's `skills_dir` from `status --json`:
101
110
 
102
111
  ```
103
112
  npx -y @execuro-sw-ecosystem/sw-specs-editor@latest install-skill --target <skills_dir>
@@ -130,7 +139,7 @@ So this row never blocks readiness, and it never probes. It asks.
130
139
  <skills_dir>` and re-run the removal" — and carry on with the other agents
131
140
  and the other editor.
132
141
  - **On no:** write the decline to `sw-setup-editors.json` in the directory
133
- `lock_file` names (create the file, and that directory, if absent; it is
142
+ `targets.settings_file` names (create the file, and that directory, if absent; it is
134
143
  never tracked) and do not
135
144
  offer that editor again on any later run. The user asking for it by name is
136
145
  the only thing that re-opens it — then delete its key and install. `--editor`
@@ -305,7 +314,8 @@ This row never blocks readiness. It folds into step 3's single question.
305
314
  npx -y @execuro-sw-ecosystem/sw-ecosystem-agentic-harness@latest install
306
315
  ```
307
316
 
308
- That command rewrites the installed `sw-setup` skill itself, so the newer
317
+ That command reuses the anchored target agents and rewrites the installed
318
+ `sw-setup` skill itself, so the newer
309
319
  skill takes effect on the next run — which step 3 already causes by re-running
310
320
  step 1. For the editors, see row 3: re-running their `install-skill` line is
311
321
  their update.