mirror of
https://github.com/Heretek-AI/opencode-rea.git
synced 2026-11-07 10:56:10 -10:00
feat(m2): rea_bootstrap_stack tool and rea-bootstrap command
This commit is contained in:
1 parent
b7b0aaa676
commit
2fb3aeb085
13 files changed
+635
-16
No files matched your search
@@ -15,7 +15,7 @@ into OpenCode — on top of an already-registered REA MCP server.
|
||||
`2.0.26`, `rea-agents` `6.5.0`)
|
||||
- Plugin id: `rea` (`src/index.ts`: `Plugin.define({ id: "rea", setup ... })`;
|
||||
setup parses options, then registers skills, agents, readiness + tool catalog
|
||||
policy, and commands)
|
||||
policy, the bootstrap tool, and commands)
|
||||
|
||||
Machine-checkable docs guard: `test/unit/docs.test.ts`
|
||||
(build/test rules: `AGENTS.md`).
|
||||
@@ -143,10 +143,11 @@ Source of truth: `src/commands.ts` (registered via `ctx.command.transform`).
|
||||
| `/rea` | Switch to the `rea` investigator agent and run a staged investigation (`rea-investigation` skill: scope → gather with evidence → infer only from cited evidence). |
|
||||
| `/rea-doctor` | Run the REA readiness check by calling the `rea_readiness` tool; report each item and act on the returned remediation. |
|
||||
| `/rea-report` | Produce the REA evidence report (observations vs inferences vs unknowns, every claim with its Evidence ID), then delegate a check to the `rea-auditor` agent. |
|
||||
| `/rea-bootstrap` | Install the REA reverse-engineering stack by calling the `rea_bootstrap_stack` tool, then call `rea_readiness` to verify; report what installed. |
|
||||
|
||||
REA's six MCP prompts surface as host commands under the `rea:<prompt>` namespace
|
||||
and are **not** duplicated by this plugin (`COMMAND_IDS` contain no `":"`, so the
|
||||
three slash commands never shadow them):
|
||||
four slash commands never shadow them):
|
||||
|
||||
- `rea:investigate_feature`
|
||||
- `rea:compare_application_versions`
|
||||
@@ -196,6 +197,41 @@ During the same `ctx.tool.transform` pass, `applyReaToolCatalogPolicy`:
|
||||
|
||||
Use it via `/rea-doctor`, or call `rea_readiness` directly.
|
||||
|
||||
## Bootstrap tool: `rea_bootstrap_stack`
|
||||
|
||||
Source of truth: `src/bootstrap-tool.ts` (registered via `ctx.tool.transform` as a
|
||||
native tool with `codemode: false`, reusing the `src/bootstrap.ts` runner; read-only
|
||||
MCP-wise — no writes, no config mutation, never `ctx.mcp.transform`). Use it via
|
||||
`/rea-bootstrap`, or call `rea_bootstrap_stack` directly.
|
||||
|
||||
`rea_bootstrap_stack` contract:
|
||||
|
||||
- Input: `{ installSkills?, installMemory?, installWorkflows? }` (all booleans,
|
||||
all default `true`; `additionalProperties: false`).
|
||||
- `installSkills`: install the four MIT/Apache-2.0 RE skill packs via
|
||||
`npx skills add` (`p4nda0s/reverse-skills`, `ljagiello/ctf-skills`,
|
||||
`cyberkaida/reverse-engineering-assistant`, `rehan-remade/universal-modder`).
|
||||
- `installMemory`: `npm install --global @aerovato/operator-helper` then
|
||||
`operator-helper install opencode-v2`.
|
||||
- `installWorkflows`: `opencode plugin add @rphang/opencode-workflows`
|
||||
(guarded by a version-probe `skip`).
|
||||
- Permission: `execute` calls `ctx.permission.assert({ ... action:
|
||||
"rea_bootstrap_stack", resources: [directory], save: ["*"] })` **before**
|
||||
running anything, so installs ask the user first. Hosts that predate
|
||||
`permission.assert` (observed: `@opencode/cli` 2.0.26) make the tool refuse
|
||||
with a descriptive error instead of running unapproved installs; hosts with
|
||||
the capability (observed: 2.0.32) gate normally.
|
||||
- Progress: each step is reported through the tool context's `progress`.
|
||||
- Output: `{ content, metadata: { results } }` where `content` is the formatted
|
||||
per-step `[OK]` / `[FAILED]` / `[SKIPPED]` summary (plus a `Failure output:`
|
||||
section when steps fail) and `metadata.results` is the raw `StepResult[]`.
|
||||
- Workflows failure is non-fatal: if the `OpenCode Workflows plugin` step does
|
||||
not complete, the bootstrap still finishes and the content notes that bounded
|
||||
fan-out falls back to REA's own subagent delegation (that plugin fails to
|
||||
load on newer CLIs; see `docs/NOTES.md` D5).
|
||||
- After the run, the tool calls `ctx.skill.reload()` so newly installed skills
|
||||
are visible without a restart.
|
||||
|
||||
## What this plugin deliberately does NOT do
|
||||
|
||||
- **No MCP registration or config writes.** Setup only calls `agent`/`skill`/`tool`/
|
||||
|
||||
+133
@@ -322,3 +322,136 @@ Root-cause chain (all paths inside the scratch cache):
|
||||
`formatResults` renders `[OK]`/`[FAILED]`/`[SKIPPED]`.
|
||||
- [x] `bun test --path-ignore-patterns 'review/**' test/unit/bootstrap.test.ts` → all pass (9/9).
|
||||
- [x] Workflows-load fact verified in `/tmp` scratch and recorded here (D5: fails on 2.0.32, loads on pinned 2.0.15; fallback = REA subagent delegation, build not blocked).
|
||||
|
||||
---
|
||||
|
||||
# M2 — `rea_bootstrap_stack` tool + `/rea-bootstrap` command discovery log
|
||||
|
||||
Date: 2026-10-18 (UTC). Branch: `claude/project-thread-gnv9zf`.
|
||||
Worktree: `/home/john/Projects/RE/opencode-rea/.worktrees/m0-spike`.
|
||||
Spec: `docs/IMPLEMENTATION_PROMPT.md` §6 M2 + §4.1 (tool transform editor
|
||||
shapes, permission assert), §7, §5 layout. CLI pin stays 2.0.26 (no bump).
|
||||
|
||||
## M2.1 — `ctx.permission.assert` availability probe (new discrepancy D6)
|
||||
|
||||
§4.1/§6-M2 say to call `ctx.permission.assert(...)` before running, citing
|
||||
`unity.gamedev.kit/src/v2.ts`. But `@opencode/[email protected]` types declare no
|
||||
`assert`: `dist/promise/permission.d.ts` is
|
||||
`Pick<PermissionApi, "list" | "get" | "reply"> & { hook }`, and the bundled
|
||||
`@opencode/[email protected]` generated permission API has only
|
||||
`request/saved/create/list/get/reply` — no `assert` anywhere in the package.
|
||||
The Unity kit targets `@opencode/[email protected]`, so the reference may postdate
|
||||
our pin. Verified empirically with a scratch probe plugin (tool returning
|
||||
`typeof ctx.permission.assert` + `Object.keys(ctx.permission)`, driven by the
|
||||
real CLI + fixture model, scratch deleted afterwards):
|
||||
|
||||
- 2.0.26 (`node_modules/.bin/opencode`, `opencode v2.0.26`):
|
||||
`{"typeofAssert":"undefined","permissionKeys":["hook","list","get","reply"]}`
|
||||
- 2.0.32 (`/tmp/opencode/wf-check/node_modules/.bin/opencode`, `opencode v2.0.32`):
|
||||
`{"typeofAssert":"function","permissionKeys":["hook","list","get","reply","assert"]}`
|
||||
|
||||
## Discrepancy vs Section 4 (new)
|
||||
|
||||
- **D6 — `permission.assert` does not exist on the pinned 2.0.26 host; it was
|
||||
added by 2.0.32.** Calling it unconditionally would throw
|
||||
`TypeError: not a function` at runtime on our pin. Design (fail-closed per
|
||||
Agency rules): `src/bootstrap-tool.ts` `readPermissionAssert()` calls the
|
||||
host capability when present and otherwise throws a descriptive error
|
||||
refusing to run installs without approval. Nothing proceeds unapproved on
|
||||
any host. re-box (2.0.32) gates normally. Build not blocked.
|
||||
|
||||
## M2.2 — implementation
|
||||
|
||||
File placement: new `src/bootstrap-tool.ts` (thin; reuses the M1 runner —
|
||||
no `node:fs`/`node:path`, no new runtime deps, no `ctx.mcp.transform`).
|
||||
`src/index.ts` `setup()` registers it after readiness; `src/commands.ts`
|
||||
gains `/rea-bootstrap`; `src/readiness.ts` + `src/bootstrap.ts` adjusted
|
||||
(see below). Exports added to `src/index.ts` for tests/docs-guard.
|
||||
|
||||
- Tool registration mirrors `gamedev_bootstrap_stack`:
|
||||
`editor.add({ name: "rea_bootstrap_stack", input: { installSkills?,
|
||||
installMemory?, installWorkflows? } booleans, additionalProperties: false,
|
||||
options: { codemode: false } })`. `execute` calls `readPermissionAssert`
|
||||
with `{ sessionID, agent, source: { type: "tool", messageID, id }, action:
|
||||
"rea_bootstrap_stack", resources: [directory], save: ["*"] }` **before**
|
||||
`runBootstrap(...)` with `onStep` wired to `context.progress`, then
|
||||
`await ctx.skill.reload()`. Returns `{ content, metadata: { results } }`.
|
||||
- D5 (workflows failure non-fatal): `runBootstrap` already records step
|
||||
failures instead of throwing; the tool additionally appends an explicit
|
||||
fallback note via `withWorkflowsFallbackNote()` when the workflows step is
|
||||
not `ok` ("bounded fan-out falls back to REA's own subagent delegation").
|
||||
New shared const `WORKFLOWS_STEP_LABEL` in `src/bootstrap.ts` (value
|
||||
unchanged, M1 unit assertions untouched).
|
||||
- New-tool fallout in `src/readiness.ts`: the plugin's own
|
||||
`rea_bootstrap_stack` matches the `rea_` prefix, so without an exclusion it
|
||||
would inflate the MCP-only `reaToolCount` (T08/T10 hermetic `0` broke: got
|
||||
1) and a global `codemode` option would clobber its direct mode. Added
|
||||
`PLUGIN_OWN_TOOL_NAMES = { rea_readiness, rea_bootstrap_stack }` skipping it
|
||||
in `countReaToolsByMode` and in the `applyReaToolCatalogPolicy` codemode
|
||||
loop. `test/unit/readiness.test.ts` extended with a bootstrap entry (counts
|
||||
unchanged).
|
||||
- `/rea-bootstrap` (`COMMAND_IDS` + definition): prompt tells the agent to
|
||||
call `rea_bootstrap_stack` (passing requested flags), then `rea_readiness`,
|
||||
and report per-step results + readiness summary + the workflows-fallback
|
||||
note when applicable. No agent switch. `test/unit/commands.test.ts` and
|
||||
`test/integration/commands.test.ts` exact lists updated to the four
|
||||
commands; `test/unit/options.test.ts` setup stub gains the host-guaranteed
|
||||
`location` (unity kit relies on `ctx.location.directory` too).
|
||||
- Docs: `README.md` gains a `/rea-bootstrap` row ("three" → "four") and a
|
||||
"Bootstrap tool: `rea_bootstrap_stack`" section (flags, permission gate +
|
||||
D6 host note, workflows non-fatal + D5 note, `codemode: false`,
|
||||
`{ content, metadata: { results } }`, `ctx.skill.reload()`).
|
||||
`test/unit/docs.test.ts` extended: `"rea-bootstrap"` in the command-id
|
||||
loop + a dedicated test asserting README tokens and `src/bootstrap-tool.ts`
|
||||
markers (`permission.assert`, `ctx.skill.reload`, `runBootstrap`,
|
||||
`codemode`).
|
||||
|
||||
## M2.3 — live gating evidence (real hosts, built plugin, scratch tests deleted)
|
||||
|
||||
Zero-step calls (`installSkills/installMemory/installWorkflows: false`, so no
|
||||
command ever runs) driven by the real CLI + fixture model:
|
||||
|
||||
- 2.0.26 + `--auto`: `tool_use` `status: "error"`,
|
||||
`error: "rea_bootstrap_stack: host permission.assert is unavailable —
|
||||
refusing to run installs without approval. ..."` — fail-closed, exit 0.
|
||||
- 2.0.32 + `--auto`: `tool_use` `status: "completed"`,
|
||||
`metadata: { results: [], ... }`, `output: ""` — host `assert` approved the
|
||||
call end to end.
|
||||
- 2.0.32 without `--auto`: `status: "completed"` as well (standalone-run
|
||||
host policy auto-resolves the programmatic assert; the user-facing denial
|
||||
path — assert rejects → execute rejects before any install — is proven
|
||||
deterministically in `test/integration/bootstrap.test.ts` via an injected
|
||||
rejecting assert, plus the D6 fail-closed path via an assert-less host).
|
||||
Recorded as observed; host approval policy is not this plugin's logic.
|
||||
|
||||
## M2.4 — test evidence
|
||||
|
||||
```
|
||||
$ bun run check # tsc --noEmit -p tsconfig.json → exit 0
|
||||
$ bun run build # tsc -p tsconfig.build.json → exit 0
|
||||
$ bun test --path-ignore-patterns 'review/**' test/unit test/integration
|
||||
126 pass, 0 fail, 2737 expect() calls, 22 files
|
||||
```
|
||||
|
||||
Includes new `test/integration/bootstrap.test.ts` (6 tests: tool listed
|
||||
after `setup()` with `codemode: false` + closed boolean schema; denial
|
||||
rejects with exact assert shape `{ action: "rea_bootstrap_stack",
|
||||
resources: [directory], save: ["*"] }` and no reload; approved all-false run
|
||||
returns `metadata.results: []` + one `skill.reload()`; assert-less host
|
||||
refuses via `/permission\.assert/`; D5 note only on non-ok workflows;
|
||||
`COMMAND_IDS` contains `rea-bootstrap`, no `":"`, command prompt names both
|
||||
tools).
|
||||
|
||||
## M2 acceptance verdict
|
||||
|
||||
- [x] `test/integration/bootstrap.test.ts`: after `setup()`,
|
||||
`ctx.tool.list()` includes `rea_bootstrap_stack`; calling it without
|
||||
approval is rejected (permission asserted with the exact spec shape);
|
||||
`COMMAND_IDS` includes `rea-bootstrap` and contains no `":"`.
|
||||
- [x] `bun run check` passes.
|
||||
`bun test --path-ignore-patterns 'review/**' test/unit test/integration`
|
||||
→ all pass (126/126).
|
||||
- [x] `docs.test.ts` (extended) confirms README documents the new tool and
|
||||
command (fails without the README section).
|
||||
- [x] Workflows failure handled non-fatal (D5 note helper + unit-covered;
|
||||
step failures never throw out of `runBootstrap`).
|
||||
@@ -0,0 +1,150 @@
|
||||
import { WORKFLOWS_STEP_LABEL, formatResults, runBootstrap, type BootstrapOptions, type StepResult } from "./bootstrap.js"
|
||||
|
||||
/**
|
||||
* M2 native tool: `rea_bootstrap_stack`. Mirrors the Unity kit's
|
||||
* `gamedev_bootstrap_stack` (permission-gated, progress-reporting installs,
|
||||
* skill reload afterwards) for the REA stack. Thin by design: it reuses the
|
||||
* M1 runner in `src/bootstrap.ts` and adds no runtime dependencies.
|
||||
*/
|
||||
|
||||
export const BOOTSTRAP_TOOL_NAME = "rea_bootstrap_stack"
|
||||
export const BOOTSTRAP_PERMISSION_ACTION = "rea_bootstrap_stack"
|
||||
|
||||
/** JSON-Schema input: three optional booleans, nothing else. */
|
||||
export const BootstrapToolInputSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
installSkills: { type: "boolean", description: "Install the four RE skill packs. Default: true." },
|
||||
installMemory: { type: "boolean", description: "Install @aerovato/operator-helper and run `operator-helper install opencode-v2`. Default: true." },
|
||||
installWorkflows: { type: "boolean", description: "Install @rphang/opencode-workflows. Default: true." },
|
||||
},
|
||||
additionalProperties: false,
|
||||
} as const
|
||||
|
||||
export interface BootstrapToolInput {
|
||||
readonly installSkills?: boolean
|
||||
readonly installMemory?: boolean
|
||||
readonly installWorkflows?: boolean
|
||||
}
|
||||
|
||||
export interface BootstrapToolCallContext {
|
||||
readonly sessionID: string
|
||||
readonly agent?: string
|
||||
readonly messageID?: string
|
||||
readonly id?: string
|
||||
readonly signal?: AbortSignal
|
||||
readonly progress: (update: { status: string }) => Promise<unknown> | unknown
|
||||
}
|
||||
|
||||
interface BootstrapToolEditorLike {
|
||||
add(tool: {
|
||||
name: string
|
||||
description?: string
|
||||
input: unknown
|
||||
options?: { codemode?: boolean }
|
||||
execute: (input: BootstrapToolInput, context: BootstrapToolCallContext) => Promise<unknown>
|
||||
}): void
|
||||
}
|
||||
|
||||
interface BootstrapToolCtxLike {
|
||||
tool: {
|
||||
transform(cb: (editor: BootstrapToolEditorLike) => void): unknown
|
||||
}
|
||||
skill: {
|
||||
reload(): Promise<unknown>
|
||||
}
|
||||
/** `permission` is untyped: `assert` exists on newer hosts only (see D6). */
|
||||
permission: unknown
|
||||
location: {
|
||||
directory: string
|
||||
}
|
||||
}
|
||||
|
||||
interface PermissionAssertRequest {
|
||||
readonly sessionID: string
|
||||
readonly agent?: string
|
||||
readonly source?: { type: string; messageID?: string; id?: string }
|
||||
readonly action: string
|
||||
readonly resources: ReadonlyArray<string>
|
||||
readonly save: ReadonlyArray<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail-closed permission gate. Newer hosts (observed: 2.0.32) provide
|
||||
* `ctx.permission.assert`; the pinned 2.0.26 host does not (see docs/NOTES.md
|
||||
* D6). When the capability is missing we refuse to run installs rather than
|
||||
* running them unapproved.
|
||||
*/
|
||||
export function readPermissionAssert(permission: unknown): (request: PermissionAssertRequest) => Promise<unknown> {
|
||||
const assert = (permission as { assert?: unknown } | null | undefined)?.assert
|
||||
if (typeof assert !== "function") {
|
||||
throw new Error(
|
||||
`${BOOTSTRAP_TOOL_NAME}: host permission.assert is unavailable — refusing to run installs without approval. ` +
|
||||
`Use a host that provides permission.assert (observed on @opencode/cli 2.0.32), or install the stack manually.`,
|
||||
)
|
||||
}
|
||||
return assert as (request: PermissionAssertRequest) => Promise<unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* D5: the OpenCode Workflows step is known to fail on hosts the plugin does
|
||||
* not support (its upstream pins opencode 2.0.15; it fails to load on
|
||||
* 2.0.32). A non-ok workflows step must not fail the whole bootstrap:
|
||||
* bounded fan-out falls back to REA's own subagent delegation. Surface that
|
||||
* fallback next to the formatted results.
|
||||
*/
|
||||
export function withWorkflowsFallbackNote(content: string, results: readonly StepResult[]): string {
|
||||
const workflows = results.find((result) => result.label === WORKFLOWS_STEP_LABEL)
|
||||
if (workflows && workflows.status !== "ok") {
|
||||
return (
|
||||
`${content}\n\nNote: the "${WORKFLOWS_STEP_LABEL}" step did not complete (${workflows.status}); ` +
|
||||
`bounded fan-out falls back to REA's own subagent delegation (see docs/NOTES.md D5).`
|
||||
)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
function normalizeInput(input: BootstrapToolInput | undefined): BootstrapOptions {
|
||||
return {
|
||||
...(input?.installSkills === undefined ? {} : { installSkills: input.installSkills }),
|
||||
...(input?.installMemory === undefined ? {} : { installMemory: input.installMemory }),
|
||||
...(input?.installWorkflows === undefined ? {} : { installWorkflows: input.installWorkflows }),
|
||||
}
|
||||
}
|
||||
|
||||
/** Register the `rea_bootstrap_stack` native tool via ctx.tool.transform (never ctx.mcp.transform). */
|
||||
export async function registerBootstrapTool(ctx: BootstrapToolCtxLike): Promise<void> {
|
||||
const directory = ctx.location.directory
|
||||
await ctx.tool.transform((tools) => {
|
||||
tools.add({
|
||||
name: BOOTSTRAP_TOOL_NAME,
|
||||
description:
|
||||
"Install the REA reverse-engineering stack (skill packs, operator-memory, parallel-fan-out workflows). Runs package installs, so it asks the user first.",
|
||||
input: BootstrapToolInputSchema,
|
||||
options: { codemode: false },
|
||||
execute: async (input, context) => {
|
||||
const args = normalizeInput(input ?? {})
|
||||
await readPermissionAssert(ctx.permission)({
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source: { type: "tool", messageID: context.messageID, id: context.id },
|
||||
action: BOOTSTRAP_PERMISSION_ACTION,
|
||||
resources: [directory],
|
||||
save: ["*"],
|
||||
})
|
||||
const results = await runBootstrap(directory, args, {
|
||||
signal: context.signal,
|
||||
onStep: (label, index, total) => {
|
||||
void context.progress({ status: `${index}/${total} ${label}` })
|
||||
},
|
||||
})
|
||||
await ctx.skill.reload()
|
||||
const content = withWorkflowsFallbackNote(formatResults(results), results)
|
||||
return { content, metadata: { results } }
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** Convention alias: the module exposes one `register` alongside `registerBootstrapTool`. */
|
||||
export const register = registerBootstrapTool
|
||||
+4
-1
@@ -27,6 +27,9 @@ export interface StepResult {
|
||||
detail: string
|
||||
}
|
||||
|
||||
/** Label of the workflows install step (guarded by a version-probe `skip`; see docs/NOTES.md D5). */
|
||||
export const WORKFLOWS_STEP_LABEL = "OpenCode Workflows plugin"
|
||||
|
||||
/** MIT/Apache-2.0 skill packs installed via `npx skills add`. */
|
||||
export const REA_SKILL_PACKS: ReadonlyArray<{ label: string; repo: string }> = [
|
||||
{ label: "p4nda0s/reverse-skills", repo: "p4nda0s/reverse-skills" },
|
||||
@@ -91,7 +94,7 @@ export function reaBootstrapSteps(cwd: string, o: BootstrapOptions = {}): Bootst
|
||||
}
|
||||
if (o.installWorkflows ?? true) {
|
||||
steps.push({
|
||||
label: "OpenCode Workflows plugin",
|
||||
label: WORKFLOWS_STEP_LABEL,
|
||||
command: "opencode",
|
||||
args: ["plugin", "add", "@rphang/opencode-workflows"],
|
||||
skip: () => workflowsSkipReason(),
|
||||
|
||||
+25
-3
@@ -1,8 +1,9 @@
|
||||
import type { ReaOptions } from "./options.js"
|
||||
import { sanitizeServerNamespace } from "./options.js"
|
||||
import { BOOTSTRAP_TOOL_NAME } from "./bootstrap-tool.js"
|
||||
import { READINESS_TOOL_NAME } from "./readiness.js"
|
||||
|
||||
export const COMMAND_IDS = ["rea", "rea-doctor", "rea-report"] as const
|
||||
export const COMMAND_IDS = ["rea", "rea-doctor", "rea-report", "rea-bootstrap"] as const
|
||||
|
||||
export type CommandId = (typeof COMMAND_IDS)[number]
|
||||
|
||||
@@ -22,7 +23,7 @@ export type ReaPromptBaseName = (typeof REA_PROMPT_BASE_NAMES)[number]
|
||||
export const REA_PROMPT_COMMANDS = REA_PROMPT_BASE_NAMES.map((name) => `rea:${name}` as const)
|
||||
|
||||
// Invariant: none of COMMAND_IDS contains ":" and none equals any of
|
||||
// REA_PROMPT_COMMANDS, so the three slash commands never shadow or duplicate
|
||||
// REA_PROMPT_COMMANDS, so the four slash commands never shadow or duplicate
|
||||
// the six `rea:<prompt>` MCP prompt commands.
|
||||
|
||||
export interface CommandPromptLike {
|
||||
@@ -97,7 +98,16 @@ function reaReportText(args: string): string {
|
||||
return appendArgs(template, args)
|
||||
}
|
||||
|
||||
/** Register the 3 rea slash commands via ctx.command.transform. */
|
||||
function reaBootstrapText(args: string): string {
|
||||
const template = [
|
||||
`Install the REA reverse-engineering stack by calling the ${BOOTSTRAP_TOOL_NAME} tool (passing through any requested installSkills/installMemory/installWorkflows flags), then call the ${READINESS_TOOL_NAME} tool to verify the installation.`,
|
||||
"Report what installed: the per-step results from the bootstrap tool ([OK] / [FAILED] / [SKIPPED] lines) plus the readiness summary (server status, tool counts, skill availability, version).",
|
||||
"If the workflows step did not complete, note that bounded fan-out falls back to REA's own subagent delegation.",
|
||||
].join(" ")
|
||||
return appendArgs(template, args)
|
||||
}
|
||||
|
||||
/** Register the 4 rea slash commands via ctx.command.transform. */
|
||||
export async function registerCommands(ctx: CommandCtxLike, options: ReaOptions): Promise<void> {
|
||||
const readinessTool = READINESS_TOOL_NAME
|
||||
await ctx.command.transform((editor) => {
|
||||
@@ -138,6 +148,18 @@ export async function registerCommands(ctx: CommandCtxLike, options: ReaOptions)
|
||||
})
|
||||
},
|
||||
})
|
||||
editor.add({
|
||||
name: "rea-bootstrap",
|
||||
description: "Install the REA reverse-engineering stack, verify with rea_readiness, and report what installed",
|
||||
execute: async (input) => {
|
||||
await ctx.session.prompt({
|
||||
...input.prompt,
|
||||
sessionID: input.sessionID,
|
||||
text: reaBootstrapText(userText(input)),
|
||||
delivery: input.delivery,
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import { Plugin } from "@opencode/plugin"
|
||||
import { registerAgents } from "./agents.js"
|
||||
import { registerBootstrapTool } from "./bootstrap-tool.js"
|
||||
import { registerCommands } from "./commands.js"
|
||||
import { parseOptions } from "./options.js"
|
||||
import { registerReadiness } from "./readiness.js"
|
||||
import { registerSkills } from "./skills.js"
|
||||
|
||||
export {
|
||||
BOOTSTRAP_PERMISSION_ACTION,
|
||||
BOOTSTRAP_TOOL_NAME,
|
||||
BootstrapToolInputSchema,
|
||||
readPermissionAssert,
|
||||
registerBootstrapTool,
|
||||
withWorkflowsFallbackNote,
|
||||
type BootstrapToolCallContext,
|
||||
type BootstrapToolInput,
|
||||
} from "./bootstrap-tool.js"
|
||||
|
||||
export {
|
||||
AGENT_IDS,
|
||||
buildAgentPermissions,
|
||||
@@ -68,6 +80,7 @@ export default Plugin.define({
|
||||
await registerSkills(ctx as any, options)
|
||||
await registerAgents(ctx as any, options)
|
||||
await registerReadiness(ctx as any, options)
|
||||
await registerBootstrapTool(ctx as any)
|
||||
await registerCommands(ctx as any, options)
|
||||
},
|
||||
})
|
||||
+13
-3
@@ -1,6 +1,7 @@
|
||||
import { readFile } from "node:fs/promises"
|
||||
import { dirname, join } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { BOOTSTRAP_TOOL_NAME } from "./bootstrap-tool.js"
|
||||
import type { ReaOptions } from "./options.js"
|
||||
import { sanitizeServerNamespace } from "./options.js"
|
||||
|
||||
@@ -32,6 +33,15 @@ export type PinnedReaToolName = (typeof PINNED_REA_TOOLS)[number]
|
||||
export const REA_NAMESPACE_DESCRIPTION =
|
||||
"REA reverse-engineering analysis, artifact inspection, version comparison, and runtime observation tools."
|
||||
|
||||
/**
|
||||
* Native tools owned by this plugin rather than the MCP server. They share
|
||||
* the `<sanitized-server>_` prefix, so the rea_ counters and the codemode
|
||||
* override loop must skip them explicitly (otherwise the hermetic count is
|
||||
* wrong and a global `codemode` option would clobber the plugin's own
|
||||
* direct-mode tools).
|
||||
*/
|
||||
const PLUGIN_OWN_TOOL_NAMES: ReadonlySet<string> = new Set([READINESS_TOOL_NAME, BOOTSTRAP_TOOL_NAME])
|
||||
|
||||
export interface ReadinessOutput {
|
||||
readonly server: string
|
||||
readonly status: string
|
||||
@@ -162,7 +172,7 @@ export interface ReaToolCounts {
|
||||
export function countReaToolsByMode(toolListResult: unknown, server: string): ReaToolCounts {
|
||||
// Effective names carry the `<sanitized-server>_` prefix (default server is "rea", so
|
||||
// this is the rea_ count). Entries expose `id` and/or `name`; check both.
|
||||
// The plugin's own readiness tool matches the default prefix, so exclude it:
|
||||
// The plugin's own native tools match the default prefix, so exclude them:
|
||||
// the count reflects MCP-server tools only (0 when the server is absent).
|
||||
const prefix = `${sanitizeServerNamespace(server)}_`
|
||||
const entries: Array<any> = Array.isArray(toolListResult)
|
||||
@@ -176,7 +186,7 @@ export function countReaToolsByMode(toolListResult: unknown, server: string): Re
|
||||
for (const entry of entries) {
|
||||
const id = typeof entry?.id === "string" ? (entry.id as string) : ""
|
||||
const name = typeof entry?.name === "string" ? (entry.name as string) : ""
|
||||
if (id === READINESS_TOOL_NAME || name === READINESS_TOOL_NAME) continue
|
||||
if (PLUGIN_OWN_TOOL_NAMES.has(id) || PLUGIN_OWN_TOOL_NAMES.has(name)) continue
|
||||
if (id.startsWith(prefix) || name.startsWith(prefix)) {
|
||||
reaToolCount++
|
||||
if (entry?.options?.codemode === false) directToolCount++
|
||||
@@ -248,7 +258,7 @@ export function applyReaToolCatalogPolicy(tools: ToolEditorLike, options: ReaOpt
|
||||
: typeof entry?.name === "string"
|
||||
? (entry.name as string)
|
||||
: ""
|
||||
if (!id.startsWith(prefix) || id === READINESS_TOOL_NAME || pinnedIds.has(id)) continue
|
||||
if (!id.startsWith(prefix) || PLUGIN_OWN_TOOL_NAMES.has(id) || pinnedIds.has(id)) continue
|
||||
tools.update(id, (tool) => {
|
||||
tool.options = {
|
||||
...(tool.options ?? {}),
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import plugin from "../../src/index.js"
|
||||
import {
|
||||
BOOTSTRAP_TOOL_NAME,
|
||||
BootstrapToolInputSchema,
|
||||
registerBootstrapTool,
|
||||
withWorkflowsFallbackNote,
|
||||
} from "../../src/bootstrap-tool.js"
|
||||
import { COMMAND_IDS, REA_PROMPT_COMMANDS, registerCommands } from "../../src/commands.js"
|
||||
|
||||
const LIVE_NOTE = "live-model behaviour unverified"
|
||||
|
||||
interface CapturedTool {
|
||||
name: string
|
||||
description?: string
|
||||
input: unknown
|
||||
options?: { codemode?: boolean }
|
||||
execute: (input: any, context: any) => Promise<any>
|
||||
}
|
||||
|
||||
interface SetupCapture {
|
||||
addedTools: CapturedTool[]
|
||||
asserted: Array<Record<string, unknown>>
|
||||
reloaded: { count: number }
|
||||
progressCalls: Array<unknown>
|
||||
directory: string
|
||||
deny: boolean
|
||||
ctx: any
|
||||
}
|
||||
|
||||
/** Fake host ctx proving registration + permission gating without network (all-flag-false runs zero steps). */
|
||||
function makeSetupCapture(opts?: { deny?: boolean; withAssert?: boolean; directory?: string }): SetupCapture {
|
||||
const cap: SetupCapture = {
|
||||
addedTools: [],
|
||||
asserted: [],
|
||||
reloaded: { count: 0 },
|
||||
progressCalls: [],
|
||||
directory: opts?.directory ?? "/tmp/rea-m2-bootstrap-ws",
|
||||
deny: opts?.deny ?? true,
|
||||
ctx: undefined as any,
|
||||
}
|
||||
const permission: Record<string, unknown> =
|
||||
opts?.withAssert === false
|
||||
? {}
|
||||
: {
|
||||
assert: async (request: Record<string, unknown>) => {
|
||||
cap.asserted.push(request)
|
||||
if (cap.deny) throw new Error("permission denied: rea_bootstrap_stack not approved")
|
||||
},
|
||||
}
|
||||
cap.ctx = {
|
||||
options: {},
|
||||
location: { directory: cap.directory },
|
||||
skill: {
|
||||
transform: async (cb: (editor: any) => void) => {
|
||||
cb({ add: () => {} })
|
||||
return { dispose: async () => {} }
|
||||
},
|
||||
reload: async () => {
|
||||
cap.reloaded.count++
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
transform: async (cb: (editor: any) => void) => {
|
||||
cb({ update: () => {} })
|
||||
return { dispose: async () => {} }
|
||||
},
|
||||
},
|
||||
tool: {
|
||||
transform: async (cb: (editor: any) => void) => {
|
||||
cb({
|
||||
add: (tool: CapturedTool) => void cap.addedTools.push(tool),
|
||||
namespace: () => {},
|
||||
update: () => {},
|
||||
list: () => [],
|
||||
})
|
||||
return { dispose: async () => {} }
|
||||
},
|
||||
list: async () => cap.addedTools.map((tool) => ({ id: tool.name, ...tool })),
|
||||
},
|
||||
command: {
|
||||
transform: async (cb: (editor: any) => void) => {
|
||||
cb({ add: () => {} })
|
||||
return { dispose: async () => {} }
|
||||
},
|
||||
},
|
||||
session: { switchAgent: async () => {}, prompt: async () => {} },
|
||||
mcp: { list: async () => [] },
|
||||
permission: { hook: async () => ({}), ...permission },
|
||||
}
|
||||
return cap
|
||||
}
|
||||
|
||||
function callContext(cap: SetupCapture) {
|
||||
return {
|
||||
sessionID: "ses-m2-test",
|
||||
agent: "rea",
|
||||
messageID: "msg-m2-test",
|
||||
id: "call-m2-test",
|
||||
progress: async (update: unknown) => void cap.progressCalls.push(update),
|
||||
}
|
||||
}
|
||||
|
||||
// M2 acceptance: after setup(), ctx.tool.list() includes rea_bootstrap_stack;
|
||||
// calling it without approval is rejected (permission asserted); COMMAND_IDS
|
||||
// includes rea-bootstrap and contains no ":". No model calls, no network:
|
||||
// installs run zero steps under all-false flags (live-model unverified).
|
||||
describe("M2 integration: rea_bootstrap_stack tool + rea-bootstrap command", () => {
|
||||
test("setup() registers rea_bootstrap_stack; tool.list() includes it with codemode:false and a closed input schema", async () => {
|
||||
const cap = makeSetupCapture()
|
||||
const setup = (plugin as unknown as { setup: (ctx: unknown) => Promise<unknown> }).setup
|
||||
await setup(cap.ctx)
|
||||
const listed = (await cap.ctx.tool.list()) as Array<{ id: string }>
|
||||
const ids = listed.map((entry) => entry.id)
|
||||
console.log(`[${LIVE_NOTE}] M2 tool ids after setup:`, JSON.stringify(ids))
|
||||
expect(ids).toContain(BOOTSTRAP_TOOL_NAME)
|
||||
expect(BOOTSTRAP_TOOL_NAME).toBe("rea_bootstrap_stack")
|
||||
|
||||
const entry = cap.addedTools.find((tool) => tool.name === BOOTSTRAP_TOOL_NAME)
|
||||
expect(entry).toBeDefined()
|
||||
expect(entry?.options).toMatchObject({ codemode: false })
|
||||
expect(entry?.input).toEqual(BootstrapToolInputSchema)
|
||||
const schema = entry?.input as { type: string; properties: Record<string, { type: string }>; additionalProperties: boolean }
|
||||
expect(schema.type).toBe("object")
|
||||
expect(schema.additionalProperties).toBe(false)
|
||||
expect(Object.keys(schema.properties).sort()).toEqual(["installMemory", "installSkills", "installWorkflows"])
|
||||
for (const prop of Object.values(schema.properties)) {
|
||||
expect(prop.type).toBe("boolean")
|
||||
}
|
||||
expect(typeof entry?.description).toBe("string")
|
||||
expect((entry?.description ?? "").length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test("calling rea_bootstrap_stack without approval is rejected; permission.assert runs first with the exact shape", async () => {
|
||||
const cap = makeSetupCapture({ deny: true })
|
||||
await registerBootstrapTool(cap.ctx)
|
||||
const entry = cap.addedTools.find((tool) => tool.name === BOOTSTRAP_TOOL_NAME)
|
||||
expect(entry).toBeDefined()
|
||||
await expect(entry!.execute({}, callContext(cap))).rejects.toThrow(/not approved/)
|
||||
// Permission was asserted exactly once, BEFORE any install ran.
|
||||
expect(cap.asserted).toHaveLength(1)
|
||||
expect(cap.asserted[0]).toMatchObject({
|
||||
action: "rea_bootstrap_stack",
|
||||
resources: [cap.directory],
|
||||
save: ["*"],
|
||||
})
|
||||
expect(cap.asserted[0]?.sessionID).toBe("ses-m2-test")
|
||||
expect(cap.asserted[0]?.agent).toBe("rea")
|
||||
expect(cap.asserted[0]?.source).toMatchObject({ type: "tool", messageID: "msg-m2-test", id: "call-m2-test" })
|
||||
// Denial means no installs ran and no skill reload happened.
|
||||
expect(cap.reloaded.count).toBe(0)
|
||||
})
|
||||
|
||||
test("approved call with all flags false runs zero steps, reports results metadata, and reloads skills", async () => {
|
||||
const cap = makeSetupCapture({ deny: false })
|
||||
await registerBootstrapTool(cap.ctx)
|
||||
const entry = cap.addedTools.find((tool) => tool.name === BOOTSTRAP_TOOL_NAME)
|
||||
expect(entry).toBeDefined()
|
||||
const res = await entry!.execute(
|
||||
{ installSkills: false, installMemory: false, installWorkflows: false },
|
||||
callContext(cap),
|
||||
)
|
||||
expect(cap.asserted).toHaveLength(1)
|
||||
expect(res?.metadata?.results).toEqual([])
|
||||
expect(typeof res?.content).toBe("string")
|
||||
expect(cap.reloaded.count).toBe(1)
|
||||
console.log(`[${LIVE_NOTE}] M2 approved zero-step content:`, JSON.stringify(res?.content))
|
||||
})
|
||||
|
||||
test("fail-closed: a host without permission.assert refuses instead of running unapproved installs (D6)", async () => {
|
||||
const cap = makeSetupCapture({ deny: false, withAssert: false })
|
||||
await registerBootstrapTool(cap.ctx)
|
||||
const entry = cap.addedTools.find((tool) => tool.name === BOOTSTRAP_TOOL_NAME)
|
||||
expect(entry).toBeDefined()
|
||||
await expect(entry!.execute({}, callContext(cap))).rejects.toThrow(/permission\.assert/)
|
||||
expect(cap.reloaded.count).toBe(0)
|
||||
})
|
||||
|
||||
test("workflows fallback note is appended only when the workflows step did not complete (D5)", () => {
|
||||
const clean = withWorkflowsFallbackNote("[OK] OpenCode Workflows plugin", [
|
||||
{ label: "OpenCode Workflows plugin", status: "ok", detail: "done" },
|
||||
])
|
||||
expect(clean).not.toContain("falls back")
|
||||
for (const status of ["failed", "skipped"] as const) {
|
||||
const noted = withWorkflowsFallbackNote(`[${status === "failed" ? "FAILED" : "SKIPPED"}] OpenCode Workflows plugin`, [
|
||||
{ label: "OpenCode Workflows plugin", status, detail: "reason" },
|
||||
])
|
||||
expect(noted).toContain("falls back to REA's own subagent delegation")
|
||||
expect(noted).toContain("D5")
|
||||
}
|
||||
})
|
||||
|
||||
test("COMMAND_IDS includes rea-bootstrap and contains no ':'", async () => {
|
||||
expect(COMMAND_IDS).toContain("rea-bootstrap")
|
||||
for (const id of COMMAND_IDS) {
|
||||
expect(id).not.toContain(":")
|
||||
expect((REA_PROMPT_COMMANDS as ReadonlyArray<string>).includes(id)).toBe(false)
|
||||
}
|
||||
const added: Array<any> = []
|
||||
const prompts: Array<any> = []
|
||||
const fakeCtx = {
|
||||
command: {
|
||||
transform: async (cb: (editor: any) => void) => {
|
||||
cb({ add: (d: any) => void added.push(d) })
|
||||
return { dispose: async () => {} }
|
||||
},
|
||||
},
|
||||
session: { switchAgent: async () => {}, prompt: async (input: any) => void prompts.push(input) },
|
||||
}
|
||||
await registerCommands(fakeCtx as any, { server: "rea", runtime: "ask" } as any)
|
||||
const def = added.find((d) => d.name === "rea-bootstrap")
|
||||
expect(def).toBeDefined()
|
||||
expect(typeof def?.description).toBe("string")
|
||||
expect((def?.description ?? "").length).toBeGreaterThan(0)
|
||||
await def.execute({ sessionID: "s-bootstrap", prompt: { text: "" }, delivery: undefined })
|
||||
const text = String(prompts[0]?.text ?? "")
|
||||
expect(text).toContain("rea_bootstrap_stack")
|
||||
expect(text).toContain("rea_readiness")
|
||||
})
|
||||
})
|
||||
@@ -85,7 +85,7 @@ describe("T09 commands via real host", () => {
|
||||
}, 60_000)
|
||||
|
||||
test("COMMAND_IDS exact; no shadowing of rea:<prompt> commands", () => {
|
||||
expect([...COMMAND_IDS]).toEqual(["rea", "rea-doctor", "rea-report"])
|
||||
expect([...COMMAND_IDS]).toEqual(["rea", "rea-doctor", "rea-report", "rea-bootstrap"])
|
||||
expect(REA_PROMPT_COMMANDS).toHaveLength(6)
|
||||
for (const name of COMMAND_IDS) {
|
||||
expect(name).not.toContain(":")
|
||||
@@ -111,7 +111,7 @@ async function collectDefinitions(options: { server: string; runtime: "ask" | "a
|
||||
|
||||
async function assertDirectRegistration() {
|
||||
const added = await collectDefinitions({ server: "rea", runtime: "ask" })
|
||||
expect(added.map((d) => d.name)).toEqual(["rea", "rea-doctor", "rea-report"])
|
||||
expect(added.map((d) => d.name)).toEqual(["rea", "rea-doctor", "rea-report", "rea-bootstrap"])
|
||||
for (const d of added) {
|
||||
expect(typeof d.description).toBe("string")
|
||||
expect((d.description ?? "").length).toBeGreaterThan(0)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { COMMAND_IDS, REA_PROMPT_COMMANDS, registerCommands } from "../../src/co
|
||||
|
||||
describe("T09 unit: slash commands registration and execution", () => {
|
||||
test("COMMAND_IDS and REA_PROMPT_COMMANDS do not overlap", () => {
|
||||
expect([...COMMAND_IDS]).toEqual(["rea", "rea-doctor", "rea-report"])
|
||||
expect([...COMMAND_IDS]).toEqual(["rea", "rea-doctor", "rea-report", "rea-bootstrap"])
|
||||
expect(REA_PROMPT_COMMANDS).toHaveLength(6)
|
||||
for (const cmd of COMMAND_IDS) {
|
||||
expect(cmd).not.toContain(":")
|
||||
@@ -11,7 +11,7 @@ describe("T09 unit: slash commands registration and execution", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("registerCommands wires /rea, /rea-doctor, and /rea-report with server context and user args", async () => {
|
||||
test("registerCommands wires /rea, /rea-doctor, /rea-report, and /rea-bootstrap with server context and user args", async () => {
|
||||
const added: any[] = []
|
||||
const sessionCalls: Array<{ method: string; input: any }> = []
|
||||
const ctx: any = {
|
||||
@@ -28,7 +28,7 @@ describe("T09 unit: slash commands registration and execution", () => {
|
||||
}
|
||||
|
||||
await registerCommands(ctx, { server: "custom.rea", runtime: "ask" })
|
||||
expect(added.map((d) => d.name)).toEqual(["rea", "rea-doctor", "rea-report"])
|
||||
expect(added.map((d) => d.name)).toEqual(["rea", "rea-doctor", "rea-report", "rea-bootstrap"])
|
||||
|
||||
// /rea switches to primary rea agent and passes target prompt + sanitized server namespace
|
||||
const reaCmd = added.find((d) => d.name === "rea")
|
||||
@@ -55,5 +55,13 @@ describe("T09 unit: slash commands registration and execution", () => {
|
||||
expect(sessionCalls.some((c) => c.method === "switchAgent")).toBe(false)
|
||||
expect(sessionCalls[0]?.input?.text).toContain("rea-auditor")
|
||||
expect(sessionCalls[0]?.input?.text).toContain("focus on TLS pinning")
|
||||
|
||||
// /rea-bootstrap tells the agent to call rea_bootstrap_stack then rea_readiness and report
|
||||
sessionCalls.length = 0
|
||||
const bootstrapCmd = added.find((d) => d.name === "rea-bootstrap")
|
||||
await bootstrapCmd.execute({ sessionID: "s4", prompt: { text: "" }, delivery: undefined })
|
||||
expect(sessionCalls.some((c) => c.method === "switchAgent")).toBe(false)
|
||||
expect(sessionCalls[0]?.input?.text).toContain("rea_bootstrap_stack")
|
||||
expect(sessionCalls[0]?.input?.text).toContain("rea_readiness")
|
||||
})
|
||||
})
|
||||
+22
-1
@@ -78,7 +78,7 @@ describe("T15 docs guard (README accuracy)", () => {
|
||||
test("every command README names exists in src/commands.ts exports", async () => {
|
||||
const readme = await Bun.file("README.md").text()
|
||||
const commandsSrc = await Bun.file("src/commands.ts").text()
|
||||
for (const id of ["rea", "rea-doctor", "rea-report"]) {
|
||||
for (const id of ["rea", "rea-doctor", "rea-report", "rea-bootstrap"]) {
|
||||
expect(readme).toContain(id)
|
||||
expect(commandsSrc).toContain(`"${id}"`)
|
||||
}
|
||||
@@ -121,4 +121,25 @@ describe("T15 docs guard (README accuracy)", () => {
|
||||
// Plugin never registers or edits an MCP server.
|
||||
expect(readme).toContain("ctx.mcp.transform")
|
||||
})
|
||||
|
||||
test("README documents the rea_bootstrap_stack tool and rea-bootstrap command", async () => {
|
||||
const readme = await Bun.file("README.md").text()
|
||||
for (const token of [
|
||||
"rea_bootstrap_stack",
|
||||
"rea-bootstrap",
|
||||
"installSkills",
|
||||
"installMemory",
|
||||
"installWorkflows",
|
||||
]) {
|
||||
expect(readme).toContain(token)
|
||||
}
|
||||
const toolSrc = await Bun.file("src/bootstrap-tool.ts").text()
|
||||
expect(toolSrc).toContain("rea_bootstrap_stack")
|
||||
expect(toolSrc).toContain("permission.assert")
|
||||
expect(toolSrc).toContain("ctx.skill.reload")
|
||||
expect(toolSrc).toContain("runBootstrap")
|
||||
expect(toolSrc).toContain("codemode")
|
||||
const commandsSrc = await Bun.file("src/commands.ts").text()
|
||||
expect(commandsSrc).toContain(`"rea-bootstrap"`)
|
||||
})
|
||||
})
|
||||
@@ -123,6 +123,7 @@ describe("T04 options boundary: setup wiring", () => {
|
||||
expect(typeof setup).toBe("function")
|
||||
const withSkill = (options: unknown) => ({
|
||||
options,
|
||||
location: { directory: process.cwd() },
|
||||
skill: { transform: async () => ({ dispose: async () => {} }) },
|
||||
agent: { transform: async () => ({ dispose: async () => {} }) },
|
||||
tool: { transform: async () => ({ dispose: async () => {} }) },
|
||||
|
||||
@@ -11,11 +11,13 @@ import {
|
||||
countReaToolsByMode,
|
||||
registerReadiness,
|
||||
} from "../../src/readiness.js"
|
||||
import { BOOTSTRAP_TOOL_NAME } from "../../src/bootstrap-tool.js"
|
||||
|
||||
describe("T08 unit: readiness & Code Mode tool catalog policy", () => {
|
||||
test("countReaMcpTools and countReaToolsByMode count MCP tools while excluding rea_readiness", () => {
|
||||
test("countReaMcpTools and countReaToolsByMode count MCP tools while excluding plugin-owned tools", () => {
|
||||
const tools = [
|
||||
{ id: READINESS_TOOL_NAME, name: READINESS_TOOL_NAME, options: { codemode: false } },
|
||||
{ id: BOOTSTRAP_TOOL_NAME, name: BOOTSTRAP_TOOL_NAME, options: { codemode: false } },
|
||||
{ id: "rea_binary_session", name: "rea_binary_session", options: { codemode: true, pinned: true } },
|
||||
{ id: "rea_inspect_artifact", name: "rea_inspect_artifact", options: {} },
|
||||
{ id: "rea_read_native_function", name: "rea_read_native_function", options: { codemode: false } },
|
||||
|
||||
Reference in new issue
Block a user