@execuro-sw-ecosystem/sw-ecosystem-agentic-harness 0.1.7 → 0.1.9

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,64 @@ release.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.9] - 2026-09-19
11
+
12
+ ### Changed
13
+
14
+ - `README.md` names `sw-setup` as part of the installation flow. Documentation only.
15
+
16
+ ## [0.1.8] - 2026-09-19
17
+
18
+ ### Changed
19
+
20
+ - **The lock file moved to `var/sw-ai-sdk/harness.lock.json`, and the
21
+ `.gitignore` line is gone with it.** A Shopware project ignores the whole of
22
+ `var/` through the `shopware/core` Flex recipe (`/var/*`, with a single
23
+ `!/var/.htaccess` negation) and already keeps its machine-local state there
24
+ — `var/cache`, `var/log` — so the installer no longer creates a directory in
25
+ the repository root and no longer writes to `.gitignore` at all. A root with
26
+ no `var/` (`--scope user` under $HOME, or a checkout that is not a Shopware
27
+ project) keeps `.sw-ai-sdk/` and its managed line, unchanged. Nothing here
28
+ reads a path outside the install root, on any platform.
29
+ **Migration is automatic and lossless:** a lock still at the old path is
30
+ read, rewritten to the new one, and the old file, its directory and the
31
+ managed `.gitignore` block are removed. Declined rules, recorded agents and
32
+ file hashes all survive, so the first run after upgrading reinstalls nothing
33
+ — it reports `migrated[]` in `--json` and one line in the human summary.
34
+ `lock_file` in every result body now carries the resolved path; anything
35
+ that hard-coded `.sw-ai-sdk/` should read that key instead.
36
+
37
+ ### Removed
38
+
39
+ - **BREAKING: the "extra component" mechanism is gone.** The CLI no longer
40
+ detects, installs, updates or removes the two optional visual editors
41
+ (Specs Editor, Tender Discovery Tool); each package installs its own skill,
42
+ orchestrated by the `sw-setup` skill. Removed with it: the
43
+ `extra_components[]` key from `status --json`, the `--no-extra-components`
44
+ flag on `install`/`plan`/`apply`, the `skipped` field from `plan`'s and
45
+ `apply`'s `summary` (nothing else could produce that state), the
46
+ `skill-copy` action kind, and the "Optional extra components" section of
47
+ `guide`. Anything reading `extra_components[]` or counting `summary.skipped`
48
+ must stop.
49
+ **Why:** the mechanism had never once worked. It spawned `npx --no-install
50
+ <pkg>@<pin> install-skill --print` and required stdout to begin with `---`,
51
+ but both editors document a `source:`/`next_step:` preamble before the skill
52
+ document, so the check could never pass and neither skill was ever installed.
53
+ `--no-install` also suppresses installing but not resolving, so every
54
+ `status`, `plan` and `apply` issued a real request to the npm registry —
55
+ `status` was never offline-safe, contrary to what the module claimed.
56
+ - The two editors are now invoked as `@latest` everywhere, including in the
57
+ `allowed-tools` permission grants of `sw-design-requirements`,
58
+ `sw-design-solution` and `sw-discover-tender`, which still named an exact
59
+ version and would therefore have denied the calls. The `@latest` check now
60
+ guards `allowed-tools` lines as well as prose.
61
+
62
+ ### Added
63
+
64
+ - `status --json` reports `skills_dir` per agent — the directory that agent
65
+ actually reads skills from, and the `--target` `sw-setup` hands to an
66
+ editor package's own `install-skill`/`uninstall-skill`.
67
+
10
68
  ## [0.1.7] - 2026-09-19
11
69
 
12
70
  ### Changed
package/README.md CHANGED
@@ -43,16 +43,20 @@ identity, not an npm package name — and can be run standalone in any stdio MCP
43
43
  client without this harness.
44
44
 
45
45
  The two editors are **optional add-ons**, and each ships its own skill inside
46
- its package rather than here. `sw-setup` offers to install them; a host that
47
- declines is fully set up without them, and `--editor` on `sw-design-requirements`,
48
- `sw-design-solution` or `sw-discover-tender` then stops with one line saying so.
46
+ its package rather than here. They are not this CLI's business at all: it
47
+ neither detects nor installs them. `sw-setup` owns their whole lifecycle —
48
+ it offers them, and installs, updates and removes each one with that
49
+ package's own `install-skill`/`uninstall-skill --target <skills dir>`. A
50
+ project that declines is fully set up without them, and `--editor` on
51
+ `sw-design-requirements`, `sw-design-solution` or `sw-discover-tender` then
52
+ stops with one line saying so.
49
53
 
