@ssheleg/agent-stack 0.16.0 → 0.16.1

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
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.16.1 — the installers refuse the shadow, and the pack stops mis-selling itself
4
+
5
+ - **Both installers refuse to write plain copies over an installed plugin.** The family
6
+ audit of 2026-08-29 reproduced the shadow live: a bare `npx @ssheleg/telegram-dev`
7
+ created three plain copies in `~/.claude/skills/` while that plugin was enabled — and
8
+ this member had **no plugin check at all**, in either installer, so a bare
9
+ `npx @ssheleg/agent-stack` on a machine with the plugin would have shipped **four**
10
+ shadows, one per skill, each serving its frozen version forever. `bin/agent-stack.js`
11
+ and `install.sh` now implement make-skill v0.25.0's canon (`distribution.md`, "The
12
+ installer must refuse the shadow it documents"): detect from the TARGET home's
13
+ `installed_plugins.json` (keys are `<name>@<marketplace>`, and the two names differ
14
+ often), keep the `marketplaces/` dir read only as the fallback signal, refuse with
15
+ exit **3** and a remedy that names the spec read from the JSON
16
+ (`claude plugin marketplace update agent-stack` + `claude plugin update
17
+ agent-stack@<marketplace>`, plus the family launcher), offer `--force` as the recorded
18
+ deliberate override, fail open on a missing or corrupt JSON, and gate only the
19
+ `~/.claude` write — no other agent has plugins.
20
+ - **`test/installer_test.js` joins `npm test` and CI** — 11 cases against throwaway
21
+ HOMEs: fresh / rerun-skip / `--force` / unknown-arg, plugin-present refusal (exit
22
+ code, remedy text, nothing written — all three asserted), a differently-named
23
+ marketplace in the remedy spec, corrupt JSON failing open, no false refusal on other
24
+ plugins or an `agent-stack-extra` prefix-collider, the marketplaces-dir fallback, and
25
+ the same matrix for `install.sh`. Watched failing before trusted: **7 of 11 red**
26
+ against the pre-fix installers (`git stash` the two, run, pop). The suite follows the
27
+ house residue rule — a failing case keeps its HOME, and the run ends by saying what it
28
+ left. It replaces the inline fresh-HOME-only step in `validate.yml`, which is the CI
29
+ shape that let the plugin-present case go unrun everywhere.
30
+ - **A successful install now says how the next version arrives** — the last line names
31
+ `npx @ssheleg/agent-stack@latest --force` and the family launcher, in both installers.
32
+ - **AST-01: the plugin stops selling itself as two skills.** `plugin.json` and
33
+ `marketplace.json` both opened with "Two skills: agent-orchestrator … and agent-evals"
34
+ while the pack ships **four** — `agent-interop` and `agent-harness` were invisible in
35
+ `claude plugin details` and on the marketplace. Both descriptions now name all four
36
+ skills with what each covers.
37
+
3
38
  ## v0.16.0 — the whole survey, not just its taxonomy
4
39
 
5
40
  v0.15.0 took the taxonomy and the named failure modes. This takes the rest: the write
@@ -16,6 +16,44 @@ const os = require('os');
16
16
 
17
17
  const ROOT = path.resolve(__dirname, '..');
18
18
  const REPO = 'ssheleg/agent-stack';
19
+ const PLUGIN = 'agent-stack';
20
+
21
+ // Exit codes are the contract: 0 installed or skipped, 1 corrupted package,
22
+ // 2 usage error, 3 refused — the plugin channel owns this agent (--force overrides).
23
+ const EXIT_PLUGIN_PRESENT = 3;
24
+
25
+ /**
26
+ * The plugin spec (`<name>@<marketplace>`) installed for `name` in this home,
27
+ * or null.
28
+ *
29
+ * `installed_plugins.json` is the record of what is actually installed. The
30
+ * `plugins/marketplaces/<name>` directory under-reports: a marketplace added
31
+ * from a local `directory` source has no dir there at all, and plugin names
32
+ * differ from marketplace names, so a check keyed on it stays green while the
33
+ * shadow lands. Absence and corruption both read as "no plugin": the fresh
34
+ * HOME is the common case, and an installer that crashes on a parse error
35
+ * refuses the machines that need it most.
36
+ */
37
+ function installedPluginSpec(home, name) {
38
+ try {
39
+ const raw = fs.readFileSync(
40
+ path.join(home, '.claude', 'plugins', 'installed_plugins.json'), 'utf8');
41
+ const parsed = JSON.parse(raw);
42
+ const plugins =
43
+ parsed && typeof parsed === 'object' &&
44
+ parsed.plugins && typeof parsed.plugins === 'object'
45
+ ? parsed.plugins
46
+ : parsed;
47
+ if (!plugins || typeof plugins !== 'object') return null;
48
+ for (const spec of Object.keys(plugins)) {
49
+ if (spec === name) return `${name}@${name}`;
50
+ if (spec.startsWith(name + '@')) return spec;
51
+ }
52
+ } catch {
53
+ // missing or corrupt = no plugin — fail open on absence, never crash
54
+ }
55
+ return null;
56
+ }
19
57
 
