@tachikomagundam/abathur 0.2.2 → 0.2.3

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/README.md CHANGED
@@ -142,7 +142,11 @@ Abathur ships an official opencode plugin adapter. The harness remains the
142
142
  orchestrator, but once set up an opencode session can also drive the CLI
143
143
  directly. Two install routes:
144
144
 
145
- **Route A one command (tool + `/abathur` slash command).**
145
+ Both routes deliver the tool **and** the `/abathur` slash command; pick by
146
+ taste — A needs no edit of your opencode config and works fully offline, B
147
+ needs no file copying and removes with one deleted line.
148
+
149
+ **Route A — one command (file-copy installer).**
146
150
 
147
151
  ```bash
148
152
  npm i -g @tachikomagundam/abathur
@@ -150,7 +154,7 @@ abathur opencode install # copies plugin assets into ~/.config/opencode/
150
154
  # restart opencode
151
155
  ```
152
156
 
153
- **Route B — config only (tool, no slash command).** Put the package name in
157
+ **Route B — config only (auto-downloaded from npm).** Put the package name in
154
158
  your opencode config and restart:
155
159
 
156
160
  ```jsonc
@@ -159,11 +163,16 @@ your opencode config and restart:
159
163
 
160
164
  opencode then downloads the package from npm at startup into its own cache
161
165
  (`~/.cache/opencode/packages/…`) and loads the package's `./server` export —
162
- no file copying, no uninstall step (delete the line). Two honest limits: the
163
- `abathur` **CLI** still must exist on `PATH` (or `ABATHUR_BIN`) because the
164
- tool spawns it, so keep the `npm i -g` install; and slash commands are plain
165
- markdown files that plugins cannot register upstream, so `/abathur` is Route A
166
- only.
166
+ no file copying, no uninstall step (delete the line). Since 0.2.3 the plugin
167
+ also self-registers `/abathur` through its `config` hook: slash commands are
168
+ entries in the merged config's `command` map, and a plugin may add one there
169
+ the upstream `opencode-acp` plugin registers `/acp` the same way (the
170
+ 0.2.0–0.2.2 note here claiming plugins cannot register slash commands was
171
+ wrong; corrected). If Route A's copied `commands/abathur.md` exists it stays
172
+ authoritative — the injection never overwrites it, and the name-keyed command
173
+ map means `/abathur` can never appear twice. One honest limit stands for both
174
+ routes: the tool spawns the `abathur` **CLI**, which must exist on `PATH` (or
175
+ via `ABATHUR_BIN`) — keep the `npm i -g` install.
167
176
 
168
177
  Either route gives the session:
169
178
 
@@ -176,9 +185,10 @@ Either route gives the session:
176
185
  so the allowlist limits typos and UX, not capability. `promote` and
177
186
  `tombstone` are deliberately NOT reachable through the tool: they are
178
187
  human gates and must be run in a terminal.
179
- - the user command **`/abathur <args…>`** (Route A only) — a slash command
180
- that tells the agent to translate its arguments into a tool call and report
181
- the exit code.
188
+ - the user command **`/abathur <args…>`** — a slash command that tells the
189
+ agent to translate its arguments into a tool call and report the exit code.
190
+ Route A provides it as the copied `commands/abathur.md` file; Route B gets
191
+ it from the plugin's `config` hook at startup.
182
192
 
183
193
  Route A ships its own manager commands: `abathur opencode status` shows each
184
194
  target's path, installed/packaged sha256, and state (up-to-date / outdated /
@@ -430,7 +440,10 @@ abathur run --genome toy-smoke --dry-run # 计划 + requires[] 探针,零
430
440
  Abathur 自带官方 opencode 插件适配器。工装仍是编排者,但配置之后,opencode
431
441
  会话也可以直接驱动 CLI。安装有两条路线:
432
442
 
433
- **路线 A——一条命令(工具 + `/abathur` 斜杠命令)。**
443
+ 两条路线都交付工具**和** `/abathur` 斜杠命令,按喜好挑选:A 不必改动你的
444
+ opencode 配置、且完全离线可用;B 不复制任何文件、删掉一行即卸载。
445
+
446
+ **路线 A——一条命令(文件复制安装器)。**
434
447
 
435
448
  ```bash
436
449
  npm i -g @tachikomagundam/abathur
