feat(pi): first-class support via pi-subagents + pi-ask-user (#651)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -193,6 +193,13 @@ bunx @every-env/compound-plugin install compound-engineering --to gemini
|
||||
bunx @every-env/compound-plugin install compound-engineering --to kiro
|
||||
```
|
||||
|
||||
**Pi prerequisites.** Pi does not ship a native subagent primitive, so the Pi install depends on [nicobailon/pi-subagents](https://github.com/nicobailon/pi-subagents) (required) and recommends [edlsh/pi-ask-user](https://github.com/edlsh/pi-ask-user) for richer blocking user questions:
|
||||
|
||||
```bash
|
||||
pi install npm:pi-subagents # required — provides the `subagent` tool used by skills that dispatch parallel agents
|
||||
pi install npm:pi-ask-user # recommended — provides the `ask_user` tool; skills fall back to numbered options in chat when it is missing
|
||||
```
|
||||
|
||||
To auto-detect custom-install targets and install to all:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -122,7 +122,7 @@ Keep rationale at the highest-level location that covers it; restate behavioral
|
||||
|
||||
### Cross-Platform User Interaction
|
||||
|
||||
- [ ] When a skill needs to ask the user a question, instruct use of the platform's blocking question tool and name the known equivalents (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini)
|
||||
- [ ] When a skill needs to ask the user a question, instruct use of the platform's blocking question tool and name the known equivalents (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi via the `pi-ask-user` extension)
|
||||
- [ ] For Claude Code, also instruct to load `AskUserQuestion` via `ToolSearch` with `select:AskUserQuestion` first if its schema isn't already loaded — `AskUserQuestion` is a deferred tool and won't be available at session start. A pending schema load is not a valid reason to fall back to text.
|
||||
- [ ] Include a fallback: when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes where `request_user_input` is unavailable, or `ToolSearch` returns no match), present numbered options in chat and wait for the user's reply — never silently skip the question.
|
||||
- [ ] **Narrow exception for legitimate option overflow:** when a menu has 5 or more genuinely relevant options — each a distinct destination or workflow, none removable without losing real user choice — render as a numbered list in chat rather than trimming to fit the 4-option cap. This is used with restraint, not as a convenience escape from the blocking tool. Default remains the blocking tool. Before invoking the exception, verify that (a) no option can be cut, (b) no two options can be merged, and (c) no option is better surfaced as contextual prose (e.g., a nudge adjacent to the menu). If any of those reductions work, prefer them over the fallback. When the exception applies, include a hint that free-form input is accepted (e.g., "Pick a number or describe what you want.") so the numbered list retains the blocking tool's open-endedness.
|
||||
@@ -148,7 +148,12 @@ Design rules for blocking question menus (`AskUserQuestion` / `request_user_inpu
|
||||
|
||||
- [ ] When a skill needs to create or track tasks, describe the intent (e.g., "create a task list") and name the known equivalents (`TaskCreate`/`TaskUpdate`/`TaskList` in Claude Code, `update_plan` in Codex)
|
||||
- [ ] Do not reference `TodoWrite` or `TodoRead` — these are legacy Claude Code tools replaced by `TaskCreate`/`TaskUpdate`/`TaskList`
|
||||
- [ ] When a skill dispatches sub-agents, prefer parallel execution but include a sequential fallback for platforms that do not support parallel dispatch
|
||||
|
||||
### Cross-Platform Sub-Agent Dispatch
|
||||
|
||||
- [ ] When a skill dispatches sub-agents, instruct use of the platform's subagent primitive and name the known equivalents (`Agent`/`Task` in Claude Code, `spawn_agent` in Codex, `subagent` in Pi via the `pi-subagents` extension)
|
||||
- [ ] Prefer parallel execution but include a sequential fallback for platforms that do not support parallel dispatch
|
||||
- [ ] Prefer sub-agents shipped with this plugin (`ce-*`) over platform built-ins. Built-ins have different names on each target (e.g., Claude Code's `Explore` is `explorer` on Codex via `spawn_agent`'s `agent_type`, `scout` on Pi via `pi-subagents`) — using our own avoids the enumeration tax. Exception: when a built-in offers a meaningful benefit worth keeping, enumerate the per-platform equivalents inline at the call site so the model can route correctly on each target.
|
||||
|
||||
### Script Path References in Skills
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Select option 7 (action parity) to load the full reference material.
|
||||
|
||||
### Step 2: Launch Parallel Sub-Agents
|
||||
|
||||
Launch 8 parallel sub-agents using the Task tool with `subagent_type: Explore`, one for each principle. Each agent should:
|
||||
Launch 8 parallel sub-agents using the platform's subagent primitive (`Agent` with `subagent_type: Explore` in Claude Code, `spawn_agent` with `agent_type: "explorer"` in Codex, `subagent` with `agent: "scout"` in Pi via the `pi-subagents` extension), one for each principle. Each agent should:
|
||||
|
||||
1. Enumerate ALL instances in the codebase (user actions, tools, contexts, data stores, etc.)
|
||||
2. Check compliance against the principle
|
||||
|
||||
@@ -30,7 +30,7 @@ This skill does not implement code. It explores, clarifies, and documents decisi
|
||||
1. **Ask one question at a time** - Do not batch several unrelated questions into one message.
|
||||
2. **Prefer single-select multiple choice** - Use single-select when choosing one direction, one priority, or one next step.
|
||||
3. **Use multi-select rarely and intentionally** - Use it only for compatible sets such as goals, constraints, non-goals, or success criteria that can all coexist. If prioritization matters, follow up by asking which selected item is primary.
|
||||
4. **Use the platform's blocking question tool** - `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
4. **Use the platform's blocking question tool** - `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
## Output Guidance
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ This content is loaded when Phase 4 begins — after the requirements document i
|
||||
|
||||
The Phase 4 menu's visible option count varies by state: no requirements doc hides the review and Proof options, unresolved `Resolve Before Planning` hides `Plan implementation` and `Build it now`, a failing direct-to-work gate hides `Build it now`. Count the visible options for the current state and choose the rendering mode accordingly:
|
||||
|
||||
- **4 or fewer visible:** use the platform's blocking question tool (`AskUserQuestion` in Claude Code — call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded; `request_user_input` in Codex; `ask_user` in Gemini). This is the default per the plugin AGENTS.md "Interactive Question Tool Design" section.
|
||||
- **4 or fewer visible:** use the platform's blocking question tool (`AskUserQuestion` in Claude Code — call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded; `request_user_input` in Codex; `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)). This is the default per the plugin AGENTS.md "Interactive Question Tool Design" section.
|
||||
- **5 or more visible:** render as a numbered list in chat. This is the narrow case-3 overflow exception documented in the same AGENTS.md section; trimming would hide legitimate choices (plan, review, Proof, build, refine, pause are all distinct destinations).
|
||||
|
||||
Never silently skip the question.
|
||||
|
||||
@@ -45,7 +45,7 @@ When the conversation has enough material to narrow — reflect back what you've
|
||||
|
||||
**Always synthesize a summary in the chat.** Before offering any next steps, reflect back what emerged: key decisions, the direction chosen, open threads, and any assumptions made. This is the primary output of the brainstorm — the user should be able to read the summary and know what they landed on.
|
||||
|
||||
**Then offer next steps** using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
**Then offer next steps** using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
**Question:** "Brainstorm wrapped. What would you like to do next?"
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ These local branches have been deleted from the remote:
|
||||
Delete all of them? (y/n)
|
||||
```
|
||||
|
||||
Wait for the user's answer using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to presenting the list in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
Wait for the user's answer using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting the list in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
This is a yes-or-no decision on the entire list -- do not offer multi-selection or per-branch choices.
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ All tokens are optional. Each one present means one less thing to infer. When ab
|
||||
|
||||
### Headless mode rules
|
||||
|
||||
- **Skip all user questions.** Never use the platform question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini) or other interactive prompts. Infer intent conservatively if the diff metadata is thin.
|
||||
- **Skip all user questions.** Never use the platform question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)) or other interactive prompts. Infer intent conservatively if the diff metadata is thin.
|
||||
- **Require a determinable diff scope.** If headless mode cannot determine a diff scope (no branch, PR, or `base:` ref determinable without user interaction), emit `Review failed (headless mode). Reason: no diff scope detected. Re-invoke with a branch name, PR number, or base:<ref>.` and stop without dispatching agents.
|
||||
- **Apply only `safe_auto -> review-fixer` findings in a single pass.** No bounded re-review rounds. Leave `gated_auto`, `manual`, `human`, and `release` work unresolved and return them in the structured output.
|
||||
- **Return all non-auto findings as structured text output.** Use the headless output envelope format (see Stage 6 below) preserving severity, autofix_class, owner, requires_verification, confidence, pre_existing, and suggested_fix per finding. Enrich with detail-tier fields (why_it_matters, evidence[]) from the per-agent artifact files on disk (see Detail enrichment in Stage 6).
|
||||
@@ -72,7 +72,7 @@ All tokens are optional. Each one present means one less thing to infer. When ab
|
||||
|
||||
### Interactive mode rules
|
||||
|
||||
- **Pre-load the platform question tool before any question fires.** In Claude Code, `AskUserQuestion` is a deferred tool — its schema is not available at session start. At the start of Interactive-mode work (before Stage 2 intent-ambiguity questions, the After-Review routing question, walk-through per-finding questions, bulk-preview Proceed/Cancel, and tracker-defer failure sub-questions), call `ToolSearch` with query `select:AskUserQuestion` to load the schema. Load it **once, eagerly, at the top of the Interactive flow** — do not wait for the first question site and do not decide it on a per-site basis. On Codex and Gemini this preload step does not apply.
|
||||
- **Pre-load the platform question tool before any question fires.** In Claude Code, `AskUserQuestion` is a deferred tool — its schema is not available at session start. At the start of Interactive-mode work (before Stage 2 intent-ambiguity questions, the After-Review routing question, walk-through per-finding questions, bulk-preview Proceed/Cancel, and tracker-defer failure sub-questions), call `ToolSearch` with query `select:AskUserQuestion` to load the schema. Load it **once, eagerly, at the top of the Interactive flow** — do not wait for the first question site and do not decide it on a per-site basis. On Codex, Gemini, and Pi this preload step does not apply.
|
||||
- **The numbered-list fallback only applies when the harness genuinely lacks a blocking question tool** — `ToolSearch` returns no match, the tool call explicitly fails, or the runtime mode does not expose it (e.g., Codex edit modes where `request_user_input` is unavailable). A pending schema load is not a fallback trigger; call `ToolSearch` first per the pre-load rule. Rendering a question as narrative text because the tool feels inconvenient, because the model is in report-formatting mode, or because the instruction was buried in a long skill is a bug. A question that calls for a user decision must either fire the tool or fall back loudly.
|
||||
|
||||
## Severity Scale
|
||||
@@ -337,7 +337,7 @@ Pass this to every reviewer in their spawn prompt. Intent shapes *how hard each
|
||||
|
||||
**When intent is ambiguous:**
|
||||
|
||||
- **Interactive mode:** Ask one question using the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini): "What is the primary goal of these changes?" Do not spawn reviewers until intent is established. **Claude Code only:** if `AskUserQuestion` has not yet been loaded this session (per the Interactive mode rules pre-load), call `ToolSearch` with query `select:AskUserQuestion` first before asking. Fall back to numbered options in chat only when the harness genuinely lacks a blocking tool or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
- **Interactive mode:** Ask one question using the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)): "What is the primary goal of these changes?" Do not spawn reviewers until intent is established. **Claude Code only:** if `AskUserQuestion` has not yet been loaded this session (per the Interactive mode rules pre-load), call `ToolSearch` with query `select:AskUserQuestion` first before asking. Fall back to numbered options in chat only when the harness genuinely lacks a blocking tool or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
- **Autofix/report-only/headless modes:** Infer intent conservatively from the branch name, diff, PR metadata, and caller context. Note the uncertainty in Coverage or Verdict reasoning instead of blocking.
|
||||
|
||||
### Stage 2b: Plan discovery (requirements verification)
|
||||
@@ -698,8 +698,8 @@ After presenting findings and verdict (Stage 6), route the next steps by mode. R
|
||||
- Apply `safe_auto -> review-fixer` findings automatically without asking. These are safe by definition.
|
||||
- **Zero-remaining case:** if no `gated_auto` or `manual` findings remain after the `safe_auto` pass, skip the routing question entirely. Emit a one-line completion summary phrased so advisory and pre-existing findings (which are not handled by this flow) are not implied to be cleared. When no advisory or pre-existing findings remain in the report, `All findings resolved — N safe_auto fixes applied.` is accurate. When advisory and/or pre-existing findings do remain, use the qualified form `All actionable findings resolved — N safe_auto fixes applied. (K advisory, J pre-existing findings remain in the report.)`, omitting any zero-count clause. Follow the summary with the existing end-of-review verdict, then proceed to Step 5 per the gating rule there.
|
||||
- **Tracker pre-detection:** before rendering the routing question, consult `references/tracker-defer.md` for the session's tracker tuple `{ tracker_name, confidence, named_sink_available, any_sink_available }`. The probe runs at most once per session and is cached for the rest of the run. `named_sink_available` drives the option C label (inline tracker name only when the named sink can actually be invoked). `any_sink_available` drives whether option C is offered at all (it can still be offered when the named tracker is unreachable but GitHub Issues via `gh` works).
|
||||
- **Verify question-tool pre-load (checklist, Claude Code only).** Before firing the routing question in Claude Code, confirm `AskUserQuestion` is loaded (per Interactive mode rules at the top of this skill). If not yet loaded this session, call `ToolSearch` with query `select:AskUserQuestion` now. Do not proceed to the routing question without this verification. Rendering the question as narrative text because the schema isn't loaded yet is a bug, not a valid fallback. On Codex and Gemini this checklist does not apply — there is no `ToolSearch` preload step to perform. (If `request_user_input` is unavailable in the current Codex runtime mode, use the numbered-list fallback described below.)
|
||||
- **Routing question.** Ask using the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). Stem: `What should the agent do with the remaining N findings?` — use third-person voice referring to "the agent", not first-person "me" / "I". Options:
|
||||
- **Verify question-tool pre-load (checklist, Claude Code only).** Before firing the routing question in Claude Code, confirm `AskUserQuestion` is loaded (per Interactive mode rules at the top of this skill). If not yet loaded this session, call `ToolSearch` with query `select:AskUserQuestion` now. Do not proceed to the routing question without this verification. Rendering the question as narrative text because the schema isn't loaded yet is a bug, not a valid fallback. On Codex, Gemini, and Pi this checklist does not apply — there is no `ToolSearch` preload step to perform. (If `request_user_input` is unavailable in the current Codex runtime mode, use the numbered-list fallback described below.)
|
||||
- **Routing question.** Ask using the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)). Stem: `What should the agent do with the remaining N findings?` — use third-person voice referring to "the agent", not first-person "me" / "I". Options:
|
||||
|
||||
```
|
||||
(A) Review each finding one by one — accept the recommendation or choose another action
|
||||
|
||||
@@ -86,7 +86,7 @@ When no `why_it_matters` is available for a finding (e.g., Unit 2's template upg
|
||||
|
||||
## Question and options
|
||||
|
||||
After the preview body is rendered, ask the user using the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). In Claude Code, the tool should already be loaded from the Interactive-mode pre-load step — if it isn't, call `ToolSearch` with query `select:AskUserQuestion` now. The text fallback below applies only when the harness genuinely lacks a blocking tool — `ToolSearch` returns no match, the tool call explicitly fails, or the runtime mode does not expose it (e.g., Codex edit modes without `request_user_input`). A pending schema load is not a fallback trigger. Never silently skip the question.
|
||||
After the preview body is rendered, ask the user using the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)). In Claude Code, the tool should already be loaded from the Interactive-mode pre-load step — if it isn't, call `ToolSearch` with query `select:AskUserQuestion` now. The text fallback below applies only when the harness genuinely lacks a blocking tool — `ToolSearch` returns no match, the tool call explicitly fails, or the runtime mode does not expose it (e.g., Codex edit modes without `request_user_input`). A pending schema load is not a fallback trigger. Never silently skip the question.
|
||||
|
||||
Stem (adapted to the path):
|
||||
- For routing B: `The agent is about to apply the plan above. Proceed?`
|
||||
|
||||
@@ -144,6 +144,6 @@ When uncertain, prefer "drop with explicit user-facing notice" over "pass throug
|
||||
|
||||
## Cross-platform notes
|
||||
|
||||
The question-tool name varies by platform. In Interactive mode, use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). In Claude Code the tool should already be loaded from the Interactive-mode pre-load step — if it isn't, call `ToolSearch` with query `select:AskUserQuestion` now. Fall back to numbered options in chat only when the harness genuinely lacks a blocking tool — `ToolSearch` returns no match, the tool call explicitly fails, or the runtime mode does not expose it (e.g., Codex edit modes without `request_user_input`). A pending schema load is not a fallback trigger. Never silently skip the question.
|
||||
The question-tool name varies by platform. In Interactive mode, use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)). In Claude Code the tool should already be loaded from the Interactive-mode pre-load step — if it isn't, call `ToolSearch` with query `select:AskUserQuestion` now. Fall back to numbered options in chat only when the harness genuinely lacks a blocking tool — `ToolSearch` returns no match, the tool call explicitly fails, or the runtime mode does not expose it (e.g., Codex edit modes without `request_user_input`). A pending schema load is not a fallback trigger. Never silently skip the question.
|
||||
|
||||
Non-interactive mode is platform-agnostic: it never prompts, so the platform's question tool is not relevant.
|
||||
|
||||
@@ -20,7 +20,7 @@ Each finding's recommended action has already been normalized by Stage 5 (step 7
|
||||
|
||||
## Per-finding presentation
|
||||
|
||||
Each finding is presented in two parts: a **terminal output block** carrying the explanation, and a **question** via the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini) carrying the decision. Never merge the two — the terminal block uses markdown; the question uses plain text.
|
||||
Each finding is presented in two parts: a **terminal output block** carrying the explanation, and a **question** via the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)) carrying the decision. Never merge the two — the terminal block uses markdown; the question uses plain text.
|
||||
|
||||
In Claude Code the tool should already be loaded from the Interactive-mode pre-load step in `SKILL.md` — if it isn't, call `ToolSearch` with query `select:AskUserQuestion` now. Fall back to presenting the per-finding options as a numbered list only when the harness genuinely lacks a blocking tool — `ToolSearch` returns no match, the tool call explicitly fails, or the runtime mode does not expose it (e.g., Codex edit modes without `request_user_input`). A pending schema load is not a fallback trigger. Never silently skip the question.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ description: Commit, push, and open a PR with an adaptive, value-first descripti
|
||||
|
||||
Go from working changes to an open pull request, or rewrite an existing PR description.
|
||||
|
||||
**Asking the user:** When this skill says "ask the user", use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to presenting the question in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
**Asking the user:** When this skill says "ask the user", use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting the question in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
## Mode detection
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ If both fail, fall back to `main`.
|
||||
|
||||
If the git status from the context above shows a clean working tree (no staged, modified, or untracked files), report that there is nothing to commit and stop.
|
||||
|
||||
If the current branch from the context above is empty, the repository is in detached HEAD state. Explain that a branch is required before committing if the user wants this work attached to a branch. Ask whether to create a feature branch now. Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
If the current branch from the context above is empty, the repository is in detached HEAD state. Explain that a branch is required before committing if the user wants this work attached to a branch. Ask whether to create a feature branch now. Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
- If the user chooses to create a branch, derive the name from the change content, create it with `git checkout -b <branch-name>`, then run `git branch --show-current` again and use that result as the current branch name for the rest of the workflow.
|
||||
- If the user declines, continue with the detached HEAD commit.
|
||||
@@ -80,7 +80,7 @@ Keep this lightweight:
|
||||
|
||||
### Step 4: Stage and commit
|
||||
|
||||
If the current branch from the context above is `main`, `master`, or the resolved default branch from Step 1, warn the user and ask whether to continue committing here or create a feature branch first. Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question. If the user chooses to create a branch, derive the name from the change content, create it with `git checkout -b <branch-name>`, then continue.
|
||||
If the current branch from the context above is `main`, `master`, or the resolved default branch from Step 1, warn the user and ask whether to continue committing here or create a feature branch first. Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question. If the user chooses to create a branch, derive the name from the change content, create it with `git checkout -b <branch-name>`, then continue.
|
||||
|
||||
Write the commit message:
|
||||
- **Subject line**: Concise, imperative mood, focused on *why* not *what*. Follow the convention determined in Step 2.
|
||||
|
||||
@@ -31,7 +31,7 @@ Check if `$ARGUMENTS` contains `mode:autofix`. If present, strip it from argumen
|
||||
|
||||
Follow the same interaction style as `ce-brainstorm`:
|
||||
|
||||
- Ask questions **one at a time** — use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in plain text only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question
|
||||
- Ask questions **one at a time** — use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in plain text only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question
|
||||
- Prefer **multiple choice** when natural options exist
|
||||
- Start with **scope and intent**, then narrow only when needed
|
||||
- Do **not** ask the user to make decisions before you have evidence
|
||||
@@ -393,7 +393,7 @@ Do **not** ask questions about whether code changes were intentional, whether th
|
||||
|
||||
#### Question Style
|
||||
|
||||
Always present choices using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in plain text only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
Always present choices using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in plain text only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
Question rules:
|
||||
|
||||
@@ -673,6 +673,6 @@ After the refresh report is generated, check whether the project's instruction f
|
||||
|
||||
`docs/solutions/` — documented solutions to past problems (bugs, best practices, workflow patterns), organized by category with YAML frontmatter (`module`, `tags`, `problem_type`). Relevant when implementing or debugging in documented areas.
|
||||
```
|
||||
c. In interactive mode, explain to the user why this matters — agents working in this repo (including fresh sessions, other tools, or collaborators without the plugin) won't know to check `docs/solutions/` unless the instruction file surfaces it. Show the proposed change and where it would go, then use the platform's blocking question tool to get consent before making the edit: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to presenting the proposal in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question. In autofix mode, include it as a "Discoverability recommendation" line in the report — do not attempt to edit instruction files (autofix scope is doc maintenance, not project config).
|
||||
c. In interactive mode, explain to the user why this matters — agents working in this repo (including fresh sessions, other tools, or collaborators without the plugin) won't know to check `docs/solutions/` unless the instruction file surfaces it. Show the proposed change and where it would go, then use the platform's blocking question tool to get consent before making the edit: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting the proposal in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question. In autofix mode, include it as a "Discoverability recommendation" line in the report — do not attempt to edit instruction files (autofix scope is doc maintenance, not project config).
|
||||
|
||||
5. **Amend or create a follow-up commit when the check produces edits.** If step 4 resulted in an edit to an instruction file and Phase 5 already committed the refresh changes, stage the newly edited file and either amend the existing commit (if still on the same branch and no push has occurred) or create a small follow-up commit (e.g., `docs: add docs/solutions/ discoverability to AGENTS.md`). If Phase 5 already pushed the branch to a remote (e.g., the branch+PR path), push the follow-up commit as well so the open PR includes the discoverability change. This keeps the working tree clean and the remote in sync at the end of the run. If the user chose "Don't commit" in Phase 5, leave the instruction-file edit unstaged alongside the other uncommitted refresh changes — no separate commit logic needed.
|
||||
|
||||
@@ -32,7 +32,7 @@ When spawning subagents, pass the relevant file contents into the task prompt so
|
||||
|
||||
## Execution Strategy
|
||||
|
||||
Present the user with two options before proceeding, using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
Present the user with two options before proceeding, using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
```
|
||||
1. Full (recommended) — the complete compound workflow. Researches,
|
||||
@@ -304,7 +304,7 @@ After the learning is written and the refresh decision is made, check whether th
|
||||
|
||||
`docs/solutions/` — documented solutions to past problems (bugs, best practices, workflow patterns), organized by category with YAML frontmatter (`module`, `tags`, `problem_type`). Relevant when implementing or debugging in documented areas.
|
||||
```
|
||||
c. In full mode, explain to the user why this matters — agents working in this repo (including fresh sessions, other tools, or collaborators without the plugin) won't know to check `docs/solutions/` unless the instruction file surfaces it. Show the proposed change and where it would go, then use the platform's blocking question tool to get consent before making the edit: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to presenting the proposal in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question. In lightweight mode, output a one-liner note and move on
|
||||
c. In full mode, explain to the user why this matters — agents working in this repo (including fresh sessions, other tools, or collaborators without the plugin) won't know to check `docs/solutions/` unless the instruction file surfaces it. Show the proposed change and where it would go, then use the platform's blocking question tool to get consent before making the edit: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting the proposal in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question. In lightweight mode, output a one-liner note and move on
|
||||
|
||||
### Phase 3: Optional Enhancement
|
||||
|
||||
@@ -460,7 +460,7 @@ What's next?
|
||||
5. Other
|
||||
```
|
||||
|
||||
**After displaying the success output, present the "What's next?" options using the platform's blocking question tool:** `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question. Do not continue the workflow or end the turn without the user's selection.
|
||||
**After displaying the success output, present the "What's next?" options using the platform's blocking question tool:** `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question. Do not continue the workflow or end the turn without the user's selection.
|
||||
|
||||
**Alternate output (when updating an existing doc due to high overlap):**
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ Once the root cause is confirmed, present:
|
||||
|
||||
Then offer next steps.
|
||||
|
||||
Use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). In Claude Code, call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded — a pending schema load is not a reason to fall back. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes). Never silently skip the question.
|
||||
Use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)). In Claude Code, call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded — a pending schema load is not a reason to fall back. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes). Never silently skip the question.
|
||||
|
||||
Options to offer:
|
||||
|
||||
@@ -203,7 +203,7 @@ How was this introduced? What allowed it to survive? If a systemic gap was found
|
||||
|
||||
**If Phase 3 was skipped** (user chose "Diagnosis only" in Phase 2), stop after the summary — the user already told you they were taking it from here. Do not prompt.
|
||||
|
||||
**If Phase 3 ran**, immediately after the summary prompt the user for the next action via the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). In Claude Code, call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded — a pending schema load is not a reason to fall back. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes). Never end the phase without collecting a response — do not stop at "ready when you are" or any other passive phrasing that leaves the user hanging.
|
||||
**If Phase 3 ran**, immediately after the summary prompt the user for the next action via the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)). In Claude Code, call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded — a pending schema load is not a reason to fall back. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes). Never end the phase without collecting a response — do not stop at "ready when you are" or any other passive phrasing that leaves the user hanging.
|
||||
|
||||
Options (include only those that apply):
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ Before capturing anything, verify the feature works by actually using it:
|
||||
- **Library**: Run example code using the new/changed API
|
||||
- **Bug fix**: Reproduce the original bug scenario and confirm it's fixed
|
||||
|
||||
Use the workspace where the feature was built. Do not reinstall from scratch. If setup requires credentials or services, use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to asking in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
Use the workspace where the feature was built. Do not reinstall from scratch. If setup requires credentials or services, use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to asking in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
## Step 2: Detect Project Type
|
||||
|
||||
@@ -114,7 +114,7 @@ python3 scripts/capture-demo.py recommend --project-type [TYPE] --change-type [m
|
||||
|
||||
This outputs JSON with `recommended` (the best tier), `available` (list of tiers whose tools are present), and `reasoning`.
|
||||
|
||||
Present the available tiers to the user via the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question. Mark the recommended tier. Always include "No evidence needed" as a final option.
|
||||
Present the available tiers to the user via the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question. Mark the recommended tier. Always include "No evidence needed" as a final option.
|
||||
|
||||
**Question:** "How should evidence be captured for this change?"
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ For multiple files (static screenshots tier), upload each file separately.
|
||||
|
||||
## Step 2: Approval Gate
|
||||
|
||||
Present the preview URL to the user for approval. Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
Present the preview URL to the user for approval. Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
**Question:** "Evidence preview (1h link): [PREVIEW_URL]"
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Review requirements or plan documents through multi-persona analysis. Dispatches
|
||||
|
||||
## Interactive mode rules
|
||||
|
||||
- **Pre-load the platform question tool before any question fires.** In Claude Code, `AskUserQuestion` is a deferred tool — its schema is not available at session start. At the start of Interactive-mode work (before the routing question, per-finding walk-through questions, bulk-preview Proceed/Cancel, and Phase 5 terminal question), call `ToolSearch` with query `select:AskUserQuestion` to load the schema. Load it once, eagerly, at the top of the Interactive flow — do not wait for the first question site. On Codex and Gemini this preload is not required.
|
||||
- **Pre-load the platform question tool before any question fires.** In Claude Code, `AskUserQuestion` is a deferred tool — its schema is not available at session start. At the start of Interactive-mode work (before the routing question, per-finding walk-through questions, bulk-preview Proceed/Cancel, and Phase 5 terminal question), call `ToolSearch` with query `select:AskUserQuestion` to load the schema. Load it once, eagerly, at the top of the Interactive flow — do not wait for the first question site. On Codex, Gemini, and Pi this preload is not required.
|
||||
- **The numbered-list fallback applies only when the harness genuinely lacks a blocking question tool** — `ToolSearch` returns no match, the tool call explicitly fails, or the runtime mode does not expose it (e.g., Codex edit modes where `request_user_input` is unavailable). A pending schema load is not a fallback trigger; call `ToolSearch` first per the pre-load rule. In genuine-fallback cases, present options as a numbered list and wait for the user's reply — never silently skip the question. Rendering a question as narrative text because the tool feels inconvenient, because the model is in report-formatting mode, or because the instruction was buried in a long skill is a bug. A question that calls for a user decision must either fire the tool or fall back loudly.
|
||||
|
||||
## Phase 0: Detect Mode
|
||||
@@ -22,7 +22,7 @@ If `mode:headless` is present, set **headless mode** for the rest of the workflo
|
||||
**Headless mode** changes the interaction model, not the classification boundaries. ce-doc-review still applies the same judgment about which tier each finding belongs in. The only difference is how non-safe_auto findings are delivered:
|
||||
|
||||
- `safe_auto` fixes are applied silently (same as interactive)
|
||||
- `gated_auto`, `manual`, and FYI findings are returned as structured text for the caller to handle — no AskUserQuestion prompts, no interactive routing
|
||||
- `gated_auto`, `manual`, and FYI findings are returned as structured text for the caller to handle — no blocking-question prompts, no interactive routing
|
||||
- Phase 5 returns immediately with "Review complete" (no routing question, no terminal question)
|
||||
|
||||
The caller receives findings with their original classifications intact and decides what to do with them.
|
||||
@@ -121,7 +121,7 @@ Add activated conditional personas:
|
||||
|
||||
### Dispatch
|
||||
|
||||
Dispatch all agents in **parallel** using the platform's task/agent tool (e.g., Agent tool in Claude Code, spawn in Codex). Omit the `mode` parameter so the user's configured permission settings apply. Each agent receives the prompt built from the subagent template included below with these variables filled:
|
||||
Dispatch all agents in **parallel** using the platform's subagent primitive (e.g., `Agent` in Claude Code, `spawn_agent` in Codex, `subagent` in Pi via the `pi-subagents` extension). Omit the `mode` parameter so the user's configured permission settings apply. Each agent receives the prompt built from the subagent template included below with these variables filled:
|
||||
|
||||
| Variable | Value |
|
||||
|----------|-------|
|
||||
|
||||
@@ -80,7 +80,7 @@ When no `why_it_matters` is available for a finding (rare — only if persona ou
|
||||
|
||||
## Question and options
|
||||
|
||||
After the preview body is rendered, ask the user using the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). In Claude Code, the tool should already be loaded from the Interactive-mode pre-load step — if it isn't, call `ToolSearch` with query `select:AskUserQuestion` now. The text fallback below applies only when the harness genuinely lacks a blocking tool — `ToolSearch` returns no match, the tool call explicitly fails, or the runtime mode does not expose it (e.g., Codex edit modes without `request_user_input`). A pending schema load is not a fallback trigger. Never silently skip the question.
|
||||
After the preview body is rendered, ask the user using the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)). In Claude Code, the tool should already be loaded from the Interactive-mode pre-load step — if it isn't, call `ToolSearch` with query `select:AskUserQuestion` now. The text fallback below applies only when the harness genuinely lacks a blocking tool — `ToolSearch` returns no match, the tool call explicitly fails, or the runtime mode does not expose it (e.g., Codex edit modes without `request_user_input`). A pending schema load is not a fallback trigger. Never silently skip the question.
|
||||
|
||||
Stem (adapted to the path):
|
||||
|
||||
|
||||
@@ -357,7 +357,7 @@ These are pipeline artifacts and must not be flagged for removal.
|
||||
|
||||
**Headless mode:** Return "Review complete" immediately. Do not ask questions. The caller receives the text envelope from Phase 4 and handles any remaining findings.
|
||||
|
||||
**Interactive mode:** fire the terminal question using the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). In Claude Code the tool should already be loaded from the Interactive-mode pre-load step in `SKILL.md` — if it isn't, call `ToolSearch` with `select:AskUserQuestion` now. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question. This question is distinct from the mid-flow routing question (`references/walkthrough.md`) — the routing question chooses *how* to engage with findings, this one chooses *what to do next* once engagement is complete. Do not merge them.
|
||||
**Interactive mode:** fire the terminal question using the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)). In Claude Code the tool should already be loaded from the Interactive-mode pre-load step in `SKILL.md` — if it isn't, call `ToolSearch` with `select:AskUserQuestion` now. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question. This question is distinct from the mid-flow routing question (`references/walkthrough.md`) — the routing question chooses *how* to engage with findings, this one chooses *what to do next* once engagement is complete. Do not merge them.
|
||||
|
||||
**Stem:** `Apply decisions and what next?`
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Interactive mode only.
|
||||
|
||||
After `safe_auto` fixes apply and synthesis produces the remaining finding set, the orchestrator asks a four-option routing question before any walk-through or bulk action runs.
|
||||
|
||||
Use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). In Claude Code, the tool should already be loaded from the Interactive-mode pre-load step in `SKILL.md` — if it isn't, call `ToolSearch` with query `select:AskUserQuestion` now. Fall back to presenting the options as a numbered list only when the harness genuinely lacks a blocking tool — `ToolSearch` returns no match, the tool call explicitly fails, or the runtime mode does not expose it (e.g., Codex edit modes without `request_user_input`). A pending schema load is not a fallback trigger. Never silently skip the question. Rendering the routing question as narrative text without the numbered-list fallback is a bug.
|
||||
Use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)). In Claude Code, the tool should already be loaded from the Interactive-mode pre-load step in `SKILL.md` — if it isn't, call `ToolSearch` with query `select:AskUserQuestion` now. Fall back to presenting the options as a numbered list only when the harness genuinely lacks a blocking tool — `ToolSearch` returns no match, the tool call explicitly fails, or the runtime mode does not expose it (e.g., Codex edit modes without `request_user_input`). A pending schema load is not a fallback trigger. Never silently skip the question. Rendering the routing question as narrative text without the numbered-list fallback is a bug.
|
||||
|
||||
**Stem:** `What should the agent do with the remaining N findings?`
|
||||
|
||||
@@ -53,7 +53,7 @@ Each finding's recommended action has already been normalized by synthesis step
|
||||
**Cascading root decisions.** When the user picks Skip or Defer on a finding with `dependents`:
|
||||
|
||||
1. Announce the cascade in the terminal before firing the next question: "Skipping/Deferring this root will auto-resolve N dependent finding(s): {titles}. Continue?"
|
||||
2. Use the platform's blocking question tool with two options: `Cascade — apply same action to all dependents` (recommended) and `Decide each dependent individually`. Labels must be self-contained per the AskUserQuestion rules.
|
||||
2. Use the platform's blocking question tool with two options: `Cascade — apply same action to all dependents` (recommended) and `Decide each dependent individually`. Labels must be self-contained per the blocking-question tool design rules.
|
||||
3. On Cascade: apply the root's action to every dependent and skip those findings' walk-through entries. Persistence follows the per-action routing rules from "Per-finding routing" below — the canonical home for every cascaded decision is the in-memory decision list (annotated with `cascaded from {root_title}` and the cascaded action), plus any action-specific side effect:
|
||||
- Cascaded `Apply` — add the dependent id to the Apply set and record in the decision list.
|
||||
- Cascaded `Defer` — invoke the open-questions append flow for the dependent and record the append outcome in the decision list. If the append fails, fall back to the per-finding failure path (Retry / Record only / Convert to Skip) for that dependent before advancing the cascade.
|
||||
|
||||
@@ -52,7 +52,7 @@ Based on detected signals, choose a mode:
|
||||
|
||||
### Asking the User
|
||||
|
||||
When context is ambiguous, use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip. If the user declines to pick, assume "partial" mode and proceed conservatively.
|
||||
When context is ambiguous, use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip. If the user declines to pick, assume "partial" mode and proceed conservatively.
|
||||
|
||||
Example question: "I found [detected signals]. Should I follow your existing design patterns or create something distinctive?"
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ This workflow produces a ranked ideation artifact in `docs/ideation/`. It does *
|
||||
|
||||
## Interaction Method
|
||||
|
||||
Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
Ask one question at a time. Prefer concise single-select choices when natural options exist.
|
||||
|
||||
@@ -96,7 +96,7 @@ State the inferred approach in one sentence at the top, using plain language the
|
||||
|
||||
The correction hints must also be plain language ("actually this is outside the repo", "actually this is about this repo"), not internal labels ("actually elsewhere-software").
|
||||
|
||||
**Active confirmation on ambiguity (V16).** When classifier confidence is low — single-keyword or short prompts mapping cleanly to either mode (`/ce-ideate ideas`, `/ce-ideate ideas for the docs`), conflicting CWD/prompt signals, or topic mentioning both repo-internal and external surfaces — ask one confirmation question via the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini) **before dispatching Phase 1 grounding**. For clear cases the one-sentence inferred-mode statement is sufficient; do not ask.
|
||||
**Active confirmation on ambiguity (V16).** When classifier confidence is low — single-keyword or short prompts mapping cleanly to either mode (`/ce-ideate ideas`, `/ce-ideate ideas for the docs`), conflicting CWD/prompt signals, or topic mentioning both repo-internal and external surfaces — ask one confirmation question via the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)) **before dispatching Phase 1 grounding**. For clear cases the one-sentence inferred-mode statement is sufficient; do not ask.
|
||||
|
||||
Sample wording (refine to fit the prompt at hand; follow the Interactive Question Tool Design rules in the plugin AGENTS.md — self-contained labels, max 4, third person, front-loaded distinguishing word, no leaked internal mode names):
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ When the proof skill returns control:
|
||||
|
||||
## Phase 6: Refine or Hand Off
|
||||
|
||||
Ask what should happen next using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
Ask what should happen next using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
**Question:** "What should the agent do next?"
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ Present survivors before any persistence. For each: title, description, rational
|
||||
|
||||
Persistence is opt-in. The terminal review loop is a complete ideation cycle. Refinement happens in conversation with no file or network cost. Persistence triggers only when the user explicitly chooses to save, share, or hand off.
|
||||
|
||||
Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question. Offer four choices:
|
||||
Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question. Offer four choices:
|
||||
|
||||
- **Refine the ideation in conversation (or stop here — no save)** — add ideas, re-evaluate, or deepen analysis without writing anything. Ending the conversation at any point after this pick is a valid no-save exit.
|
||||
- **Open and iterate in Proof** — invoke the Proof HITL review path per the §6.2 contract in `references/post-ideation-workflow.md`: upload the survivors to Proof (rendered to a temp file since no local file is written in non-software elsewhere mode), iterate via comments, and exit cleanly with the Proof URL as the canonical record on successful return. Proof iteration is typically the terminal act in this mode, so the flow does not force another menu choice afterward. Only an `aborted` status returns to this menu. On persistent Proof failure, apply the §6.5 Proof Failure Ladder from `references/post-ideation-workflow.md` so the iteration attempt is not stranded without recovery.
|
||||
|
||||
@@ -10,7 +10,7 @@ Run metric-driven iterative optimization. Define a goal, build measurement scaff
|
||||
|
||||
## Interaction Method
|
||||
|
||||
Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
## Input
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ This workflow produces a durable implementation plan. It does **not** implement
|
||||
|
||||
## Interaction Method
|
||||
|
||||
When asking the user a question, use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
When asking the user a question, use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
Ask one question at a time. Prefer a concise single-select choice when natural options exist.
|
||||
|
||||
@@ -832,7 +832,7 @@ When deepening is warranted, read `references/deepening-workflow.md` for confide
|
||||
|
||||
**Load `references/plan-handoff.md` now.** It contains the full instructions for 5.3.8 (document review), 5.3.9 (final checks and cleanup), and 5.4 (post-generation handoff, including the Proof HITL flow, post-HITL re-review, and Issue Creation branching). Document review is mandatory — do not skip it even if the confidence check already ran.
|
||||
|
||||
After document review and final checks, present this menu using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
After document review and final checks, present this menu using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
**Question:** "Plan ready at `docs/plans/YYYY-MM-DD-NNN-<type>-<name>-plan.md`. What would you like to do next?"
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ If artifact-backed mode was used:
|
||||
|
||||
**Pipeline mode:** If invoked from an automated workflow such as LFG, SLFG, or any `disable-model-invocation` context, skip the interactive menu below and return control to the caller immediately. The plan file has already been written, the confidence check has already run, and ce-doc-review has already run — the caller (e.g., lfg, slfg) determines the next step.
|
||||
|
||||
After document-review completes, present the options using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
After document-review completes, present the options using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
**Question:** "Plan ready at `docs/plans/YYYY-MM-DD-NNN-<type>-<name>-plan.md`. What would you like to do next?"
|
||||
|
||||
@@ -82,7 +82,7 @@ When the user selects "Create Issue", detect their project tracker:
|
||||
linear issue create --title "<title>" --description "$(cat <plan_path>)"
|
||||
```
|
||||
|
||||
4. If no tracker is configured, ask the user which tracker they use with the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to asking in chat only when no blocking tool exists or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip. Options: `GitHub`, `Linear`, `Skip`. Then:
|
||||
4. If no tracker is configured, ask the user which tracker they use with the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to asking in chat only when no blocking tool exists or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip. Options: `GitHub`, `Linear`, `Skip`. Then:
|
||||
- Proceed with the chosen tracker's command above
|
||||
- Offer to persist the choice by adding `project_tracker: <value>` to `AGENTS.md`, where `<value>` is the lowercase tracker key (`github` or `linear`) — not the display label — so future runs match the detector in step 1 and skip this prompt
|
||||
- If `Skip`, return to the options without creating an issue
|
||||
|
||||
@@ -44,7 +44,7 @@ Example for "plan a date night in Seattle this Saturday":
|
||||
|
||||
## Step 1b: Focused Q&A
|
||||
|
||||
Ask up to 3 questions targeting the unknowns that would most change the plan. Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in chat only when no blocking tool exists or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
Ask up to 3 questions targeting the unknowns that would most change the plan. Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in chat only when no blocking tool exists or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
**How to ask well:**
|
||||
- Offer informed options, not open-ended blanks. Instead of "When are you going?", try "Mid-week visits have 30-40% shorter lines — are you flexible on timing?" The question should give the user a frame of reference, not just extract information.
|
||||
@@ -93,7 +93,7 @@ Example: A date night plan should present 2-3 restaurant options, 2-3 activity o
|
||||
|
||||
## Step 3: Save or Share
|
||||
|
||||
After structuring the plan, ask the user how they want to receive it using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in chat only when no blocking tool exists or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
After structuring the plan, ask the user how they want to receive it using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in chat only when no blocking tool exists or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
**Question:** "Plan ready. How would you like to receive it?"
|
||||
|
||||
|
||||
@@ -397,4 +397,4 @@ If Step 1 exited gracefully (closed/merged PR, invalid range, empty commit list)
|
||||
|
||||
This skill does not ask questions directly. If the diff is ambiguous about something the caller should decide (e.g., focus conflicts with the actual changes, or evidence is technically capturable but the caller did not pre-stage it), surface the ambiguity in the returned output or a short note to the caller — do not invoke a platform question tool.
|
||||
|
||||
Callers that need to ask the user are responsible for using their own platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
Callers that need to ask the user are responsible for using their own platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
@@ -40,7 +40,7 @@ Return shape (used by upstream callers to resume their handoff; also shown to th
|
||||
Doc ready for review: <tokenUrl>
|
||||
```
|
||||
|
||||
5. Ask the user with the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
5. Ask the user with the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
**Question:** "Highlight text in Proof to leave a comment. The agent will read each one, reply in-thread or apply the fix, then sync changes back to your local file. What's next?"
|
||||
|
||||
@@ -182,7 +182,7 @@ Phrase them in whatever voice matches the situation rather than matching a templ
|
||||
|
||||
## Phase 4: Next-Signal Prompt
|
||||
|
||||
Ask the user with the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
Ask the user with the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
**Question:** "Proof review pass done. What's next?"
|
||||
|
||||
@@ -220,7 +220,7 @@ Runs when the user selects **Proceed**. Before prompting anything, check whether
|
||||
|
||||
**If different** — continue to step 3.
|
||||
|
||||
3. Ask with the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
3. Ask with the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
**Question:** "Sync the reviewed doc back to `<localPath>`? Proof has your review changes; local still has the pre-review copy."
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Report bugs encountered while using the compound-engineering plugin. This skill
|
||||
|
||||
## Step 1: Gather Bug Information
|
||||
|
||||
Ask the user the following questions using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question:
|
||||
Ask the user the following questions using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question:
|
||||
|
||||
**Question 1: Bug Category**
|
||||
- What type of issue are you experiencing?
|
||||
|
||||
@@ -344,7 +344,7 @@ Still pending from a previous run (count):
|
||||
|
||||
If a blocking question tool is available, use it to ask about all pending decisions (both new `needs-human` and previous-run pending) together. If there are only pending decisions and no new work was done, the summary is just the pending items.
|
||||
|
||||
Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Use it to present the decisions and wait for the user's response. After they decide, process the remaining items: fix the code, compose the reply, post it, and resolve the thread.
|
||||
Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Use it to present the decisions and wait for the user's response. After they decide, process the remaining items: fix the code, compose the reply, post it, and resolve the thread.
|
||||
|
||||
Fall back to presenting the decisions in the summary output and waiting in conversation only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip. If the user doesn't respond, the items remain open on the PR for later handling.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ If the lines above resolved to plain values (a folder name like `my-repo` and a
|
||||
|
||||
## Execution
|
||||
|
||||
If no argument is provided, ask what the user wants to know about their session history. Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to asking in plain text only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
If no argument is provided, ask what the user wants to know about their session history. Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to asking in plain text only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
Dispatch `ce-session-historian` with the user's question as the task prompt. Omit the `mode` parameter so the user's configured permission settings apply. Include in the dispatch prompt:
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ disable-model-invocation: true
|
||||
|
||||
## Interaction Method
|
||||
|
||||
Ask the user each question below using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to presenting each question as a numbered list in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip or auto-configure. For multiSelect questions, accept comma-separated numbers (e.g. `1, 3`).
|
||||
Ask the user each question below using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting each question as a numbered list in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip or auto-configure. For multiSelect questions, accept comma-separated numbers (e.g. `1, 3`).
|
||||
|
||||
Interactive setup for compound-engineering — diagnoses environment health, cleans obsolete repo-local CE config, and helps configure required tools. Review agent selection is handled automatically by `ce-code-review`; project-specific review guidance belongs in `CLAUDE.md` or `AGENTS.md`.
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ The input can be a keyword, a natural language question, or include Slack search
|
||||
|
||||
## Execution
|
||||
|
||||
If no argument is provided, ask what topic to research. Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to asking in plain text only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
If no argument is provided, ask what topic to research. Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to asking in plain text only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.
|
||||
|
||||
Dispatch `ce-slack-researcher` with the user's topic as the task prompt. Omit the `mode` parameter so the user's configured permission settings apply.
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ If not installed, inform the user: "`agent-browser` is not installed. Run `/ce-s
|
||||
|
||||
### 2. Ask Browser Mode
|
||||
|
||||
Ask the user whether to run headed or headless using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question:
|
||||
Ask the user whether to run headed or headless using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question:
|
||||
|
||||
```
|
||||
Do you want to watch the browser tests run?
|
||||
|
||||
@@ -109,7 +109,7 @@ Pause for human input when testing touches flows that require device interaction
|
||||
| Location | "Allow location access and verify map updates" |
|
||||
| SwiftUI Text links | "Please tap on [element description] manually — automated taps cannot trigger inline text links" |
|
||||
|
||||
Ask the user using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini. Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question:
|
||||
Ask the user using the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to numbered options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question:
|
||||
|
||||
```
|
||||
Human Verification Needed
|
||||
|
||||
@@ -66,7 +66,7 @@ If it shows an unresolved command string, run `command -v codex` using a shell t
|
||||
|
||||
If `consent_granted` is not true (from config `work_delegate_consent`):
|
||||
|
||||
Present a one-time consent warning using the platform's blocking question tool (AskUserQuestion in Claude Code). The consent warning explains:
|
||||
Present a one-time consent warning using the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)). The consent warning explains:
|
||||
- Delegation sends implementation units to `codex exec` as a structured prompt
|
||||
- **yolo mode** (`--yolo`): Full system access including network. Required for verification steps that run tests or install dependencies. **Recommended.**
|
||||
- **full-auto mode** (`--full-auto`): Workspace-write sandbox, no network access.
|
||||
|
||||
@@ -32,7 +32,7 @@ This file contains the shipping workflow (Phase 3-4). Load it only when all Phas
|
||||
|
||||
After Tier 2 code review completes, inspect the Residual Actionable Work summary it returned (or read the run artifact directly if the summary was not emitted). If one or more residual `downstream-resolver` findings remain, do not proceed to Final Validation until the user decides how to handle them.
|
||||
|
||||
Ask the user using the platform's blocking question tool (`AskUserQuestion` in Claude Code with `ToolSearch select:AskUserQuestion` pre-loaded if needed, `request_user_input` in Codex, `ask_user` in Gemini). Fall back to numbered options in chat only when the harness genuinely lacks a blocking tool. Never silently skip the gate.
|
||||
Ask the user using the platform's blocking question tool (`AskUserQuestion` in Claude Code with `ToolSearch select:AskUserQuestion` pre-loaded if needed, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)). Fall back to numbered options in chat only when the harness genuinely lacks a blocking tool. Never silently skip the gate.
|
||||
|
||||
Stem: `Code review found N residual finding(s) the skill did not auto-fix. How should the agent proceed?`
|
||||
|
||||
|
||||
@@ -144,6 +144,6 @@ When uncertain, prefer "drop with explicit user-facing notice" over "pass throug
|
||||
|
||||
## Cross-platform notes
|
||||
|
||||
The question-tool name varies by platform. In Interactive mode, use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). In Claude Code the tool should already be loaded from the Interactive-mode pre-load step — if it isn't, call `ToolSearch` with query `select:AskUserQuestion` now. Fall back to numbered options in chat only when the harness genuinely lacks a blocking tool — `ToolSearch` returns no match, the tool call explicitly fails, or the runtime mode does not expose it (e.g., Codex edit modes without `request_user_input`). A pending schema load is not a fallback trigger. Never silently skip the question.
|
||||
The question-tool name varies by platform. In Interactive mode, use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)). In Claude Code the tool should already be loaded from the Interactive-mode pre-load step — if it isn't, call `ToolSearch` with query `select:AskUserQuestion` now. Fall back to numbered options in chat only when the harness genuinely lacks a blocking tool — `ToolSearch` returns no match, the tool call explicitly fails, or the runtime mode does not expose it (e.g., Codex edit modes without `request_user_input`). A pending schema load is not a fallback trigger. Never silently skip the question.
|
||||
|
||||
Non-interactive mode is platform-agnostic: it never prompts, so the platform's question tool is not relevant.
|
||||
|
||||
@@ -32,7 +32,7 @@ This file contains the shipping workflow (Phase 3-4). Load it only when all Phas
|
||||
|
||||
After Tier 2 code review completes, inspect the Residual Actionable Work summary it returned (or read the run artifact directly if the summary was not emitted). If one or more residual `downstream-resolver` findings remain, do not proceed to Final Validation until the user decides how to handle them.
|
||||
|
||||
Ask the user using the platform's blocking question tool (`AskUserQuestion` in Claude Code with `ToolSearch select:AskUserQuestion` pre-loaded if needed, `request_user_input` in Codex, `ask_user` in Gemini). Fall back to numbered options in chat only when the harness genuinely lacks a blocking tool. Never silently skip the gate.
|
||||
Ask the user using the platform's blocking question tool (`AskUserQuestion` in Claude Code with `ToolSearch select:AskUserQuestion` pre-loaded if needed, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)). Fall back to numbered options in chat only when the harness genuinely lacks a blocking tool. Never silently skip the gate.
|
||||
|
||||
Stem: `Code review found N residual finding(s) the skill did not auto-fix. How should the agent proceed?`
|
||||
|
||||
|
||||
@@ -144,6 +144,6 @@ When uncertain, prefer "drop with explicit user-facing notice" over "pass throug
|
||||
|
||||
## Cross-platform notes
|
||||
|
||||
The question-tool name varies by platform. In Interactive mode, use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). In Claude Code the tool should already be loaded from the Interactive-mode pre-load step — if it isn't, call `ToolSearch` with query `select:AskUserQuestion` now. Fall back to numbered options in chat only when the harness genuinely lacks a blocking tool — `ToolSearch` returns no match, the tool call explicitly fails, or the runtime mode does not expose it (e.g., Codex edit modes without `request_user_input`). A pending schema load is not a fallback trigger. Never silently skip the question.
|
||||
The question-tool name varies by platform. In Interactive mode, use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)). In Claude Code the tool should already be loaded from the Interactive-mode pre-load step — if it isn't, call `ToolSearch` with query `select:AskUserQuestion` now. Fall back to numbered options in chat only when the harness genuinely lacks a blocking tool — `ToolSearch` returns no match, the tool call explicitly fails, or the runtime mode does not expose it (e.g., Codex edit modes without `request_user_input`). A pending schema load is not a fallback trigger. Never silently skip the question.
|
||||
|
||||
Non-interactive mode is platform-agnostic: it never prompts, so the platform's question tool is not relevant.
|
||||
|
||||
@@ -144,6 +144,6 @@ When uncertain, prefer "drop with explicit user-facing notice" over "pass throug
|
||||
|
||||
## Cross-platform notes
|
||||
|
||||
The question-tool name varies by platform. In Interactive mode, use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini). In Claude Code the tool should already be loaded from the Interactive-mode pre-load step — if it isn't, call `ToolSearch` with query `select:AskUserQuestion` now. Fall back to numbered options in chat only when the harness genuinely lacks a blocking tool — `ToolSearch` returns no match, the tool call explicitly fails, or the runtime mode does not expose it (e.g., Codex edit modes without `request_user_input`). A pending schema load is not a fallback trigger. Never silently skip the question.
|
||||
The question-tool name varies by platform. In Interactive mode, use the platform's blocking question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension)). In Claude Code the tool should already be loaded from the Interactive-mode pre-load step — if it isn't, call `ToolSearch` with query `select:AskUserQuestion` now. Fall back to numbered options in chat only when the harness genuinely lacks a blocking tool — `ToolSearch` returns no match, the tool call explicitly fails, or the runtime mode does not expose it (e.g., Codex edit modes without `request_user_input`). A pending schema load is not a fallback trigger. Never silently skip the question.
|
||||
|
||||
Non-interactive mode is platform-agnostic: it never prompts, so the platform's question tool is not relevant.
|
||||
|
||||
@@ -2,12 +2,11 @@ import { formatFrontmatter } from "../utils/frontmatter"
|
||||
import { type ClaudeAgent, type ClaudeCommand, type ClaudeMcpServer, type ClaudePlugin, filterSkillsByPlatform } from "../types/claude"
|
||||
import type {
|
||||
PiBundle,
|
||||
PiGeneratedSkill,
|
||||
PiGeneratedAgent,
|
||||
PiMcporterConfig,
|
||||
PiMcporterServer,
|
||||
} from "../types/pi"
|
||||
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
|
||||
import { PI_COMPAT_EXTENSION_SOURCE } from "../templates/pi/compat-extension"
|
||||
|
||||
export type ClaudeToPiOptions = ClaudeToOpenCodeOptions
|
||||
|
||||
@@ -19,20 +18,17 @@ export function convertClaudeToPi(
|
||||
): PiBundle {
|
||||
const platformSkills = filterSkillsByPlatform(plugin.skills, "pi")
|
||||
const promptNames = new Set<string>()
|
||||
const usedSkillNames = new Set<string>(platformSkills.map((skill) => normalizeName(skill.name)))
|
||||
// Pi agents and skills live in separate directories (.pi/agents/<name>.md vs
|
||||
// .pi/skills/<name>/SKILL.md), so their names don't need to be deduplicated
|
||||
// against each other — nicobailon/pi-subagents resolves agents by filename
|
||||
// match and ignores skill dirs.
|
||||
const usedAgentNames = new Set<string>()
|
||||
|
||||
const prompts = plugin.commands
|
||||
.filter((command) => !command.disableModelInvocation)
|
||||
.map((command) => convertPrompt(command, promptNames))
|
||||
|
||||
const generatedSkills = plugin.agents.map((agent) => convertAgent(agent, usedSkillNames))
|
||||
|
||||
const extensions = [
|
||||
{
|
||||
name: "compound-engineering-compat.ts",
|
||||
content: PI_COMPAT_EXTENSION_SOURCE,
|
||||
},
|
||||
]
|
||||
const agents = plugin.agents.map((agent) => convertAgent(agent, usedAgentNames))
|
||||
|
||||
return {
|
||||
pluginName: plugin.manifest.name,
|
||||
@@ -41,12 +37,38 @@ export function convertClaudeToPi(
|
||||
name: skill.name,
|
||||
sourceDir: skill.sourceDir,
|
||||
})),
|
||||
generatedSkills,
|
||||
extensions,
|
||||
generatedSkills: [],
|
||||
agents,
|
||||
extensions: [],
|
||||
mcporterConfig: plugin.mcpServers ? convertMcpToMcporter(plugin.mcpServers) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function convertMcpToMcporter(servers: Record<string, ClaudeMcpServer>): PiMcporterConfig {
|
||||
const mcpServers: Record<string, PiMcporterServer> = {}
|
||||
|
||||
for (const [name, server] of Object.entries(servers)) {
|
||||
if (server.command) {
|
||||
mcpServers[name] = {
|
||||
command: server.command,
|
||||
args: server.args,
|
||||
env: server.env,
|
||||
headers: server.headers,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (server.url) {
|
||||
mcpServers[name] = {
|
||||
baseUrl: server.url,
|
||||
headers: server.headers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { mcpServers }
|
||||
}
|
||||
|
||||
function convertPrompt(command: ClaudeCommand, usedNames: Set<string>) {
|
||||
const name = uniqueName(normalizeName(command.name), usedNames)
|
||||
const frontmatter: Record<string, unknown> = {
|
||||
@@ -54,8 +76,7 @@ function convertPrompt(command: ClaudeCommand, usedNames: Set<string>) {
|
||||
"argument-hint": command.argumentHint,
|
||||
}
|
||||
|
||||
let body = transformContentForPi(command.body)
|
||||
body = appendCompatibilityNoteIfNeeded(body)
|
||||
const body = transformContentForPi(command.body)
|
||||
|
||||
return {
|
||||
name,
|
||||
@@ -63,7 +84,7 @@ function convertPrompt(command: ClaudeCommand, usedNames: Set<string>) {
|
||||
}
|
||||
}
|
||||
|
||||
function convertAgent(agent: ClaudeAgent, usedNames: Set<string>): PiGeneratedSkill {
|
||||
function convertAgent(agent: ClaudeAgent, usedNames: Set<string>): PiGeneratedAgent {
|
||||
const name = uniqueName(normalizeName(agent.name), usedNames)
|
||||
const description = sanitizeDescription(
|
||||
agent.description ?? `Converted from Claude agent ${agent.name}`,
|
||||
@@ -107,8 +128,6 @@ export function transformContentForPi(body: string): string {
|
||||
: `${prefix}Run subagent with agent=\"${skillName}\".`
|
||||
})
|
||||
|
||||
// Claude-specific tool references
|
||||
result = result.replace(/\bAskUserQuestion\b/g, "ask_user_question")
|
||||
// Claude Code task-tracking primitives: current Task* API (TaskCreate/TaskUpdate/TaskList/TaskGet/TaskStop/TaskOutput)
|
||||
// plus the deprecated legacy pair (TodoWrite/TodoRead). All map to the platform's task-tracking primitive.
|
||||
result = result.replace(
|
||||
@@ -141,46 +160,6 @@ export function transformContentForPi(body: string): string {
|
||||
return result
|
||||
}
|
||||
|
||||
function appendCompatibilityNoteIfNeeded(body: string): string {
|
||||
if (!/\bmcp\b/i.test(body)) return body
|
||||
|
||||
const note = [
|
||||
"",
|
||||
"## Pi + MCPorter note",
|
||||
"For MCP access in Pi, use MCPorter via the generated tools:",
|
||||
"- `mcporter_list` to inspect available MCP tools",
|
||||
"- `mcporter_call` to invoke a tool",
|
||||
"",
|
||||
].join("\n")
|
||||
|
||||
return body + note
|
||||
}
|
||||
|
||||
function convertMcpToMcporter(servers: Record<string, ClaudeMcpServer>): PiMcporterConfig {
|
||||
const mcpServers: Record<string, PiMcporterServer> = {}
|
||||
|
||||
for (const [name, server] of Object.entries(servers)) {
|
||||
if (server.command) {
|
||||
mcpServers[name] = {
|
||||
command: server.command,
|
||||
args: server.args,
|
||||
env: server.env,
|
||||
headers: server.headers,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (server.url) {
|
||||
mcpServers[name] = {
|
||||
baseUrl: server.url,
|
||||
headers: server.headers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { mcpServers }
|
||||
}
|
||||
|
||||
function normalizeName(value: string): string {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return "item"
|
||||
|
||||
@@ -25,12 +25,13 @@ const PI_AGENTS_BLOCK_BODY = `## Compound Engineering (Pi compatibility)
|
||||
|
||||
This block is managed by compound-plugin.
|
||||
|
||||
Compatibility notes:
|
||||
- Claude Task(agent, args) maps to the subagent extension tool
|
||||
- For parallel agent runs, batch multiple subagent calls with multi_tool_use.parallel
|
||||
- AskUserQuestion maps to the ask_user_question extension tool
|
||||
- MCP access uses MCPorter via mcporter_list and mcporter_call extension tools
|
||||
- MCPorter config path: .pi/compound-engineering/mcporter.json (project) or ~/.pi/agent/compound-engineering/mcporter.json (global)
|
||||
Pi extensions used by this plugin:
|
||||
- Required: \`pi-subagents\` (by nicobailon) provides the \`subagent\` tool used by skills that dispatch parallel agents
|
||||
- Recommended: \`pi-ask-user\` (by edlsh) provides the \`ask_user\` tool; skills fall back to numbered options in chat when it is missing
|
||||
|
||||
Install with:
|
||||
pi install npm:pi-subagents
|
||||
pi install npm:pi-ask-user
|
||||
`
|
||||
|
||||
export type PiInstallManifest = {
|
||||
@@ -39,6 +40,8 @@ export type PiInstallManifest = {
|
||||
skills: string[]
|
||||
prompts: string[]
|
||||
extensions: string[]
|
||||
// Added in v2.69+. Older manifests omit this; reads default to [].
|
||||
agents: string[]
|
||||
}
|
||||
|
||||
type PiPaths = {
|
||||
@@ -46,6 +49,7 @@ type PiPaths = {
|
||||
skillsDir: string
|
||||
promptsDir: string
|
||||
extensionsDir: string
|
||||
agentsDir: string
|
||||
mcporterConfigPath: string
|
||||
agentsPath: string
|
||||
}
|
||||
@@ -61,15 +65,18 @@ export async function writePiBundle(outputRoot: string, bundle: PiBundle): Promi
|
||||
...bundle.skillDirs.map((skill) => sanitizePathName(skill.name)),
|
||||
...bundle.generatedSkills.map((skill) => sanitizePathName(skill.name)),
|
||||
]
|
||||
const currentAgents = bundle.agents.map((agent) => `${sanitizePathName(agent.name)}.md`)
|
||||
const currentExtensions = bundle.extensions.map((extension) => extension.name)
|
||||
|
||||
await ensureDir(paths.skillsDir)
|
||||
await ensureDir(paths.promptsDir)
|
||||
await ensureDir(paths.extensionsDir)
|
||||
await ensureDir(paths.agentsDir)
|
||||
|
||||
await cleanupStaleAgents(paths.skillsDir, null)
|
||||
await cleanupRemovedPrompts(paths.promptsDir, manifest, currentPrompts)
|
||||
await cleanupRemovedSkills(paths.skillsDir, manifest, currentSkills)
|
||||
await cleanupRemovedAgents(paths.agentsDir, manifest, currentAgents)
|
||||
await cleanupRemovedExtensions(paths.extensionsDir, manifest, currentExtensions)
|
||||
|
||||
for (const prompt of bundle.prompts) {
|
||||
@@ -90,6 +97,13 @@ export async function writePiBundle(outputRoot: string, bundle: PiBundle): Promi
|
||||
await writeText(path.join(targetDir, "SKILL.md"), skill.content + "\n")
|
||||
}
|
||||
|
||||
for (const agent of bundle.agents) {
|
||||
const agentFileName = `${sanitizePathName(agent.name)}.md`
|
||||
const targetPath = path.join(paths.agentsDir, agentFileName)
|
||||
await cleanupCurrentManagedAgentFile(targetPath, manifest, agentFileName)
|
||||
await writeText(targetPath, agent.content + "\n")
|
||||
}
|
||||
|
||||
for (const extension of bundle.extensions) {
|
||||
await writeText(path.join(paths.extensionsDir, extension.name), extension.content + "\n")
|
||||
}
|
||||
@@ -111,6 +125,7 @@ export async function writePiBundle(outputRoot: string, bundle: PiBundle): Promi
|
||||
skills: currentSkills,
|
||||
prompts: currentPrompts,
|
||||
extensions: currentExtensions,
|
||||
agents: currentAgents,
|
||||
})
|
||||
await archiveLegacyInstallManifestIfOwned(paths.managedDir, pluginName)
|
||||
await cleanupKnownLegacyPiArtifacts(paths, bundle)
|
||||
@@ -131,6 +146,7 @@ function resolvePiPaths(outputRoot: string, pluginName?: string): PiPaths {
|
||||
skillsDir: path.join(outputRoot, "skills"),
|
||||
promptsDir: path.join(outputRoot, "prompts"),
|
||||
extensionsDir: path.join(outputRoot, "extensions"),
|
||||
agentsDir: path.join(outputRoot, "agents"),
|
||||
mcporterConfigPath: path.join(outputRoot, managedSegment, "mcporter.json"),
|
||||
agentsPath: path.join(outputRoot, "AGENTS.md"),
|
||||
}
|
||||
@@ -142,6 +158,7 @@ function resolvePiPaths(outputRoot: string, pluginName?: string): PiPaths {
|
||||
skillsDir: path.join(outputRoot, "skills"),
|
||||
promptsDir: path.join(outputRoot, "prompts"),
|
||||
extensionsDir: path.join(outputRoot, "extensions"),
|
||||
agentsDir: path.join(outputRoot, "agents"),
|
||||
mcporterConfigPath: path.join(outputRoot, managedSegment, "mcporter.json"),
|
||||
agentsPath: path.join(outputRoot, "AGENTS.md"),
|
||||
}
|
||||
@@ -152,6 +169,7 @@ function resolvePiPaths(outputRoot: string, pluginName?: string): PiPaths {
|
||||
skillsDir: path.join(outputRoot, ".pi", "skills"),
|
||||
promptsDir: path.join(outputRoot, ".pi", "prompts"),
|
||||
extensionsDir: path.join(outputRoot, ".pi", "extensions"),
|
||||
agentsDir: path.join(outputRoot, ".pi", "agents"),
|
||||
mcporterConfigPath: path.join(outputRoot, ".pi", managedSegment, "mcporter.json"),
|
||||
agentsPath: path.join(outputRoot, "AGENTS.md"),
|
||||
}
|
||||
@@ -259,7 +277,7 @@ async function readInstallManifest(
|
||||
Array.isArray(parsed.extensions)
|
||||
) {
|
||||
// Filter manifest entries at read time. Cleanup functions join these
|
||||
// strings into `fs.rm` paths against the Pi skills/prompts/extensions
|
||||
// strings into `fs.rm` paths against the Pi skills/prompts/extensions/agents
|
||||
// directories, so a tampered or corrupted `install-manifest.json` with
|
||||
// entries like `../../config.toml` or `/etc/passwd` would otherwise
|
||||
// delete outside the Pi managed tree. Validate each group against the
|
||||
@@ -270,12 +288,18 @@ async function readInstallManifest(
|
||||
const skillsRoot = paths?.skillsDir ?? managedDir
|
||||
const promptsRoot = paths?.promptsDir ?? managedDir
|
||||
const extensionsRoot = paths?.extensionsDir ?? managedDir
|
||||
const agentsRoot = paths?.agentsDir ?? managedDir
|
||||
// `agents` was added in v2.69+; accept missing/omitted to stay
|
||||
// backward-compatible with v2.x manifests that only tracked skills,
|
||||
// prompts, and extensions. Drop non-array values defensively.
|
||||
const rawAgents = Array.isArray(parsed.agents) ? parsed.agents : []
|
||||
return {
|
||||
version: 1,
|
||||
pluginName,
|
||||
skills: filterSafePiManifestEntries(parsed.skills, skillsRoot, manifestPath, "skills"),
|
||||
prompts: filterSafePiManifestEntries(parsed.prompts, promptsRoot, manifestPath, "prompts"),
|
||||
extensions: filterSafePiManifestEntries(parsed.extensions, extensionsRoot, manifestPath, "extensions"),
|
||||
agents: filterSafePiManifestEntries(rawAgents, agentsRoot, manifestPath, "agents"),
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -354,6 +378,20 @@ async function cleanupRemovedExtensions(
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupRemovedAgents(
|
||||
agentsDir: string,
|
||||
manifest: PiInstallManifest | null,
|
||||
currentAgents: string[],
|
||||
): Promise<void> {
|
||||
if (!manifest) return
|
||||
const current = new Set(currentAgents)
|
||||
for (const agentFile of manifest.agents) {
|
||||
if (current.has(agentFile)) continue
|
||||
if (!isSafeManagedPath(agentsDir, agentFile)) continue
|
||||
await fs.rm(path.join(agentsDir, agentFile), { force: true })
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupCurrentManagedSkillDir(
|
||||
targetDir: string,
|
||||
manifest: PiInstallManifest | null,
|
||||
@@ -363,6 +401,43 @@ async function cleanupCurrentManagedSkillDir(
|
||||
await fs.rm(targetDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
async function cleanupCurrentManagedAgentFile(
|
||||
targetPath: string,
|
||||
manifest: PiInstallManifest | null,
|
||||
agentFileName: string,
|
||||
): Promise<void> {
|
||||
if (!manifest?.agents.includes(agentFileName)) return
|
||||
await fs.rm(targetPath, { force: true })
|
||||
}
|
||||
|
||||
// Explicit legacy Pi extension names this plugin has historically shipped and
|
||||
// no longer does. The manifest-diff cleanup in cleanupRemovedExtensions handles
|
||||
// post-manifest installs automatically, but pre-manifest installs return null
|
||||
// from readInstallManifestWithLegacyFallback and would otherwise leak the file
|
||||
// on upgrade. This list is the safety net for that case.
|
||||
const LEGACY_PI_EXTENSIONS_BY_PLUGIN: Record<string, string[]> = {
|
||||
"compound-engineering": ["compound-engineering-compat.ts"],
|
||||
}
|
||||
|
||||
// Plugins that historically shipped an mcporter.json (via the now-removed
|
||||
// compat extension) but no longer do when `bundle.mcporterConfig` is absent.
|
||||
// The per-plugin guard keeps us from touching mcporter configs owned by
|
||||
// plugins that still legitimately emit one.
|
||||
const LEGACY_PI_MCPORTER_PLUGINS = new Set<string>(["compound-engineering"])
|
||||
|
||||
type LegacyArtifactKind = "skills" | "prompts" | "extensions" | "mcporter"
|
||||
|
||||
// Display label used in the "Moved legacy Pi <label> artifact ..." log line.
|
||||
// Most kinds are a simple plural→singular trim, but "mcporter" isn't a plural,
|
||||
// so we special-case it instead of slicing off a character and logging
|
||||
// "mcporte".
|
||||
const LEGACY_ARTIFACT_LABELS: Record<LegacyArtifactKind, string> = {
|
||||
skills: "skill",
|
||||
prompts: "prompt",
|
||||
extensions: "extension",
|
||||
mcporter: "mcporter config",
|
||||
}
|
||||
|
||||
async function cleanupKnownLegacyPiArtifacts(paths: PiPaths, bundle: PiBundle): Promise<void> {
|
||||
const pluginName = bundle.pluginName
|
||||
if (!pluginName) return
|
||||
@@ -377,11 +452,29 @@ async function cleanupKnownLegacyPiArtifacts(paths: PiPaths, bundle: PiBundle):
|
||||
const legacyPromptPath = path.join(paths.promptsDir, promptFile)
|
||||
await moveLegacyArtifactToBackup(paths.managedDir, "prompts", legacyPromptPath)
|
||||
}
|
||||
|
||||
// Only sweep legacy extensions the current bundle is not actively writing.
|
||||
// A caller that explicitly ships an extension (e.g., tests or a future
|
||||
// bundle that reintroduces one) must not have its write undone.
|
||||
const currentExtensionNames = new Set(bundle.extensions.map((extension) => extension.name))
|
||||
for (const extensionFile of LEGACY_PI_EXTENSIONS_BY_PLUGIN[pluginName] ?? []) {
|
||||
if (currentExtensionNames.has(extensionFile)) continue
|
||||
const legacyExtensionPath = path.join(paths.extensionsDir, extensionFile)
|
||||
await moveLegacyArtifactToBackup(paths.managedDir, "extensions", legacyExtensionPath)
|
||||
}
|
||||
|
||||
// Sweep the stale mcporter.json left behind by the removed compat extension.
|
||||
// Only runs when the current bundle is NOT writing a fresh mcporter config —
|
||||
// if it IS (e.g. a plugin with `mcpServers`), the existing write path backs
|
||||
// up and overwrites the file and this sweep would undo that write.
|
||||
if (!bundle.mcporterConfig && LEGACY_PI_MCPORTER_PLUGINS.has(pluginName)) {
|
||||
await moveLegacyArtifactToBackup(paths.managedDir, "mcporter", paths.mcporterConfigPath)
|
||||
}
|
||||
}
|
||||
|
||||
async function moveLegacyArtifactToBackup(
|
||||
managedDir: string,
|
||||
kind: "skills" | "prompts",
|
||||
kind: LegacyArtifactKind,
|
||||
artifactPath: string,
|
||||
): Promise<void> {
|
||||
if (!(await pathExists(artifactPath))) return
|
||||
@@ -390,11 +483,12 @@ async function moveLegacyArtifactToBackup(
|
||||
const backupPath = path.join(backupDir, path.basename(artifactPath))
|
||||
await ensureDir(backupDir)
|
||||
await fs.rename(artifactPath, backupPath)
|
||||
console.warn(`Moved legacy Pi ${kind.slice(0, -1)} artifact to ${backupPath}`)
|
||||
console.warn(`Moved legacy Pi ${LEGACY_ARTIFACT_LABELS[kind]} artifact to ${backupPath}`)
|
||||
}
|
||||
|
||||
export {
|
||||
cleanupRemovedSkills as cleanupRemovedPiSkills,
|
||||
cleanupRemovedPrompts as cleanupRemovedPiPrompts,
|
||||
cleanupRemovedExtensions as cleanupRemovedPiExtensions,
|
||||
cleanupRemovedAgents as cleanupRemovedPiAgents,
|
||||
}
|
||||
|
||||
@@ -1,452 +0,0 @@
|
||||
export const PI_COMPAT_EXTENSION_SOURCE = `import fs from "node:fs"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
|
||||
import { Type } from "@sinclair/typebox"
|
||||
|
||||
const MAX_BYTES = 50 * 1024
|
||||
const DEFAULT_SUBAGENT_TIMEOUT_MS = 10 * 60 * 1000
|
||||
const MAX_PARALLEL_SUBAGENTS = 8
|
||||
|
||||
type SubagentTask = {
|
||||
agent: string
|
||||
task: string
|
||||
cwd?: string
|
||||
}
|
||||
|
||||
type SubagentResult = {
|
||||
agent: string
|
||||
task: string
|
||||
cwd: string
|
||||
exitCode: number
|
||||
output: string
|
||||
stderr: string
|
||||
}
|
||||
|
||||
function truncate(value: string): string {
|
||||
const input = value ?? ""
|
||||
if (Buffer.byteLength(input, "utf8") <= MAX_BYTES) return input
|
||||
const head = input.slice(0, MAX_BYTES)
|
||||
return head + "\\n\\n[Output truncated to 50KB]"
|
||||
}
|
||||
|
||||
function shellEscape(value: string): string {
|
||||
return "'" + value.replace(/'/g, "'\\"'\\"'") + "'"
|
||||
}
|
||||
|
||||
function normalizeName(value: string): string {
|
||||
return String(value || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_-]+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
}
|
||||
|
||||
function resolveBundledMcporterConfigPath(): string | undefined {
|
||||
try {
|
||||
const extensionDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const candidates = [
|
||||
path.join(extensionDir, "..", "pi-resources", "compound-engineering", "mcporter.json"),
|
||||
path.join(extensionDir, "..", "compound-engineering", "mcporter.json"),
|
||||
]
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) return candidate
|
||||
}
|
||||
} catch {
|
||||
// noop: bundled path is best-effort fallback
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function resolveMcporterConfigPath(cwd: string, explicit?: string): string | undefined {
|
||||
if (explicit && explicit.trim()) {
|
||||
return path.resolve(explicit)
|
||||
}
|
||||
|
||||
const projectPath = path.join(cwd, ".pi", "compound-engineering", "mcporter.json")
|
||||
if (fs.existsSync(projectPath)) return projectPath
|
||||
|
||||
const globalPath = path.join(os.homedir(), ".pi", "agent", "compound-engineering", "mcporter.json")
|
||||
if (fs.existsSync(globalPath)) return globalPath
|
||||
|
||||
return resolveBundledMcporterConfigPath()
|
||||
}
|
||||
|
||||
function resolveTaskCwd(baseCwd: string, taskCwd?: string): string {
|
||||
if (!taskCwd || !taskCwd.trim()) return baseCwd
|
||||
const expanded = taskCwd === "~"
|
||||
? os.homedir()
|
||||
: taskCwd.startsWith("~" + path.sep)
|
||||
? path.join(os.homedir(), taskCwd.slice(2))
|
||||
: taskCwd
|
||||
return path.resolve(baseCwd, expanded)
|
||||
}
|
||||
|
||||
async function runSingleSubagent(
|
||||
pi: ExtensionAPI,
|
||||
baseCwd: string,
|
||||
task: SubagentTask,
|
||||
signal?: AbortSignal,
|
||||
timeoutMs = DEFAULT_SUBAGENT_TIMEOUT_MS,
|
||||
): Promise<SubagentResult> {
|
||||
const agent = normalizeName(task.agent)
|
||||
if (!agent) {
|
||||
throw new Error("Subagent task is missing a valid agent name")
|
||||
}
|
||||
|
||||
const taskText = String(task.task ?? "").trim()
|
||||
if (!taskText) {
|
||||
throw new Error("Subagent task for " + agent + " is empty")
|
||||
}
|
||||
|
||||
const cwd = resolveTaskCwd(baseCwd, task.cwd)
|
||||
const prompt = "/skill:" + agent + " " + taskText
|
||||
const script = "cd " + shellEscape(cwd) + " && pi --no-session -p " + shellEscape(prompt)
|
||||
const result = await pi.exec("bash", ["-lc", script], { signal, timeout: timeoutMs })
|
||||
|
||||
return {
|
||||
agent,
|
||||
task: taskText,
|
||||
cwd,
|
||||
exitCode: result.code,
|
||||
output: truncate(result.stdout || ""),
|
||||
stderr: truncate(result.stderr || ""),
|
||||
}
|
||||
}
|
||||
|
||||
async function runParallelSubagents(
|
||||
pi: ExtensionAPI,
|
||||
baseCwd: string,
|
||||
tasks: SubagentTask[],
|
||||
signal?: AbortSignal,
|
||||
timeoutMs = DEFAULT_SUBAGENT_TIMEOUT_MS,
|
||||
maxConcurrency = 4,
|
||||
onProgress?: (completed: number, total: number) => void,
|
||||
): Promise<SubagentResult[]> {
|
||||
const safeConcurrency = Math.max(1, Math.min(maxConcurrency, MAX_PARALLEL_SUBAGENTS, tasks.length))
|
||||
const results: SubagentResult[] = new Array(tasks.length)
|
||||
|
||||
let nextIndex = 0
|
||||
let completed = 0
|
||||
|
||||
const workers = Array.from({ length: safeConcurrency }, async () => {
|
||||
while (true) {
|
||||
const current = nextIndex
|
||||
nextIndex += 1
|
||||
if (current >= tasks.length) return
|
||||
|
||||
results[current] = await runSingleSubagent(pi, baseCwd, tasks[current], signal, timeoutMs)
|
||||
completed += 1
|
||||
onProgress?.(completed, tasks.length)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(workers)
|
||||
return results
|
||||
}
|
||||
|
||||
function formatSubagentSummary(results: SubagentResult[]): string {
|
||||
if (results.length === 0) return "No subagent work was executed."
|
||||
|
||||
const success = results.filter((result) => result.exitCode === 0).length
|
||||
const failed = results.length - success
|
||||
const header = failed === 0
|
||||
? "Subagent run completed: " + success + "/" + results.length + " succeeded."
|
||||
: "Subagent run completed: " + success + "/" + results.length + " succeeded, " + failed + " failed."
|
||||
|
||||
const lines = results.map((result) => {
|
||||
const status = result.exitCode === 0 ? "ok" : "error"
|
||||
const body = result.output || result.stderr || "(no output)"
|
||||
const preview = body.split("\\n").slice(0, 6).join("\\n")
|
||||
return "\\n[" + status + "] " + result.agent + "\\n" + preview
|
||||
})
|
||||
|
||||
return header + lines.join("\\n")
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.registerTool({
|
||||
name: "ask_user_question",
|
||||
label: "Ask User Question",
|
||||
description: "Ask the user a question with optional choices.",
|
||||
parameters: Type.Object({
|
||||
question: Type.String({ description: "Question shown to the user" }),
|
||||
options: Type.Optional(Type.Array(Type.String(), { description: "Selectable options" })),
|
||||
allowCustom: Type.Optional(Type.Boolean({ default: true })),
|
||||
}),
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
if (!ctx.hasUI) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: "text", text: "UI is unavailable in this mode." }],
|
||||
details: {},
|
||||
}
|
||||
}
|
||||
|
||||
const options = params.options ?? []
|
||||
const allowCustom = params.allowCustom ?? true
|
||||
|
||||
if (options.length === 0) {
|
||||
const answer = await ctx.ui.input(params.question)
|
||||
if (!answer) {
|
||||
return {
|
||||
content: [{ type: "text", text: "User cancelled." }],
|
||||
details: { answer: null },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: "User answered: " + answer }],
|
||||
details: { answer, mode: "input" },
|
||||
}
|
||||
}
|
||||
|
||||
const customLabel = "Other (type custom answer)"
|
||||
const selectable = allowCustom ? [...options, customLabel] : options
|
||||
const selected = await ctx.ui.select(params.question, selectable)
|
||||
|
||||
if (!selected) {
|
||||
return {
|
||||
content: [{ type: "text", text: "User cancelled." }],
|
||||
details: { answer: null },
|
||||
}
|
||||
}
|
||||
|
||||
if (selected === customLabel) {
|
||||
const custom = await ctx.ui.input("Your answer")
|
||||
if (!custom) {
|
||||
return {
|
||||
content: [{ type: "text", text: "User cancelled." }],
|
||||
details: { answer: null },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: "User answered: " + custom }],
|
||||
details: { answer: custom, mode: "custom" },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: "User selected: " + selected }],
|
||||
details: { answer: selected, mode: "select" },
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const subagentTaskSchema = Type.Object({
|
||||
agent: Type.String({ description: "Skill/agent name to invoke" }),
|
||||
task: Type.String({ description: "Task instructions for that skill" }),
|
||||
cwd: Type.Optional(Type.String({ description: "Optional working directory for this task" })),
|
||||
})
|
||||
|
||||
pi.registerTool({
|
||||
name: "subagent",
|
||||
label: "Subagent",
|
||||
description: "Run one or more skill-based subagent tasks. Supports single, parallel, and chained execution.",
|
||||
parameters: Type.Object({
|
||||
agent: Type.Optional(Type.String({ description: "Single subagent name" })),
|
||||
task: Type.Optional(Type.String({ description: "Single subagent task" })),
|
||||
cwd: Type.Optional(Type.String({ description: "Working directory for single mode" })),
|
||||
tasks: Type.Optional(Type.Array(subagentTaskSchema, { description: "Parallel subagent tasks" })),
|
||||
chain: Type.Optional(Type.Array(subagentTaskSchema, { description: "Sequential tasks; supports {previous} placeholder" })),
|
||||
maxConcurrency: Type.Optional(Type.Number({ default: 4 })),
|
||||
timeoutMs: Type.Optional(Type.Number({ default: DEFAULT_SUBAGENT_TIMEOUT_MS })),
|
||||
}),
|
||||
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
||||
const hasSingle = Boolean(params.agent && params.task)
|
||||
const hasTasks = Boolean(params.tasks && params.tasks.length > 0)
|
||||
const hasChain = Boolean(params.chain && params.chain.length > 0)
|
||||
const modeCount = Number(hasSingle) + Number(hasTasks) + Number(hasChain)
|
||||
|
||||
if (modeCount !== 1) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: "text", text: "Provide exactly one mode: single (agent+task), tasks, or chain." }],
|
||||
details: {},
|
||||
}
|
||||
}
|
||||
|
||||
const timeoutMs = Number(params.timeoutMs || DEFAULT_SUBAGENT_TIMEOUT_MS)
|
||||
|
||||
try {
|
||||
if (hasSingle) {
|
||||
const result = await runSingleSubagent(
|
||||
pi,
|
||||
ctx.cwd,
|
||||
{ agent: params.agent!, task: params.task!, cwd: params.cwd },
|
||||
signal,
|
||||
timeoutMs,
|
||||
)
|
||||
|
||||
const body = formatSubagentSummary([result])
|
||||
return {
|
||||
isError: result.exitCode !== 0,
|
||||
content: [{ type: "text", text: body }],
|
||||
details: { mode: "single", results: [result] },
|
||||
}
|
||||
}
|
||||
|
||||
if (hasTasks) {
|
||||
const tasks = params.tasks as SubagentTask[]
|
||||
const maxConcurrency = Number(params.maxConcurrency || 4)
|
||||
|
||||
const results = await runParallelSubagents(
|
||||
pi,
|
||||
ctx.cwd,
|
||||
tasks,
|
||||
signal,
|
||||
timeoutMs,
|
||||
maxConcurrency,
|
||||
(completed, total) => {
|
||||
onUpdate?.({
|
||||
content: [{ type: "text", text: "Subagent progress: " + completed + "/" + total }],
|
||||
details: { mode: "parallel", completed, total },
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
const body = formatSubagentSummary(results)
|
||||
const hasFailure = results.some((result) => result.exitCode !== 0)
|
||||
|
||||
return {
|
||||
isError: hasFailure,
|
||||
content: [{ type: "text", text: body }],
|
||||
details: { mode: "parallel", results },
|
||||
}
|
||||
}
|
||||
|
||||
const chain = params.chain as SubagentTask[]
|
||||
const results: SubagentResult[] = []
|
||||
let previous = ""
|
||||
|
||||
for (const step of chain) {
|
||||
const resolvedTask = step.task.replace(/\\{previous\\}/g, previous)
|
||||
const result = await runSingleSubagent(
|
||||
pi,
|
||||
ctx.cwd,
|
||||
{ agent: step.agent, task: resolvedTask, cwd: step.cwd },
|
||||
signal,
|
||||
timeoutMs,
|
||||
)
|
||||
results.push(result)
|
||||
previous = result.output || result.stderr
|
||||
|
||||
onUpdate?.({
|
||||
content: [{ type: "text", text: "Subagent chain progress: " + results.length + "/" + chain.length }],
|
||||
details: { mode: "chain", completed: results.length, total: chain.length },
|
||||
})
|
||||
|
||||
if (result.exitCode !== 0) break
|
||||
}
|
||||
|
||||
const body = formatSubagentSummary(results)
|
||||
const hasFailure = results.some((result) => result.exitCode !== 0)
|
||||
|
||||
return {
|
||||
isError: hasFailure,
|
||||
content: [{ type: "text", text: body }],
|
||||
details: { mode: "chain", results },
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
||||
details: {},
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
pi.registerTool({
|
||||
name: "mcporter_list",
|
||||
label: "MCPorter List",
|
||||
description: "List tools on an MCP server through MCPorter.",
|
||||
parameters: Type.Object({
|
||||
server: Type.String({ description: "Configured MCP server name" }),
|
||||
allParameters: Type.Optional(Type.Boolean({ default: false })),
|
||||
json: Type.Optional(Type.Boolean({ default: true })),
|
||||
configPath: Type.Optional(Type.String({ description: "Optional mcporter config path" })),
|
||||
}),
|
||||
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
||||
const args = ["list", params.server]
|
||||
if (params.allParameters) args.push("--all-parameters")
|
||||
if (params.json ?? true) args.push("--json")
|
||||
|
||||
const configPath = resolveMcporterConfigPath(ctx.cwd, params.configPath)
|
||||
if (configPath) {
|
||||
args.push("--config", configPath)
|
||||
}
|
||||
|
||||
const result = await pi.exec("mcporter", args, { signal })
|
||||
const output = truncate(result.stdout || result.stderr || "")
|
||||
|
||||
return {
|
||||
isError: result.code !== 0,
|
||||
content: [{ type: "text", text: output || "(no output)" }],
|
||||
details: {
|
||||
exitCode: result.code,
|
||||
command: "mcporter " + args.join(" "),
|
||||
configPath,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
pi.registerTool({
|
||||
name: "mcporter_call",
|
||||
label: "MCPorter Call",
|
||||
description: "Call a specific MCP tool through MCPorter.",
|
||||
parameters: Type.Object({
|
||||
call: Type.Optional(Type.String({ description: "Function-style call, e.g. linear.list_issues(limit: 5)" })),
|
||||
server: Type.Optional(Type.String({ description: "Server name (if call is omitted)" })),
|
||||
tool: Type.Optional(Type.String({ description: "Tool name (if call is omitted)" })),
|
||||
args: Type.Optional(Type.Record(Type.String(), Type.Any(), { description: "JSON arguments object" })),
|
||||
configPath: Type.Optional(Type.String({ description: "Optional mcporter config path" })),
|
||||
}),
|
||||
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
||||
const args = ["call"]
|
||||
|
||||
if (params.call && params.call.trim()) {
|
||||
args.push(params.call.trim())
|
||||
} else {
|
||||
if (!params.server || !params.tool) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: "text", text: "Provide either call, or server + tool." }],
|
||||
details: {},
|
||||
}
|
||||
}
|
||||
args.push(params.server + "." + params.tool)
|
||||
if (params.args) {
|
||||
args.push("--args", JSON.stringify(params.args))
|
||||
}
|
||||
}
|
||||
|
||||
args.push("--output", "json")
|
||||
|
||||
const configPath = resolveMcporterConfigPath(ctx.cwd, params.configPath)
|
||||
if (configPath) {
|
||||
args.push("--config", configPath)
|
||||
}
|
||||
|
||||
const result = await pi.exec("mcporter", args, { signal })
|
||||
const output = truncate(result.stdout || result.stderr || "")
|
||||
|
||||
return {
|
||||
isError: result.code !== 0,
|
||||
content: [{ type: "text", text: output || "(no output)" }],
|
||||
details: {
|
||||
exitCode: result.code,
|
||||
command: "mcporter " + args.join(" "),
|
||||
configPath,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
`
|
||||
@@ -13,6 +13,11 @@ export type PiGeneratedSkill = {
|
||||
content: string
|
||||
}
|
||||
|
||||
export type PiGeneratedAgent = {
|
||||
name: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export type PiExtensionFile = {
|
||||
name: string
|
||||
content: string
|
||||
@@ -36,6 +41,7 @@ export type PiBundle = {
|
||||
prompts: PiPrompt[]
|
||||
skillDirs: PiSkillDir[]
|
||||
generatedSkills: PiGeneratedSkill[]
|
||||
agents: PiGeneratedAgent[]
|
||||
extensions: PiExtensionFile[]
|
||||
mcporterConfig?: PiMcporterConfig
|
||||
}
|
||||
|
||||
@@ -1680,8 +1680,14 @@ describe("CLI", () => {
|
||||
expect(stdout).toContain("Converted compound-engineering")
|
||||
expect(stdout).toContain(piRoot)
|
||||
expect(await exists(path.join(piRoot, "prompts", "workflows-review.md"))).toBe(true)
|
||||
expect(await exists(path.join(piRoot, "skills", "repo-research-analyst", "SKILL.md"))).toBe(true)
|
||||
expect(await exists(path.join(piRoot, "extensions", "compound-engineering-compat.ts"))).toBe(true)
|
||||
// Claude agents now install at .pi/agents/<name>.md (Pi agent format) so
|
||||
// nicobailon/pi-subagents can resolve them via the `subagent` tool.
|
||||
expect(await exists(path.join(piRoot, "agents", "repo-research-analyst.md"))).toBe(true)
|
||||
// Pi installs no longer ship a plugin-authored compat extension; users install
|
||||
// community pi-subagents + pi-ask-user extensions directly in Pi. MCP servers
|
||||
// declared in plugin.json are still translated to mcporter.json so plugins
|
||||
// with MCP wiring keep their backends after conversion.
|
||||
expect(await exists(path.join(piRoot, "extensions", "compound-engineering-compat.ts"))).toBe(false)
|
||||
expect(await exists(path.join(piRoot, "compound-engineering", "mcporter.json"))).toBe(true)
|
||||
})
|
||||
|
||||
@@ -1721,7 +1727,7 @@ describe("CLI", () => {
|
||||
expect(stdout).toContain("Installed compound-engineering")
|
||||
expect(stdout).toContain(piRoot)
|
||||
expect(await exists(path.join(piRoot, "prompts", "workflows-review.md"))).toBe(true)
|
||||
expect(await exists(path.join(piRoot, "extensions", "compound-engineering-compat.ts"))).toBe(true)
|
||||
expect(await exists(path.join(piRoot, "extensions", "compound-engineering-compat.ts"))).toBe(false)
|
||||
})
|
||||
|
||||
test("install --to opencode uses permissions:none by default", async () => {
|
||||
|
||||
@@ -94,6 +94,19 @@ describe("frontmatter YAML validity", () => {
|
||||
`Shorten description to ${MAX_SKILL_DESCRIPTION_LENGTH} chars or less`,
|
||||
).toBeLessThanOrEqual(MAX_SKILL_DESCRIPTION_LENGTH)
|
||||
})
|
||||
|
||||
// Pi rejects skill names that don't match the parent directory or contain
|
||||
// characters outside [a-z0-9-]. Upgrading from a pre-v3 install with
|
||||
// `name: ce:brainstorm` frontmatter in a renamed `ce-brainstorm` directory
|
||||
// triggered issue #449. Catch any reintroduction at the source.
|
||||
test(`${pluginRoot}/${rel} skill frontmatter name matches directory and uses valid characters`, () => {
|
||||
const parsed = load(yaml) as Record<string, unknown> | null
|
||||
const name = parsed && typeof parsed.name === "string" ? parsed.name : ""
|
||||
const dirName = path.basename(path.dirname(rel))
|
||||
expect(name, `frontmatter name must be present`).not.toBe("")
|
||||
expect(name, `frontmatter name "${name}" must match parent directory "${dirName}"`).toBe(dirName)
|
||||
expect(name, `frontmatter name "${name}" must be lowercase a-z, 0-9, and hyphens`).toMatch(/^[a-z0-9-]+$/)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { ClaudePlugin } from "../src/types/claude"
|
||||
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
|
||||
|
||||
describe("convertClaudeToPi", () => {
|
||||
test("converts commands, skills, extensions, and MCPorter config", async () => {
|
||||
test("converts commands, skills, agents, and MCP servers without shipping a Pi extension", async () => {
|
||||
const plugin = await loadClaudePlugin(fixtureRoot)
|
||||
const bundle = convertClaudeToPi(plugin, {
|
||||
agentMode: "subagent",
|
||||
@@ -28,22 +28,61 @@ describe("convertClaudeToPi", () => {
|
||||
const parsedPrompt = parseFrontmatter(workflowsReview!.content)
|
||||
expect(parsedPrompt.data.description).toBe("Run a multi-agent review workflow")
|
||||
|
||||
// Existing skills are copied and agents are converted into generated Pi skills
|
||||
// Existing skills are copied as skill dirs; Claude agents are converted to
|
||||
// Pi agent files (under bundle.agents, written to .pi/agents/<name>.md) so
|
||||
// that nicobailon/pi-subagents' `subagent` tool can resolve them by name.
|
||||
expect(bundle.skillDirs.some((skill) => skill.name === "skill-one")).toBe(true)
|
||||
expect(bundle.generatedSkills.some((skill) => skill.name === "repo-research-analyst")).toBe(true)
|
||||
expect(bundle.agents.some((agent) => agent.name === "repo-research-analyst")).toBe(true)
|
||||
// Agents no longer leak into generatedSkills — that field is reserved for
|
||||
// commands-as-skills on other targets; Pi keeps it empty.
|
||||
expect(bundle.generatedSkills).toEqual([])
|
||||
|
||||
// Pi compatibility extension is included (with subagent + MCPorter tools)
|
||||
const compatExtension = bundle.extensions.find((extension) => extension.name === "compound-engineering-compat.ts")
|
||||
expect(compatExtension).toBeDefined()
|
||||
expect(compatExtension!.content).toContain('name: "subagent"')
|
||||
expect(compatExtension!.content).toContain('name: "mcporter_call"')
|
||||
// Pi installs now depend on the community pi-subagents and pi-ask-user extensions,
|
||||
// so the converter emits no bundled extension. Legacy cleanup in the Pi writer
|
||||
// removes any prior compound-engineering-compat.ts on upgrade.
|
||||
expect(bundle.extensions).toEqual([])
|
||||
|
||||
// Claude MCP config is translated to MCPorter config
|
||||
expect(bundle.mcporterConfig?.mcpServers.context7?.baseUrl).toBe("https://mcp.context7.com/mcp")
|
||||
expect(bundle.mcporterConfig?.mcpServers["local-tooling"]?.command).toBe("echo")
|
||||
// MCP servers declared in plugin.json are translated to Pi's mcporter.json
|
||||
// shape so plugins with MCP wiring keep their backends after conversion.
|
||||
// The fixture declares both an HTTP url server (context7) and a stdio
|
||||
// command server (local-tooling).
|
||||
expect(bundle.mcporterConfig).toEqual({
|
||||
mcpServers: {
|
||||
context7: {
|
||||
baseUrl: "https://mcp.context7.com/mcp",
|
||||
headers: undefined,
|
||||
},
|
||||
"local-tooling": {
|
||||
command: "echo",
|
||||
args: ["fixture"],
|
||||
env: undefined,
|
||||
headers: undefined,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("transforms Task calls, AskUserQuestion, slash commands, and todo tool references", () => {
|
||||
test("omits mcporterConfig when the plugin declares no MCP servers", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
root: "/tmp/plugin",
|
||||
manifest: { name: "fixture", version: "1.0.0" },
|
||||
agents: [],
|
||||
commands: [],
|
||||
skills: [],
|
||||
hooks: undefined,
|
||||
mcpServers: undefined,
|
||||
}
|
||||
|
||||
const bundle = convertClaudeToPi(plugin, {
|
||||
agentMode: "subagent",
|
||||
inferTemperature: false,
|
||||
permissions: "none",
|
||||
})
|
||||
|
||||
expect(bundle.mcporterConfig).toBeUndefined()
|
||||
})
|
||||
|
||||
test("transforms Task calls, slash commands, and todo tool references; preserves AskUserQuestion", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
root: "/tmp/plugin",
|
||||
manifest: { name: "fixture", version: "1.0.0" },
|
||||
@@ -79,7 +118,10 @@ describe("convertClaudeToPi", () => {
|
||||
|
||||
expect(parsedPrompt.body).toContain("Run subagent with agent=\"repo-research-analyst\" and task=\"feature_description\".")
|
||||
expect(parsedPrompt.body).toContain("Run subagent with agent=\"learnings-researcher\" and task=\"feature_description\".")
|
||||
expect(parsedPrompt.body).toContain("ask_user_question")
|
||||
// AskUserQuestion is preserved; skill source-side enumerations name each platform's
|
||||
// blocking-question tool (including `ask_user` for Pi via pi-ask-user), so the
|
||||
// converter no longer rewrites the token.
|
||||
expect(parsedPrompt.body).toContain("AskUserQuestion")
|
||||
expect(parsedPrompt.body).toContain("/workflows-work")
|
||||
expect(parsedPrompt.body).toContain("/todo-resolve")
|
||||
expect(parsedPrompt.body).toContain("the platform's task-tracking primitive")
|
||||
@@ -184,32 +226,4 @@ describe("convertClaudeToPi", () => {
|
||||
expect(parsedPrompt.body).not.toContain("()")
|
||||
})
|
||||
|
||||
test("appends MCPorter compatibility note when command references MCP", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
root: "/tmp/plugin",
|
||||
manifest: { name: "fixture", version: "1.0.0" },
|
||||
agents: [],
|
||||
commands: [
|
||||
{
|
||||
name: "docs",
|
||||
description: "Read MCP docs",
|
||||
body: "Use MCP servers for docs lookup.",
|
||||
sourcePath: "/tmp/plugin/commands/docs.md",
|
||||
},
|
||||
],
|
||||
skills: [],
|
||||
hooks: undefined,
|
||||
mcpServers: undefined,
|
||||
}
|
||||
|
||||
const bundle = convertClaudeToPi(plugin, {
|
||||
agentMode: "subagent",
|
||||
inferTemperature: false,
|
||||
permissions: "none",
|
||||
})
|
||||
|
||||
const parsedPrompt = parseFrontmatter(bundle.prompts[0].content)
|
||||
expect(parsedPrompt.body).toContain("Pi + MCPorter note")
|
||||
expect(parsedPrompt.body).toContain("mcporter_call")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -47,6 +47,7 @@ describe("writePiBundle", () => {
|
||||
prompts: [],
|
||||
skillDirs: [],
|
||||
generatedSkills: [],
|
||||
agents: [],
|
||||
extensions: [],
|
||||
}
|
||||
|
||||
@@ -69,7 +70,8 @@ describe("writePiBundle", () => {
|
||||
sourceDir: path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one"),
|
||||
},
|
||||
],
|
||||
generatedSkills: [{ name: "repo-research-analyst", content: "---\nname: repo-research-analyst\n---\n\nBody" }],
|
||||
generatedSkills: [],
|
||||
agents: [{ name: "repo-research-analyst", content: "---\nname: repo-research-analyst\n---\n\nBody" }],
|
||||
extensions: [{ name: "compound-engineering-compat.ts", content: "export default function () {}" }],
|
||||
mcporterConfig: {
|
||||
mcpServers: {
|
||||
@@ -82,7 +84,10 @@ describe("writePiBundle", () => {
|
||||
|
||||
expect(await exists(path.join(outputRoot, "prompts", "workflows-plan.md"))).toBe(true)
|
||||
expect(await exists(path.join(outputRoot, "skills", "skill-one", "SKILL.md"))).toBe(true)
|
||||
expect(await exists(path.join(outputRoot, "skills", "repo-research-analyst", "SKILL.md"))).toBe(true)
|
||||
// Claude agents are now written as Pi agent files (.pi/agents/<name>.md),
|
||||
// not skill directories, so nicobailon/pi-subagents can resolve them via
|
||||
// the `subagent` tool.
|
||||
expect(await exists(path.join(outputRoot, "agents", "repo-research-analyst.md"))).toBe(true)
|
||||
expect(await exists(path.join(outputRoot, "extensions", "compound-engineering-compat.ts"))).toBe(true)
|
||||
expect(await exists(path.join(outputRoot, "compound-engineering", "mcporter.json"))).toBe(true)
|
||||
expect(await exists(path.join(outputRoot, "compound-engineering", "install-manifest.json"))).toBe(true)
|
||||
@@ -90,7 +95,8 @@ describe("writePiBundle", () => {
|
||||
const agentsPath = path.join(outputRoot, "AGENTS.md")
|
||||
const agentsContent = await fs.readFile(agentsPath, "utf8")
|
||||
expect(agentsContent).toContain("BEGIN COMPOUND PI TOOL MAP")
|
||||
expect(agentsContent).toContain("MCPorter")
|
||||
expect(agentsContent).toContain("pi-subagents")
|
||||
expect(agentsContent).toContain("pi-ask-user")
|
||||
})
|
||||
|
||||
test("transforms Task calls in copied SKILL.md files", async () => {
|
||||
@@ -117,6 +123,7 @@ Run these research agents:
|
||||
prompts: [],
|
||||
skillDirs: [{ name: "ce-plan", sourceDir: sourceSkillDir }],
|
||||
generatedSkills: [],
|
||||
agents: [],
|
||||
extensions: [],
|
||||
}
|
||||
|
||||
@@ -141,6 +148,7 @@ Run these research agents:
|
||||
prompts: [{ name: "workflows-work", content: "Prompt content" }],
|
||||
skillDirs: [],
|
||||
generatedSkills: [],
|
||||
agents: [],
|
||||
extensions: [],
|
||||
}
|
||||
|
||||
@@ -162,6 +170,7 @@ Run these research agents:
|
||||
prompts: [],
|
||||
skillDirs: [],
|
||||
generatedSkills: [],
|
||||
agents: [],
|
||||
extensions: [],
|
||||
mcporterConfig: {
|
||||
mcpServers: {
|
||||
@@ -193,7 +202,8 @@ Run these research agents:
|
||||
sourceDir: path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one"),
|
||||
},
|
||||
],
|
||||
generatedSkills: [{ name: "old-agent", content: "---\nname: old-agent\n---\n\nBody" }],
|
||||
generatedSkills: [],
|
||||
agents: [{ name: "old-agent", content: "---\nname: old-agent\n---\n\nBody" }],
|
||||
extensions: [{ name: "compound-engineering-compat.ts", content: "export default function first() {}" }],
|
||||
})
|
||||
|
||||
@@ -201,15 +211,16 @@ Run these research agents:
|
||||
pluginName: "compound-engineering",
|
||||
prompts: [{ name: "new-prompt", content: "Prompt content" }],
|
||||
skillDirs: [],
|
||||
generatedSkills: [{ name: "new-agent", content: "---\nname: new-agent\n---\n\nBody" }],
|
||||
generatedSkills: [],
|
||||
agents: [{ name: "new-agent", content: "---\nname: new-agent\n---\n\nBody" }],
|
||||
extensions: [],
|
||||
})
|
||||
|
||||
expect(await exists(path.join(outputRoot, "prompts", "old-prompt.md"))).toBe(false)
|
||||
expect(await exists(path.join(outputRoot, "prompts", "new-prompt.md"))).toBe(true)
|
||||
expect(await exists(path.join(outputRoot, "skills", "skill-one", "SKILL.md"))).toBe(false)
|
||||
expect(await exists(path.join(outputRoot, "skills", "old-agent", "SKILL.md"))).toBe(false)
|
||||
expect(await exists(path.join(outputRoot, "skills", "new-agent", "SKILL.md"))).toBe(true)
|
||||
expect(await exists(path.join(outputRoot, "agents", "old-agent.md"))).toBe(false)
|
||||
expect(await exists(path.join(outputRoot, "agents", "new-agent.md"))).toBe(true)
|
||||
expect(await exists(path.join(outputRoot, "extensions", "compound-engineering-compat.ts"))).toBe(false)
|
||||
})
|
||||
|
||||
@@ -228,6 +239,7 @@ Run these research agents:
|
||||
},
|
||||
],
|
||||
generatedSkills: [{ name: "ce-gen-skill", content: "---\nname: ce-gen-skill\n---\n\nBody" }],
|
||||
agents: [],
|
||||
extensions: [{ name: "ce-ext.ts", content: "export default function () {}" }],
|
||||
})
|
||||
|
||||
@@ -242,6 +254,7 @@ Run these research agents:
|
||||
},
|
||||
],
|
||||
generatedSkills: [{ name: "tutor-gen-skill", content: "---\nname: tutor-gen-skill\n---\n\nBody" }],
|
||||
agents: [],
|
||||
extensions: [{ name: "tutor-ext.ts", content: "export default function () {}" }],
|
||||
})
|
||||
|
||||
@@ -258,6 +271,7 @@ Run these research agents:
|
||||
prompts: [],
|
||||
skillDirs: [],
|
||||
generatedSkills: [],
|
||||
agents: [],
|
||||
extensions: [],
|
||||
})
|
||||
|
||||
@@ -272,6 +286,53 @@ Run these research agents:
|
||||
expect(await exists(path.join(outputRoot, "coding-tutor", "install-manifest.json"))).toBe(true)
|
||||
})
|
||||
|
||||
test("moves stale compound-engineering mcporter.json to legacy backup when bundle has no mcporterConfig", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "pi-legacy-mcporter-"))
|
||||
const outputRoot = path.join(tempRoot, ".pi")
|
||||
const staleConfigPath = path.join(outputRoot, "compound-engineering", "mcporter.json")
|
||||
|
||||
await fs.mkdir(path.dirname(staleConfigPath), { recursive: true })
|
||||
await fs.writeFile(
|
||||
staleConfigPath,
|
||||
JSON.stringify({ mcpServers: { stale: { baseUrl: "https://example.invalid/mcp" } } }, null, 2),
|
||||
)
|
||||
|
||||
const bundle: PiBundle = {
|
||||
pluginName: "compound-engineering",
|
||||
prompts: [],
|
||||
skillDirs: [],
|
||||
generatedSkills: [],
|
||||
agents: [],
|
||||
extensions: [],
|
||||
// No mcporterConfig — the compound-engineering plugin ships no MCP
|
||||
// servers, so the file written by the removed compat extension should
|
||||
// be swept into legacy-backup rather than lingering on disk.
|
||||
}
|
||||
|
||||
await writePiBundle(outputRoot, bundle)
|
||||
|
||||
expect(await exists(staleConfigPath)).toBe(false)
|
||||
|
||||
const legacyBackupRoot = path.join(outputRoot, "compound-engineering", "legacy-backup")
|
||||
expect(await exists(legacyBackupRoot)).toBe(true)
|
||||
|
||||
const timestamps = await fs.readdir(legacyBackupRoot)
|
||||
const mcporterBackup = (
|
||||
await Promise.all(
|
||||
timestamps.map(async (timestamp) => {
|
||||
const candidate = path.join(legacyBackupRoot, timestamp, "mcporter", "mcporter.json")
|
||||
return (await exists(candidate)) ? candidate : null
|
||||
}),
|
||||
)
|
||||
).find((candidate): candidate is string => candidate !== null)
|
||||
|
||||
expect(mcporterBackup).toBeDefined()
|
||||
const backedUp = JSON.parse(await fs.readFile(mcporterBackup!, "utf8")) as {
|
||||
mcpServers: Record<string, { baseUrl?: string }>
|
||||
}
|
||||
expect(backedUp.mcpServers.stale?.baseUrl).toBe("https://example.invalid/mcp")
|
||||
})
|
||||
|
||||
test("moves legacy flat Pi CE artifacts to a namespaced backup", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "pi-legacy-artifacts-"))
|
||||
const outputRoot = path.join(tempRoot, ".pi")
|
||||
@@ -297,7 +358,9 @@ Run these research agents:
|
||||
expect(await exists(path.join(outputRoot, "prompts", "reproduce-bug.md"))).toBe(false)
|
||||
expect(await exists(path.join(outputRoot, "prompts", "report-bug.md"))).toBe(false)
|
||||
expect(await exists(path.join(outputRoot, "skills", "ce-plan", "SKILL.md"))).toBe(true)
|
||||
expect(await exists(path.join(outputRoot, "skills", "ce-repo-research-analyst", "SKILL.md"))).toBe(true)
|
||||
// ce-repo-research-analyst is a Claude agent, so it installs to .pi/agents/<name>.md
|
||||
// (not .pi/skills/<name>/SKILL.md) so nicobailon/pi-subagents can resolve it.
|
||||
expect(await exists(path.join(outputRoot, "agents", "ce-repo-research-analyst.md"))).toBe(true)
|
||||
expect(await exists(path.join(outputRoot, "compound-engineering", "legacy-backup"))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user