20
58
  function usage() {
21
59
  console.log(`agent-stack installer
@@ -25,6 +63,12 @@ Usage:
25
63
  into ~/.claude (skip existing unless --force)
26
64
  npx @ssheleg/agent-stack --help
27
65
 
66
+ Exit codes:
67
+ 0 installed or skipped 2 usage error
68
+ 1 corrupted package 3 refused: the agent-stack PLUGIN is installed in
69
+ this home — plain copies would shadow it (pass
70
+ --force to write them anyway)
71
+
28
72
  Other install paths:
29
73
  Claude Code plugin: /plugin marketplace add ${REPO}
30
74
  /plugin install agent-stack@agent-stack
@@ -86,6 +130,41 @@ function main(argv) {
86
130
  }
87
131
 
88
132
  const home = os.homedir();
133
+
134
+ // One channel per agent. A plain ~/.claude/skills/<name> beside the installed
135
+ // agent-stack plugin is two listings of the same skill per skill this pack
136
+ // ships, and the stale copy wins — the exact shadow make-skill's distribution
137
+ // canon forbids (§ "The installer must refuse the shadow it documents").
138
+ // Refuse rather than create it, and refuse LOUDLY: a presence check keyed on
139
+ // the marketplaces/ dir alone that exits 0 is the fail-open class — a
140
+ // directory-sourced marketplace has no dir there, plugin names differ from
141
+ // marketplace names, and exit 0 reads as success to every script above it.
142
+ // Reproduced live 2026-08-29: a bare `npx @ssheleg/telegram-dev` shipped
143
+ // three shadows past exactly this hole while the plugin was enabled. Only the
144
+ // ~/.claude write is gated — no other agent has plugins.
145
+ const spec = installedPluginSpec(home, PLUGIN);
146
+ const marketplace = path.join(home, '.claude', 'plugins', 'marketplaces', PLUGIN);
147
+ const viaMarketplaceDir = !spec && fs.existsSync(marketplace);
148
+ if ((spec || viaMarketplaceDir) && !force) {
149
+ const found = spec
150
+ ? `installed as the Claude Code plugin ${spec}\n` +
151
+ ' (declared in ~/.claude/plugins/installed_plugins.json)'
152
+ : `registered as a Claude Code marketplace\n (${marketplace})`;
153
+ console.error(
154
+ `refused: agent-stack is already ${found}.\n` +
155
+ ` Plain copies in ~/.claude/skills/ (${names.join(', ')})\n` +
156
+ ' would shadow the plugin and serve this frozen version forever.\n' +
157
+ ' Update the plugin channel instead:\n' +
158
+ ' claude plugin marketplace update agent-stack\n' +
159
+ ` claude plugin update ${spec || 'agent-stack@agent-stack'}\n` +
160
+ ' Family launcher (updates every member, prunes shadow copies):\n' +
161
+ ' npx --yes sshlg-skills@latest update\n' +
162
+ ' Pass --force to write the plain copies anyway — a deliberate choice\n' +
163
+ ' to run two channels, where the stale one wins.'
164
+ );
165
+ return EXIT_PLUGIN_PRESENT;
166
+ }
167
+
89
168
  for (const name of names) {
90
169
  installOne(
91
170
  `${name} skill`,
@@ -95,6 +174,15 @@ function main(argv) {
95
174
  force
96
175
  );
97
176
  }
177
+ // The last line says how the next version arrives — "Installed" is not a
178
+ // complete sentence. Auto-update is off on purpose: this member composes
179
+ // with its family, and per-marketplace autoUpdate moves each member on its
180
+ // own clock, into combinations nobody tested together.
181
+ console.log(
182
+ '\nUpdates: rerun `npx @ssheleg/agent-stack@latest --force`, or refresh the\n' +
183
+ 'whole family with `npx --yes sshlg-skills@latest update` (every channel,\n' +
184
+ 'and it prunes plain copies that would shadow a plugin).'
185
+ );
98
186
  return 0;
99
187
  }
100
188
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@ssheleg/agent-stack",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "scripts": {
5
- "test": "python3 test/validate.py && python3 test/plant_guard_test.py"
5
+ "test": "python3 test/validate.py && python3 test/plant_guard_test.py && node test/installer_test.js"
6
6
  },
7
7
  "description": "Production patterns for AI agent orchestrators — tool-calling loops, multi-stage pipelines with checkpoints, LLM provider routing with fallback, four-layer memory with confidence decay — plus the wallet side of reselling LLM access. This package is the installer CLI.",
8
8
  "bin": {
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "agent-stack",
3
3
  "displayName": "Agent Stack",
4
- "description": "Two skills: agent-orchestrator — tool-calling loops, multi-stage pipelines with checkpoints, provider routing with fallback, four-layer memory, context engineering, plus the wallet side of reselling LLM access; and agent-evals — run/trace/thread evals, judges, and fixtures grown from production.",
5
- "version": "0.16.0",
4
+ "description": "Four skills: agent-orchestrator — tool-calling loops, pipelines with checkpoints, provider routing with fallback, memory architecture, plus the wallet side of reselling LLM access; agent-evals — run/trace/thread evals, LLM judges, and fixtures grown from production; agent-interop — MCP servers and clients, A2A agent cards, the MCP Registry, and gateways; agent-harness — system prompts, tool shaping, workflow-vs-agent, and auditing an agent system.",
5
+ "version": "0.16.1",
6
6
  "author": {
7
7
  "name": "ssheleg",
8
8
  "url": "https://x.com/sshlg93"