50
54
  ## Commands
51
55
 
52
56
  | Command | What it does |
53
57
  | --- | --- |
54
58
  | `install` | installs into the resolved coding agent(s) (see Flags below); the same as `apply --yes` |
55
- | `status` | read-only; what is installed, what drifted, which optional extra components are present |
59
+ | `status` | read-only; what is installed, what drifted, and each agent's `skills_dir` |
56
60
  | `plan` | the exact changes, written as a diff; writes nothing |
57
61
  | `apply --yes` | performs them |
58
62
  | `uninstall --yes` | removes what the lock file records |
@@ -61,8 +65,7 @@ declines is fully set up without them, and `--editor` on `sw-design-requirements
61
65
  Flags: `--agent claude-code|codex|copilot|cursor` (repeatable), `--scope
62
66
  project|user` (default `project`), `--root <path>` (default the current
63
67
  directory), `--yes` (required by `apply` and `uninstall` — `install` implies
64
- it), `--no-extra-components` (skip detection of the optional editor
65
- packages), `--json` and `--verbose` (see Output below).
68
+ it), `--json` and `--verbose` (see Output below).
66
69
 
67
70
  `install`/`apply` resolve which coding agent(s) to write into, in order:
68
71
  `--agent` if given; else the agents a lock file already records (a re-run
@@ -97,6 +100,13 @@ writes anything.
97
100
  Then restart your coding agent — every host reads its configuration at startup.
98
101
  The command's output tells you which ones and how.
99
102
 
103
+ Then run the `sw-setup` skill in that agent. The installer only writes the
104
+ harness files; `sw-setup` checks and completes everything else the skills need
105
+ — `vendor/`, Node, `shopware-cli`, the knowledge-base MCP, the acceptance-test
106
+ project and its Playwright browsers, the per-plugin test scaffolding, and the
107
+ optional editor add-ons. It prints one readiness table and is done when every
108
+ row is ticked.
109
+
100
110
  `apply` refuses without `--yes`. Neither `apply` nor `install` prompts for
101
111
  consent — typing the verb, or passing `--yes`, is the consent — but with no
102
112
  `--agent`, no existing lock and no terminal (Codex's question tool does not
@@ -118,6 +128,17 @@ unless you ask.
118
128
  `AGENTS.md` gets a managed block too — Codex, Copilot and Cursor read it, and
119
129
  Codex never reads `CLAUDE.md`.
120
130
 
131
+ The installer's own state — the lock file recording what it wrote — goes to
132
+ `var/sw-ai-sdk/harness.lock.json`. A Shopware project already ignores the whole
133
+ of `var/` through the `shopware/core` Flex recipe (`/var/*`) and already keeps
134
+ its machine-local state there, so the repository root stays clean and **no
135
+ `.gitignore` line is written at all**. A root with no `var/` — `--scope user`
136
+ under your home directory, or a checkout that is not a Shopware project —
137
+ falls back to `.sw-ai-sdk/` and gets the managed `.gitignore` line as before.
138
+ An install recorded at the old path is moved on the next `install`, which also
139
+ takes back the `.gitignore` line it no longer needs; nothing is reinstalled and
140
+ no declined rule is forgotten.
141
+
121
142
  ### What it will not do
122
143
 
123
144
  - **It never overwrites a file it did not install.** A file already at one of
@@ -137,7 +158,7 @@ Undo everything with:
137
158
  npx -y @execuro-sw-ecosystem/sw-ecosystem-agentic-harness@latest uninstall --yes
138
159
  ```
139
160
 
140
- It removes exactly what the lock file at `.sw-ai-sdk/harness.lock.json` records,
161
+ It removes exactly what the lock file at `var/sw-ai-sdk/harness.lock.json` records,
141
162
  keeps anything you hand-edited, and tells you what it kept.
142
163
 
143
164
  ### From a skill
@@ -160,16 +181,16 @@ Three things update on different schedules.
160
181
  | --- | --- |
161
182
  | This package — skills, sub-agents, permission rules, MCP registrations | `npx -y @execuro-sw-ecosystem/sw-ecosystem-agentic-harness@latest install` |
162
183
  | The MCP servers (`ShopwareDevKnowledgeBase`, `playwright`) | By themselves. They are registered as `@latest`, so your agent fetches the newest build the next time it starts one. Nothing to run. |
163
- | Specs Editor, Tender Discovery Tool | You update them. Each has its own line — see below. |
184
+ | Specs Editor, Tender Discovery Tool | `sw-setup` does, by re-running each package's own `install-skill` at `@latest` — see below. |
164
185
 
165
186
  ### The optional editors
166
187
 
167
- Install or update each at the exact version this release pins — the CLI
168
- probes for that version and skips the skill if it finds another:
188
+ They are installed, updated and removed by the `sw-setup` skill, not by this
189
+ CLI, and there is no version to match against this package:
169
190
 
170
191
  ```bash
171
- npm i -D @execuro-sw-ecosystem/sw-specs-editor@0.1.0
172
- npm i -D @execuro-sw-ecosystem/sw-tender-discovery-tool@0.1.0
192
+ npx -y @execuro-sw-ecosystem/sw-specs-editor@latest install-skill --target <skills dir>
193
+ npx -y @execuro-sw-ecosystem/sw-tender-discovery-tool@latest install-skill --target <skills dir>
173
194
  ```
174
195
 
175
196
  - `sw-specs-editor` backs the `--editor` flag on `sw-design-requirements` and
@@ -177,9 +198,11 @@ npm i -D @execuro-sw-ecosystem/sw-tender-discovery-tool@0.1.0
177
198
  - `sw-tender-discovery-tool` backs the `--editor` flag and the `.xlsx` import
178
199
  on `sw-discover-tender`.
179
200
 
180
- Re-run `install` afterwards so the new skill lands in each host. `status`'s
181
- `extra_components[]` array reports the pinned `version` and the exact `install`
182
- command for whichever one is missing (`status --json`).
201
+ `<skills dir>` is the `skills_dir` this CLI reports for that agent in
202
+ `status --json`. Re-running the same line is the update — `@latest` plus the
203
+ editor's own "already current" path makes it idempotent — and
204
+ `uninstall-skill` with the same `--target` removes it. Nothing needs to be
205
+ re-run here afterwards.
183
206
 
184
207
  Re-running `install` is safe at any time: it rewrites only what it owns,
185
208
  leaves anything you hand-edited alone (reported as drift), and records the new
@@ -192,8 +215,8 @@ in this project's `node_modules`, so you would silently keep running the
192
215
  version you first installed.
193
216
 
194
217
  To look before you change anything: `... @latest status` reports
195
- `installed_version`, any drift, and which extra components are present. It
196
- writes nothing.
218
+ `installed_version`, any drift, and each agent's `skills_dir`. It writes
219
+ nothing.
197
220
 
198
221
  ## Prerequisites
199
222
 
package/SECURITY.md CHANGED
@@ -41,7 +41,7 @@ scripts** on install, which removes a large part of the usual npm attack
41
41
  surface.
42
42
 
43
43
  Out of scope: vulnerabilities in the coding agents themselves, in the
44
- separately published extra components (they have their own repositories
44
+ separately published visual editors (they have their own repositories
45
45
  and their own reporting), or in a third-party MCP server this package merely
46
46
  registers, such as Playwright's (report those upstream).
47
47
 
package/bin/cli.mjs CHANGED
@@ -28,10 +28,10 @@ const COMMANDS = {
28
28
 
29
29
  const USAGE = `usage: sw-ecosystem-agentic-harness <command> [options]
30
30
 
31
- install [--agent <a>] [--scope project|user] [--root <path>] [--no-extra-components]
31
+ install [--agent <a>] [--scope project|user] [--root <path>]
32
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]
33
+ plan [--agent <a>] [--scope project|user] [--root <path>]
34
+ apply --yes [--agent <a>] [--scope project|user] [--root <path>]
35
35
  uninstall --yes [--agent <a>] [--scope project|user] [--root <path>]
36
36
  guide print the install protocol
37
37
 
@@ -49,8 +49,10 @@ const USAGE = `usage: sw-ecosystem-agentic-harness <command> [options]
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.
51
51
 
52
- --no-extra-components skips the two optional extra-component skills (Specs
53
- Editor, Tender Discovery Tool) even when the package is present locally.
52
+ The two optional visual editors (Specs Editor, Tender Discovery Tool) are
53
+ not this CLI's business: each ships its own skill inside its own npm
54
+ package, and the sw-setup skill offers, installs, updates and removes
55
+ them.
54
56
 
55
57
  --json print the result object instead of the human summary
56
58
  --verbose include the per-file detail --json carries
package/lib/actions.mjs CHANGED
@@ -20,13 +20,11 @@ import {
20
20
  import { PKG_ROOT } from './content.mjs';
21
21
  import { isDeclined } from './lock.mjs';
22
22
  import { readFileSync } from 'node:fs';
23
- import { EXTRA_COMPONENTS, probe } from './extra-components.mjs';
24
23
 
25
24
  /** Preview one action against the filesystem. Reads only. */