@@ -438,7 +451,7 @@ abathur opencode install # 把插件资产复制进 ~/.config/opencode/
438
451
  # 重启 opencode
439
452
  ```
440
453
 
441
- **路线 B——纯配置(只有工具,没有斜杠命令)。** 把包名写进 opencode 配置再
454
+ **路线 B——纯配置(自动从 npm 下载)。** 把包名写进 opencode 配置再
442
455
  重启:
443
456
 
444
457
  ```jsonc
@@ -447,10 +460,14 @@ abathur opencode install # 把插件资产复制进 ~/.config/opencode/
447
460
 
448
461
  opencode 会在启动时自行从 npm 把包下载到它自己的缓存
449
462
  (`~/.cache/opencode/packages/…`)并加载该包的 `./server` 导出——不复制文件,
450
- 也没有 uninstall 步骤(删掉那行即可)。两点诚实的限制:工具 spawn 的是
451
- `abathur` **CLI**,所以 CLI 仍必须在 `PATH`(或 `ABATHUR_BIN`)上可达,
452
- `npm i -g` 不能省;而斜杠命令在上游只是 markdown 文件、插件无法注册,
453
- 所以 `/abathur` 只有路线 A 提供。
463
+ 也没有 uninstall 步骤(删掉那行即可)。自 0.2.3 起,插件还会通过它的
464
+ `config` 钩子自注册 `/abathur`:斜杠命令本质是合并后配置的 `command`
465
+ 映射里的条目,插件可以在钩子里添加一条——上游的 `opencode-acp` 插件正是
466
+ 这样注册 `/acp` 的(0.2.0–0.2.2 在此声称"插件无法注册斜杠命令"是错的,
467
+ 已更正)。若路线 A 复制的 `commands/abathur.md` 存在,它保持权威——注入
468
+ 绝不覆盖它,而且命令映射按名字键控,`/abathur` 绝不会出现两份。对两条
469
+ 路线同样成立的一个诚实限制:工具 spawn 的是 `abathur` **CLI**,CLI 必须
470
+ 在 `PATH`(或 `ABATHUR_BIN`)上可达,`npm i -g` 不能省。
454
471
 
455
472
  任一路线都会让会话获得:
456
473
 
@@ -461,8 +478,9 @@ opencode 会在启动时自行从 npm 把包下载到它自己的缓存
461
478
  bash 等同的权限——`run` 与 `genome` 按设计就会 spawn 变异器/引擎二进制——
462
479
  所以允许清单限制的是笔误与体验,而非能力。`promote` 与 `tombstone` 刻意
463
480
  不可经由工具触达:它们是人类闸门,必须在终端里运行。
464
- - 用户命令 **`/abathur <参数…>`**(仅路线 A)——斜杠命令,指示智能体把参数
465
- 翻译成工具调用并回报退出码。
481
+ - 用户命令 **`/abathur <参数…>`**——斜杠命令,指示智能体把参数
482
+ 翻译成工具调用并回报退出码。路线 A 由复制的 `commands/abathur.md`
483
+ 文件提供;路线 B 由插件的 `config` 钩子在启动时注册。
466
484
 
467
485
  路线 A 自带管理命令:`abathur opencode status` 逐目标打印路径、已安装/随包
468
486
  sha256 与状态
@@ -62,9 +62,13 @@ fi
62
62
  echo "ran $unit"
63
63
  echo '{"tokensEst":123,"turns":4}'
64
64
  `;
65
+ // Unique sleep duration: node --test runs files concurrently and this test's
66
+ // orphan check scans the whole machine's `ps` output — "sleep 31.7" is the
67
+ // shipped toy genome's hang marker (bench-toy.test.ts); reusing it here made
68
+ // the two files' checks see each other's live sleeps (CI-verified 5/5 collision).
65
69
  const BIN_HANG = `#!/usr/bin/env bash
66
70
  if [ "\${1:-}" = "--version" ]; then echo "1.2.3"; exit 0; fi
67
- exec sleep 31.7
71
+ exec sleep 29.31
68
72
  `;
69
73
  const BIN_NO_VERSION = `#!/usr/bin/env bash
70
74
  if [ "\${1:-}" = "--version" ]; then echo "unsupported flag" >&2; exit 1; fi
