@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 +13 -0
- package/README.md +18 -8
- package/bin/cli.mjs +10 -4
- package/lib/commands.mjs +80 -18
- package/lib/guide.mjs +14 -6
- package/lib/settings.mjs +77 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/sw-setup/SKILL.md +6 -7
- package/skills/sw-setup/reference/rows.md +27 -17
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
|
-
| `
|
|
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
|
|
72
|
-
needs no question); else, with a terminal
|
|
73
|
-
else exit 2 with the four
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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
|
-
* `
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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 === '
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+
6. \`${RUN} uninstall --yes\`
|
|
44
52
|
Removes what the lock file records and reverts the managed config blocks.
|
|
45
53
|
|
|
46
54
|
## Flags
|
package/lib/settings.mjs
ADDED
|
@@ -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.
|
|
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.
|
|
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)",
|
package/skills/sw-setup/SKILL.md
CHANGED
|
@@ -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`,
|
|
54
|
-
|
|
55
|
-
environment row's check from `reference/rows.md`
|
|
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
|
-
- [
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
|
82
|
-
in
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
|
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.
|