26
25
  export function preview(action, ctx) {
27
26
  switch (action.kind) {
28
27
  case 'file-copy': return previewFile(action, ctx);
29
- case 'skill-copy': return previewSkill(action, ctx);
30
28
  case 'json-merge': return previewJson(action, ctx);
31
29
  case 'toml-fence':
32
30
  case 'text-fence': return previewFence(action, ctx);
@@ -42,8 +40,7 @@ function previewFile(action, ctx) {
42
40
  return previewBytes(action, ctx, shipped);
43
41
  }
44
42
 
45
- /** The adopt/conflict/drift/update ladder, shared by `file-copy` and, with a
46
- * probed body instead of a shipped one, `skill-copy`. */
43
+ /** The adopt/conflict/drift/update ladder every byte-for-byte target walks. */
47
44
  function previewBytes(action, ctx, bytes) {
48
45
  const hash = sha256(bytes);
49
46
  const target = abs(ctx.root, action.target);
@@ -74,35 +71,6 @@ function previewBytes(action, ctx, bytes) {
74
71
  detail: 'shipped content changed' };
75
72
  }
76
73
 
77
- // ------------------------------------------------------------ skill-copy
78
-
79
- /** Strip the probed body (a Buffer) before it lands anywhere near the
80
- * action, which `commands.mjs` serialises straight to stdout. */
81
- function stripProbeBody({ body, ...rest }) {
82
- return rest;
83
- }
84
-
85
- function previewSkill(action, ctx) {
86
- const component = EXTRA_COMPONENTS.find((c) => c.id === action.component);
87
- const probed = probe(component, { platform: ctx.platform });
88
- const probeSummary = stripProbeBody(probed);
89
-
90
- if (!probed.available) {
91
- const remedy = probed.state === 'absent'
92
- ? `npm i -D ${probed.spec}`
93
- : 'check that npx works — probing needs network-free npx --no-install';
94
- return {
95
- ...action, state: 'skipped', available: false,
96
- probe: probeSummary, reason: probed.reason, remedy,
97
- };
98
- }
99
-
100
- return {
101
- ...previewBytes(action, ctx, probed.body),
102
- available: true, probe: probeSummary, body: probed.body,
103
- };
104
- }
105
-
106
74
  // ------------------------------------------------------------- json-merge
107
75
 
108
76
  function previewJson(action, ctx) {
@@ -275,18 +243,6 @@ export function execute(action, ctx) {
275
243
  return { kind: 'file-copy', host: action.host, target: action.target, source: action.source,
276
244
  hash: action.hash, bytes: action.bytes, created_file: action.created_file !== false, dirs };
277
245
  }
278
- case 'skill-copy': {
279
- // Collapses to a `file-copy` lock entry on purpose: `lock.record`,
280
- // `lock.entries`, `revert` and `rebuild` need no extra-component-specific
281
- // case, and `uninstall` needs no probe. `source` is provenance only —
282
- // `revert` never reads it.
283
- ensureDir(dirname(target));
284
- writeAtomic(target, action.body);
285
- const dirs = ancestorDirs(ctx.root, action.target);
286
- return { kind: 'file-copy', host: action.host, target: action.target,
287
- source: `npm:${action.package}#install-skill --print`,
288
- hash: action.hash, bytes: action.bytes, created_file: action.created_file !== false, dirs };
289
- }
290
246
  case 'json-merge': {
291
247
  // Recompute from what is on disk right now rather than writing the
292
248
  // snapshot `preview` took. Several actions can target one file — the two
package/lib/commands.mjs CHANGED
@@ -8,13 +8,14 @@ import { homedir } from 'node:os';
8
8
  import { resolve } from 'node:path';
9
9
  import * as out from './out.mjs';
10
10
  import {
11
- CONTENT_VERSION, LOCK_FILE, PKG_NAME, PKG_VERSION, agents, skills,
11
+ CONTENT_VERSION, LOCK_DIR as LOCK_DIR_LEGACY, PKG_NAME, PKG_VERSION, agents,
12
+ lockDir, lockFile, lockNeedsGitignore, skills,
12
13
  } from './content.mjs';
13
14
  import { exists, isWritable, abs } from './fsx.mjs';
14
- import { AGENTS, buildPlan, detectAll, installedCounts } from './hosts.mjs';
15
+ import { AGENTS, buildPlan, detectAll, installedCounts, skillsDir } from './hosts.mjs';
15
16
  import * as lockfile from './lock.mjs';
16
17
  import { dedupe, execute, isWritableState, preview, pruneDirs, revert } from './actions.mjs';
17
- import { probeAll } from './extra-components.mjs';
18
+ import { removeDirIfEmpty, removeFile } from './fsx.mjs';
18
19
  import { askAgents } from './prompt.mjs';
19
20
  import { RESTART } from './render.mjs';
20
21
 
@@ -31,14 +32,12 @@ export function buildContext(argv, command) {
31
32
  let root = process.cwd();
32
33
  let yes = false;
33
34
  let projectWiki;
34
- let extraComponents = true;
35
35
  let asJson = false;
36
36
  let verbose = false;
37
37
 
38
38
  for (let i = 0; i < argv.length; i += 1) {
39
39
  const arg = argv[i];
40
40
  if (arg === '--yes' || arg === '-y') { yes = true; continue; }
41
- if (arg === '--no-extra-components') { extraComponents = false; continue; }
42
41
  if (arg === '--json') { asJson = true; continue; }
43
42
  if (arg === '--verbose') { verbose = true; continue; }
44
43
  if (arg === '--agent') {
@@ -73,7 +72,7 @@ export function buildContext(argv, command) {
73
72
  if (!exists(root)) throw new UsageError(`--root does not exist: ${root}`, [`${RUN} ${command} --root .`]);
74
73
 
75
74
  return {
76
- root, scope, yes, projectWiki, extraComponents, json: asJson, verbose,
75
+ root, scope, yes, projectWiki, json: asJson, verbose,
77
76
  // Empty means "nothing chosen yet" — resolveAgents() applies the
78
77
  // per-command fallback once the lock (and, for apply/install, a TTY)
79
78
  // is available to consult.
@@ -152,18 +151,11 @@ function previewAll(ctx) {
152
151
  }
153
152
 
154
153
  function summarise(actions) {
155
- const s = { create: 0, update: 0, unchanged: 0, drift: 0, conflict: 0, manual: 0, skipped: 0 };
154
+ const s = { create: 0, update: 0, unchanged: 0, drift: 0, conflict: 0, manual: 0 };
156
155
  for (const a of actions) if (a.state in s) s[a.state] += 1;
157
156
  return s;
158
157
  }
159
158
 
160
- /** The `npm i -D <spec>` line(s) for every skipped skill-copy action. */
161
- function skippedInstalls(actions) {
162
- return [...new Set(
163
- actions.filter((a) => a.state === 'skipped' && a.remedy?.startsWith('npm i -D')).map((a) => a.remedy),
164
- )];
165
- }
166
-
167
159
  /** Strip the fields that are machinery, not result. */
168
160
  function publicAction(a) {
169
161
  const { doc, fmt, body, value, conflict, newly_declined: nd, absolute, ...rest } = a;
@@ -202,6 +194,7 @@ export async function status(argv) {
202
194
  }
203
195
  return {
204
196
  id: d.id, detected: d.detected, marker: d.marker, writable: d.writable,
197
+ skills_dir: skillsDir(d.id, ctx),
205
198
  installed: installedCounts(d.id, ctx), state, notes: d.notes,
206
199
  };
207
200
  });
@@ -217,24 +210,9 @@ export async function status(argv) {
217
210
  const worst = ['conflict', 'drift', 'not-installed', 'ok', 'absent']
218
211
  .find((s) => agentStates.some((a) => a.state === s)) ?? 'absent';
219
212
 
220
- const extraComponents = ctx.extraComponents === false ? [] : probeAll({ platform: ctx.platform }).map((p) => ({
221
- id: p.id, package: p.pkg, version: p.version, label: p.label,
222
- available: p.available, probe: p.state, reason: p.reason ?? null,
223
- installed_for: Object.entries(lock?.files ?? {})
224
- .filter(([target]) => target.endsWith(`/skills/${p.id}/SKILL.md`))
225
- .map(([, v]) => v.host),
226
- install: p.available ? null : `npm i -D ${p.spec}`,
227
- }));
228
-
229
213
  let next = pending
230
214
  ? `Run \`${RUN} plan\` to see the ${pending} change(s) needed, then \`${RUN} apply --yes\`.`
231
215
  : `Nothing to do — the harness is installed and up to date. Run \`${RUN} plan\` any time to re-check.`;
232
- if (ctx.extraComponents !== false) {
233
- const unavailable = extraComponents.filter((c) => !c.available);
234
- if (unavailable.length) {
235
- next += ` ${unavailable.length} optional extra component(s) are not on this machine — install with ${unavailable.map((c) => `\`${c.install}\``).join(', ')} to enable their skills.`;
236
- }
237
- }
238
216
 
239
217
  return out.ok('status', head(ctx, {
240
218
  installed_version: lock?.version ?? null,
@@ -242,7 +220,7 @@ export async function status(argv) {
242
220
  next_step: next,
243
221
  help: [`${RUN} plan`, `${RUN} apply --yes`, `${RUN} guide`],
244
222
  inventory: { skills: skills().length, agents: agents().length },
245
- agents: agentStates, extra_components: extraComponents, drift,
223
+ agents: agentStates, drift,
246
224
  declined: lock?.declined ?? [],
247
225
  manual: lock?.manual ?? [],
248
226
  errors,
@@ -275,10 +253,6 @@ export async function plan(argv) {
275
253
  ? ` The ${summary.conflict} conflict(s) and ${summary.manual} manual step(s) will be reported again and left to you.`
276
254
  : '')
277
255
  : `Nothing to do — every target is already up to date.`;
278
- const installs = skippedInstalls(actions);
279
- if (installs.length) {
280
- next += ` ${summary.skipped} action(s) skipped — run ${installs.map((i) => `\`${i}\``).join(', ')} to enable them.`;
281
- }
282
256
 
283
257
  return out.ok('plan', head(ctx, {
284
258
  summary, next_step: next,
@@ -311,13 +285,35 @@ export async function apply(argv, label = 'apply') {
311
285
 
312
286
  // An apply we cannot record is an apply we cannot undo. Refuse up front
313
287
  // rather than leave the user with files and no way back.
314
- if (!isWritable(abs(ctx.root, LOCK_FILE))) {
315
- return out.failure(label, `cannot write the lock file at ${LOCK_FILE}`, {
288
+ if (!isWritable(abs(ctx.root, lockFile(ctx.root)))) {
289
+ return out.failure(label, `cannot write the lock file at ${lockFile(ctx.root)}`, {
316
290
  next_step: `Make ${ctx.root} writable, then re-run \`${RUN} apply --yes\`.`,
317
291
  help: [`${RUN} status`],
318
292
  });
319
293
  }
320
294
 
295
+ // Migration: a lock that used to live in the repository root carried a
296
+ // managed `.gitignore` line. Under `var/` the recipe ignores it already, so
297
+ // that line is ours to take back rather than leave behind as litter.
298
+ const staleIgnore = !lockNeedsGitignore(ctx.root)
299
+ && (lock.fences ?? []).find((f) => f.file === '.gitignore' && f.block === 'lock');
300
+ const migrated = [];
301
+ if (staleIgnore) {
302
+ try {
303
+ const result = revert({ kind: 'text-fence', target: '.gitignore', ...staleIgnore }, ctx);
304
+ if (result.state !== 'kept') {
305
+ lock = { ...lock, fences: lock.fences.filter((f) => f !== staleIgnore) };
306
+ migrated.push({
307
+ target: '.gitignore',
308
+ detail: `removed the managed \`${LOCK_DIR_LEGACY}/\` line — the lock now lives in ${lockDir(ctx.root)}/, which this project already ignores`,
309
+ });
310
+ }
311
+ } catch {
312
+ // Cosmetic. An apply that installed everything must not fail over a
313
+ // line we were trying to tidy away.
314
+ }
315
+ }
316
+
321
317
  const { actions, errors } = previewAll(ctx);
322
318
  const results = [];
323
319
  const perAgent = new Map(ctx.agents.map((a) => [a, { id: a, created: 0, updated: 0, unchanged: 0, failed: 0 }]));
@@ -371,7 +367,6 @@ export async function apply(argv, label = 'apply') {
371
367
  conflict: results.filter((r) => r.state === 'conflict').length,
372
368
  drift: results.filter((r) => r.state === 'drift').length,
373
369
  manual: manual.length,
374
- skipped: results.filter((r) => r.state === 'skipped').length,
375
370
  failed: results.filter((r) => r.state === 'failed').length,
376
371
  };
377
372
 
@@ -382,14 +377,11 @@ export async function apply(argv, label = 'apply') {
382
377
  ? `${summary.failed} change(s) failed — see \`errors\`. Fix those, then re-run \`${RUN} apply --yes\`.`
383
378
  : `Restart the coding agents listed in \`help\` so they pick up the new skills, sub-agents and MCP servers.`
384
379
  + (manual.length ? ` Then finish the ${manual.length} manual step(s) in \`manual\`.` : '');
385
- const installs = skippedInstalls(results);
386
- if (installs.length) {
387
- next += ` ${summary.skipped} action(s) skipped — run ${installs.map((i) => `\`${i}\``).join(', ')} to enable them.`;
388
- }
389
380
 
390
381
  const body = head(ctx, {
391
- summary, lock_file: LOCK_FILE, next_step: next, help,
382
+ summary, lock_file: lockFile(ctx.root), next_step: next, help,
392
383
  agents: [...perAgent.values()], actions: results, manual, errors,
384
+ ...(migrated.length ? { migrated } : {}),
393
385
  });
394
386
  return summary.failed ? out.failure(label, `${summary.failed} change(s) failed`, body) : out.ok(label, body);
395
387
  }
@@ -423,7 +415,7 @@ export async function uninstall(argv) {
423
415
  // someone else's files, so it does nothing and says so.
424
416
  return out.ok('uninstall', head(ctx, {
425
417
  summary: { removed: 0, reverted: 0, kept: 0, missing: 0, failed: 0 },
426
- next_step: `Nothing to remove — no ${LOCK_FILE} was found, so this repository has no recorded install.`,
418
+ next_step: `Nothing to remove — no ${lockFile(ctx.root)} was found, so this repository has no recorded install.`,
427
419
  help: [`${RUN} status`],
428
420
  kept: [], missing: [], errors: [],
429
421
  }));
@@ -458,9 +450,15 @@ export async function uninstall(argv) {
458
450
 
459
451
  if (failed === 0 && survivors.length === 0 && selected.size === AGENTS.length) {
460
452
  // Everything we recorded is gone; the lock has nothing left to describe.
461
- const { removeFile } = await import('./fsx.mjs');
462
453
  removeFile(lockfile.lockPath(ctx.root));
463
- pruneDirs(['.sw-ai-sdk'], ctx);
454
+ removeFile(lockfile.legacyLockPath(ctx.root));
455
+ // `pruneDirs` deliberately never touches a top-level directory — that
456
+ // rule protects a host's own `.claude`/`.codex`, but `.sw-ai-sdk` and
457
+ // `var/sw-ai-sdk` are ours, and an empty one left behind is the clutter
458
+ // this move set out to remove.
459
+ pruneDirs([lockDir(ctx.root)], ctx);
460
+ removeDirIfEmpty(abs(ctx.root, LOCK_DIR_LEGACY));
461
+ removeDirIfEmpty(abs(ctx.root, lockDir(ctx.root)));
464
462
  } else {
465
463
  // Keep a lock describing exactly what survived, so a re-run resumes.
466
464
  lockfile.write(ctx.root, rebuild(lock, survivors, selected));
package/lib/content.mjs CHANGED
@@ -9,7 +9,7 @@
9
9
  import { readFileSync, readdirSync, statSync } from 'node:fs';
10
10
  import { dirname, join, resolve } from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
- import { walk } from './fsx.mjs';
12
+ import { isDir, walk } from './fsx.mjs';
13
13
 
14
14
  export const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
15
15
 
@@ -25,8 +25,38 @@ export const PKG_VERSION = pkg.version;
25
25
  export const CONTENT_VERSION = 1;
26
26
  export const SUPPORTED_CONTENT = [1];
27
27
 
28
+ /**
29
+ * Where the lock lives, relative to the install root.
30
+ *
31
+ * A Shopware project ignores the whole of `var/` through the `shopware/core`
32
+ * Flex recipe (`/var/*` with a single `!/var/.htaccess` negation), and `var/`
33
+ * is already where that project keeps machine-local state — `var/cache`,
34
+ * `var/log`. Writing the lock there keeps the repository root clean and means
35
+ * the installer needs no `.gitignore` line at all.
36
+ *
37
+ * A root with no `var/` — a user-scope install under $HOME, or a checkout that
38
+ * is not a Shopware project — keeps the original dot-directory, which is
39
+ * gitignored through the managed block as before. Nothing here looks at the
40
+ * platform or at any path outside the install root.
41
+ */
28
42
  export const LOCK_DIR = '.sw-ai-sdk';
29
43
  export const LOCK_FILE = `${LOCK_DIR}/harness.lock.json`;
44
+ export const VAR_LOCK_DIR = 'var/sw-ai-sdk';
45
+ export const VAR_LOCK_FILE = `${VAR_LOCK_DIR}/harness.lock.json`;
46
+
47
+ /** `var/sw-ai-sdk` when the root has a `var/`, else `.sw-ai-sdk`. */
48
+ export function lockDir(root) {
49
+ return isDir(join(root, 'var')) ? VAR_LOCK_DIR : LOCK_DIR;
50
+ }
51
+
52
+ export function lockFile(root) {
53
+ return `${lockDir(root)}/harness.lock.json`;
54
+ }
55
+
56
+ /** Whether this root still needs the managed `.gitignore` line for the lock. */
57
+ export function lockNeedsGitignore(root) {
58
+ return lockDir(root) === LOCK_DIR;
59
+ }
30
60
 
31
61
  function dirsIn(abs) {
32
62
  return readdirSync(abs)
package/lib/guide.mjs CHANGED
@@ -6,10 +6,8 @@
6
6
 
7
7
  import * as out from './out.mjs';
8
8
  import { PKG_NAME, PKG_VERSION } from './content.mjs';
9
- import { EXTRA_COMPONENTS } from './extra-components.mjs';
10
9
 
11
10
  const RUN = `npx -y ${PKG_NAME}@${PKG_VERSION}`;
12
- const EXTRA_COMPONENT_INSTALLS = EXTRA_COMPONENTS.map((c) => `npm i -D ${c.pkg}@${c.version}`).join('\n');
13
11
 
14
12
  export const GUIDE = `# ${PKG_NAME} — install protocol
15
13
 
@@ -55,26 +53,21 @@ registers the MCP servers and permission rules they depend on.
55
53
  terminal
56
54
  --scope project|user default: project
57
55
  --root <path> default: the current directory
58
- --no-extra-components skip the optional extra-component skills
59
56
  --json print the result object instead of
60
57
  the human summary — a skill or
61
58
  agent always passes this
62
59
  --verbose include the per-file detail
63
60
 
64
- ## Optional extra components
61
+ ## The two visual editors
65
62
 
66
- Two skills — Specs Editor and Tender Discovery Tool — ship inside their own
67
- npm packages, not this one. \`plan\`/\`apply\` detect them offline
68
- (\`npx --no-install\`) and never fetch them, probing for the exact version
69
- this release pins — any other version, including \`@latest\`, is treated as
70
- absent and the skill is skipped:
71
-
72
- \`\`\`bash
73
- ${EXTRA_COMPONENT_INSTALLS}
74
- \`\`\`
75
-
76
- The exact command is also in a \`skipped\` action's \`remedy\`, and in
77
- \`status\`'s \`extra_components\` array. Install it, then re-run \`${RUN} apply --yes\`.
63
+ The Specs Editor and the Tender Discovery Tool are optional, ship their own
64
+ skill inside their own npm package, and are **not installed by this CLI**.
65
+ The \`sw-setup\` skill owns their whole lifecycle — it offers them, installs
66
+ each one with that package's own \`install-skill --target <skills dir>\`,
67
+ updates them by re-running it, and removes them with \`uninstall-skill\`.
68
+ This CLI never probes for them, never reports them, and never reaches the
69
+ network for them; \`status --json\` reports each agent's \`skills_dir\`,
70
+ which is the target \`sw-setup\` hands them.
78
71
 
79
72
  ## What you can rely on
80
73