@@ -296,7 +300,7 @@ test("(E) hanging unit: group kill at timeoutS, zero orphans", async (t) => {
296
300
  assert.equal(run.exitCode, null);
297
301
  assert.match(run.note ?? "", /killed after 1s: process group SIGKILL/);
298
302
  const ps = spawnSync("ps", ["-eo", "args"], { encoding: "utf8" });
299
- assert.ok(!ps.stdout.includes("sleep 31.7"), "orphaned sleep survived the group kill");
303
+ assert.ok(!ps.stdout.includes("sleep 29.31"), "orphaned sleep survived the group kill");
300
304
  });
301
305
  // ------------------------------------------------------------------ AC (F)
302
306
  test("(F) infra_failed is recorded with a distinct status/note, never a silent 0", async (t) => {
@@ -241,6 +241,27 @@ test("plugin/abathur.ts: V1 shape — marker, default {id, server}, tool allowli
241
241
  assert.ok(!/shell\s*:\s*true/.test(bytes), "shell:true is banned");
242
242
  assert.ok(bytes.includes("ABATHUR_BIN"), "bin overridable via ABATHUR_BIN");
243
243
  });
244
+ test("plugin/abathur.ts: config hook self-registers /abathur — template byte-mirrors the command md body", async () => {
245
+ const bytes = await readFile(pluginAssetPath, "utf8");
246
+ // Route B needs the config hook: opencode hands plugins the fully-merged config
247
+ // (file commands already inside cfg.command), so a `??=` injection registers
248
+ // /abathur ONLY when no commands/abathur.md exists — Route A bytes/behaviour
249
+ // stay untouched and the name-keyed command map guarantees no duplicate entry.
250
+ assert.ok(/config:\s*async\s*\(cfg:\s*Config\)/.test(bytes), "config hook with typed cfg required");
251
+ assert.ok(bytes.includes('import { tool, type Config }'), "Config type imported from @opencode-ai/plugin");
252
+ assert.ok(/cfg\.command\s*\?\?=\s*\{\}/.test(bytes), "cfg.command must be lazily created with ??=");
253
+ assert.ok(/cfg\.command\.abathur\s*\?\?=/.test(bytes), "entry must be ??= — a file-installed command is never overwritten");
254
+ assert.ok(bytes.includes('description: "Drive the abathur evolution harness (usage: /abathur <command> [args...])"'), "injected description must be pinned verbatim");
255
+ // Extract the template literal (inner backticks appear escaped as \`) and byte-compare.
256
+ const literal = bytes.match(/const COMMAND_TEMPLATE = `((?:[^`\\]|\\.)*)`;/);
257
+ assert.ok(literal !== null, "COMMAND_TEMPLATE literal must exist");
258
+ const template = (literal[1] ?? "").replaceAll("\\`", "`");
259
+ const md = await readFile(commandAssetPath, "utf8");
260
+ assert.ok(md.startsWith(COMMAND_MARKER), "md must still carry its first-line marker");
261
+ const body = md.slice(md.indexOf("\n") + 1); // everything after the marker line
262
+ assert.ok(body.startsWith("Drive the abathur evolution harness"), "body start sanity");
263
+ assert.equal(template, body, "injected template must byte-equal the command md minus its marker line");
264
+ });
244
265
  test("plugin/abathur-command.md: first-line marker, $ARGUMENTS, points at the abathur tool", async () => {
245
266
  const text = await readFile(commandAssetPath, "utf8");
246
267
  assert.equal(text.split("\n")[0], COMMAND_MARKER, "first line must be the command marker");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tachikomagundam/abathur",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Evolution harness: observe failures, mutate, re-bench, select — human-gated promotion, offline lineage bundles.",
5
5
  "author": "TachikomaGundam",
6
6
  "type": "module",
package/plugin/abathur.ts CHANGED
@@ -1,4 +1,4 @@
1
- // abathur-opencode-plugin v0.2.2
1
+ // abathur-opencode-plugin v0.2.3
2
2
  // Official opencode plugin adapter for the abathur evolution harness.
3
3
  // Registers ONE agent tool, `abathur`, that shells out to the abathur CLI —
4
4
  // argv-only (node:child_process execFile, never a shell), top-level commands
@@ -9,17 +9,19 @@
9
9
  // server(input, options) resolves to Hooks; Hooks.tool is a
10
10
  // { [name]: ToolDefinition } record (see @opencode-ai/plugin).
11
11
  // This file is shipped as-is (package.json "files") and reaches a session by
12
- // two routes, neither compiling it through abathur's tsc:
13
- // (A) copied into ~/.config/opencode/plugins/ by `abathur opencode install`
14
- // (adds the /abathur command too); "@opencode-ai/plugin" then resolves in
15
- // opencode's config-directory node_modules.
12
+ // two routes, neither compiling it through abathur's tsc. Both deliver the
13
+ // tool AND the /abathur slash command:
14
+ // (A) copied into ~/.config/opencode/plugins/ by `abathur opencode install`,
15
+ // which also drops commands/abathur.md; "@opencode-ai/plugin" then
16
+ // resolves in opencode's config-directory node_modules.
16
17
  // (B) served as the package's "./server" export when the npm package name is
17
18
  // listed in opencode.jsonc "plugin" — opencode's arborist install places
18
19
  // @opencode-ai/plugin (runtime dependency) next to the package in its
19
- // cache, so the same import resolves there too.
20
+ // cache, so the same import resolves there too; with no commands/abathur.md
21
+ // on disk, the config hook below self-registers the command instead.
20
22
 
21
23
  import { execFile } from "node:child_process";
22
- import { tool } from "@opencode-ai/plugin";
24
+ import { tool, type Config } from "@opencode-ai/plugin";
23
25
 
24
26
  /** Spawn cap: a CLI call that outlives this is killed and reported, never awaited forever. */
25
27
  const TIMEOUT_MS = 120_000;
@@ -114,9 +116,47 @@ function runCli(bin: string, argv: readonly string[]): Promise<CliResult> {
114
116
  });
115
117
  }
116
118
 
119
+
120
+ /**
121
+ * /abathur slash-command template, injected into cfg.command.abathur by the
122
+ * config hook for installs without a commands/abathur.md on disk (Route B).
123
+ * Byte-mirror of plugin/abathur-command.md minus its first-line marker —
124
+ * src/test/opencode.test.ts pins the equality.
125
+ */
126
+ const COMMAND_TEMPLATE = `Drive the abathur evolution harness through the \`abathur\` tool on this machine.
127
+
128
+ User request: $ARGUMENTS
129
+
130
+ Interpret the request as one \`abathur\` CLI invocation: the first word is the
131
+ top-level command (\`genome\`, \`run\`, \`status\`, \`bundle\`, \`graft\`, \`self-eval\`,
132
+ \`kernel\`, or \`--help\`) and the rest are argv tokens. Call the \`abathur\` tool
133
+ with \`command\` set to the first word and \`extra\` set to the remaining tokens,
134
+ then report the CLI exit code (0 ok / 1 blocked decision / 2 cannot-answer)
135
+ and the relevant lines of its output. If no request was given, call the tool
136
+ with \`command: "--help"\` and summarize the command list. \`promote\` and
137
+ \`tombstone\` cannot be called through the tool at all — the tool refuses them.
138
+ They are human gates that belong to a terminal: if the user asks for one,
139
+ tell them to run \`abathur promote …\` / \`abathur tombstone …\` there.
140
+ `;
141
+
117
142
  export default {
118
143
  id: "abathur",
119
144
  server: async () => ({
145
+ // Self-registering /abathur: opencode calls hook.config(cfg) once per
146
+ // instance after ALL config sources are merged — file-based commands are
147
+ // already in cfg.command by then (config.ts merges {command,commands}/**/*.md;
148
+ // the hook fires from plugin/index.ts after config.get()). `??=` keeps a
149
+ // Route-A commands/abathur.md authoritative (identical behaviour to 0.2.2)
150
+ // and only fills the gap for Route B; the command map is keyed by name, so
151
+ // a same-name file + injection never duplicates the entry. Proven upstream
152
+ // mechanism: opencode-acp registers its /acp command exactly this way.
153
+ config: async (cfg: Config) => {
154
+ cfg.command ??= {};
155
+ cfg.command.abathur ??= {
156
+ description: "Drive the abathur evolution harness (usage: /abathur <command> [args...])",
157
+ template: COMMAND_TEMPLATE,
158
+ };
159
+ },
120
160
  tool: {
121
161
  abathur: tool({
122
162
  description: