@pi-unipi/subagents 2.6.1 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -78
- package/agents/delegate.md +14 -0
- package/agents/oracle.md +78 -0
- package/agents/researcher.md +52 -0
- package/agents/reviewer.md +79 -0
- package/agents/scout.md +50 -0
- package/agents/worker.md +59 -0
- package/package.json +22 -20
- package/prompts/council.md +48 -0
- package/prompts/gather-context-and-clarify.md +13 -0
- package/prompts/parallel-cleanup.md +59 -0
- package/prompts/parallel-research.md +50 -0
- package/prompts/parallel-review.md +54 -0
- package/prompts/review-loop.md +43 -0
- package/skills/council-mode/SKILL.md +230 -0
- package/skills/subagents/SKILL.md +49 -0
- package/skills/subagents/references/constraints-and-recipes.md +259 -0
- package/skills/subagents/references/execution-controls.md +454 -0
- package/skills/subagents/references/management-authoring-rpc.md +161 -0
- package/skills/subagents/references/multi-lane-orchestration.md +39 -0
- package/skills/subagents/references/prompting-and-roles.md +267 -0
- package/src/acceptance.ts +331 -0
- package/src/agent-manager.ts +405 -0
- package/src/agent-memory.ts +300 -0
- package/src/agent-overrides.ts +183 -0
- package/src/agent-runner.ts +271 -0
- package/src/async-runner.ts +417 -0
- package/src/authority-policy.ts +47 -0
- package/src/budgets.ts +285 -0
- package/src/child-safety.ts +140 -0
- package/src/config.ts +226 -0
- package/src/conversation-viewer.ts +281 -0
- package/src/core-compat.ts +94 -0
- package/src/custom-agents.ts +323 -0
- package/src/file-system-retry.ts +50 -0
- package/src/fleet-data.ts +61 -0
- package/src/fleet-view.ts +316 -0
- package/src/foreground-detach.ts +59 -0
- package/src/fork-context.ts +285 -0
- package/src/global.d.ts +7 -0
- package/src/guide.ts +129 -0
- package/src/index.ts +1149 -0
- package/src/mission-state.ts +133 -0
- package/src/mission-store.ts +434 -0
- package/src/model-resolver.ts +79 -0
- package/src/output-limits.ts +142 -0
- package/src/parity-types.ts +315 -0
- package/src/pi-args.ts +161 -0
- package/src/pi-spawn.ts +147 -0
- package/src/result-files.ts +232 -0
- package/src/result-watcher.ts +221 -0
- package/src/retained-children.ts +176 -0
- package/src/run-fanout-budget.ts +356 -0
- package/src/scheduled-runs.ts +351 -0
- package/src/schemas.ts +263 -0
- package/src/slash-commands.ts +77 -0
- package/src/supervisor-channel.ts +249 -0
- package/src/tool-handler.ts +1144 -0
- package/src/types.ts +238 -0
- package/src/widget.ts +447 -0
- package/src/workflow-script.ts +737 -0
- package/src/workflow-worker.ts +384 -0
- package/src/worktree.ts +614 -0
- package/dist/agent-manager.d.ts +0 -81
- package/dist/agent-manager.d.ts.map +0 -1
- package/dist/agent-manager.js +0 -295
- package/dist/agent-manager.js.map +0 -1
- package/dist/agent-runner.d.ts +0 -51
- package/dist/agent-runner.d.ts.map +0 -1
- package/dist/agent-runner.js +0 -262
- package/dist/agent-runner.js.map +0 -1
- package/dist/config.d.ts +0 -24
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -132
- package/dist/config.js.map +0 -1
- package/dist/conversation-viewer.d.ts +0 -40
- package/dist/conversation-viewer.d.ts.map +0 -1
- package/dist/conversation-viewer.js +0 -276
- package/dist/conversation-viewer.js.map +0 -1
- package/dist/core-compat.d.ts +0 -21
- package/dist/core-compat.d.ts.map +0 -1
- package/dist/core-compat.js +0 -86
- package/dist/core-compat.js.map +0 -1
- package/dist/custom-agents.d.ts +0 -14
- package/dist/custom-agents.d.ts.map +0 -1
- package/dist/custom-agents.js +0 -113
- package/dist/custom-agents.js.map +0 -1
- package/dist/file-lock.d.ts +0 -42
- package/dist/file-lock.d.ts.map +0 -1
- package/dist/file-lock.js +0 -91
- package/dist/file-lock.js.map +0 -1
- package/dist/index.d.ts +0 -10
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -758
- package/dist/index.js.map +0 -1
- package/dist/model-resolver.d.ts +0 -19
- package/dist/model-resolver.d.ts.map +0 -1
- package/dist/model-resolver.js +0 -61
- package/dist/model-resolver.js.map +0 -1
- package/dist/types.d.ts +0 -98
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -47
- package/dist/types.js.map +0 -1
- package/dist/widget.d.ts +0 -56
- package/dist/widget.d.ts.map +0 -1
- package/dist/widget.js +0 -396
- package/dist/widget.js.map +0 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Multi-Lane Orchestration
|
|
2
|
+
|
|
3
|
+
Use this reference when several independent tasks need coordinated workers, worktrees, or repositories. It defines lane ownership; use the other unipi subagents references for run controls, prompts, and mission details. The parent remains the final decision-maker.
|
|
4
|
+
|
|
5
|
+
## Lane board and authority
|
|
6
|
+
|
|
7
|
+
Before multiple mutation-capable lanes start, record this board in the parent context:
|
|
8
|
+
|
|
9
|
+
`Lane | repo/cwd | exact decision | claimed files or contract | isolation path | authority | next gate | handoff | why independent`
|
|
10
|
+
|
|
11
|
+
Record the isolation path before the first mutation. Do not split one source seam or decision into duplicate lanes. Make overlapping work one lane with one source of truth.
|
|
12
|
+
|
|
13
|
+
For every lane, record the delivery target, allowed actions, required validation, and review rigor. For cross-repository work, name the shared contract and which repository changes first. A blocked decision is a lane state: record the owner, options, recommended default, and evidence needed to continue.
|
|
14
|
+
|
|
15
|
+
## Partitioned runs
|
|
16
|
+
|
|
17
|
+
Use one writer per repo/cwd or worktree. Mutation lanes need distinct isolation paths and explicit `cwd` values. Set `worktree: true` when a run needs managed worktree isolation within one repository. Read-only runs can share a checkout only when they cannot change state or create generated files.
|
|
18
|
+
|
|
19
|
+
For Pi extension repositories, keep lane worktrees outside auto-discovered extension directories such as `~/.pi/agent/extensions`. A stale extension worktree there can auto-load duplicate tools and shortcuts. Remove or move it only after its handoff is durable, the worktree is clean, and no run owns it.
|
|
20
|
+
|
|
21
|
+
Partition fanout by repository, source seam, decision, or review angle. Each run needs a stable key, lane-specific task, and durable output path. Do not launch prompts that differ only by item name or broad file glob.
|
|
22
|
+
|
|
23
|
+
Use one async `workflowScript` for a coordinated wave. Use `runs.all` for independent lanes and `runs.run` for dependent lane stages. Give cross-repository runs explicit `cwd` values and lane-qualified outputs. Use `outputMode: "file-only"` when a report must survive the run or feed a later stage.
|
|
24
|
+
|
|
25
|
+
## Keep independent work moving
|
|
26
|
+
|
|
27
|
+
While one lane waits, run safe independent preparation, validation, or fresh read-only review lanes. Do not block the parent just because a run is active. If no safe lane remains, record the blocker and the event that will reopen work.
|
|
28
|
+
|
|
29
|
+
An ordinary coordinated workflow has one mission. Use its durable state, artifacts, run records, and receipts for recovery. Treat a receipt as evidence, not as authority or acceptance.
|
|
30
|
+
|
|
31
|
+
After a writer produces a candidate, run the required fresh-context, read-only reviewer. The reviewer inspects the exact worktree and returns evidence-backed findings. The parent decides which findings are in scope and whether the lane is ready. Send accepted fixes to that lane's sole writer, then rerun only the affected gate.
|
|
32
|
+
|
|
33
|
+
## Handoff, cleanup, and recovery
|
|
34
|
+
|
|
35
|
+
Use stable lane-qualified paths for reports and review output. A handoff states the lane status, repository and worktree, changed files, validation, open decisions, next action, and artifact or receipt paths.
|
|
36
|
+
|
|
37
|
+
Keep a worktree until its handoff is durable, no run owns it, and no later gate needs it. Clean up only inside the recorded authority boundary. If a run stops or needs attention, preserve its worktree and artifacts, record the last known state and recovery owner, then resume that run or create one replacement lane from the handoff. Do not start another writer while worktree ownership is uncertain.
|
|
38
|
+
|
|
39
|
+
Before completion, inspect the board. Every lane must be terminal or blocked with a named next action. Confirm one writer per repo/cwd or worktree, required validation, required fresh read-only review, and a durable handoff. The parent reports outcomes, evidence, residual risks, and the next decision.
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Pi Subagents: Prompting And Roles
|
|
2
|
+
|
|
3
|
+
This file is a detailed reference loaded from `skills/subagents/SKILL.md`.
|
|
4
|
+
|
|
5
|
+
## Capability ceilings
|
|
6
|
+
|
|
7
|
+
Parent extensions may register a session-scoped, out-of-band ceiling through `unipi subagents/capability-ceiling`. Child tools and eligible canonical agent names are intersected with every active registration and inherited snapshot; `denyExtensions` removes ambient/provider extension loading while retaining package protocol runtime. `{ action: "list" }` marks non-allowlisted agents as restricted, and launch rejects them before spawn. Do not add a model-visible ceiling field or rely on unrestricted role selection for enforcement. Restricted schedules are rejected until their ceiling can be persisted safely.
|
|
8
|
+
|
|
9
|
+
## When to Use
|
|
10
|
+
|
|
11
|
+
- **Complex work orchestration**: use Fable mode as the default parent-agent loop for complex work. Complex means the task has multiple moving parts, unclear acceptance, cross-cutting code, meaningful user-visible impact, expensive or irreversible validation, broad review surface, or the user asks for orchestration. Lightweight one-off delegation can stay lightweight.
|
|
12
|
+
- **Advisory review**: use fresh-context `reviewer` agents for adversarial code review, or fork to `oracle` when inherited decisions and drift matter
|
|
13
|
+
- **Implementation handoff**: have `oracle` advise, then `worker` implement only after an approved direction
|
|
14
|
+
- **Recon and planning**: use `scout`, then write a plan when needed
|
|
15
|
+
- **Parallel exploration**: run multiple non-conflicting tasks concurrently
|
|
16
|
+
- **Regular skill specialists**: when discovery shows proactive skill subagent suggestions and the current work is broad enough, launch a small fresh-context fanout that asks one subagent per relevant regularly used skill to apply that skill's perspective to the task
|
|
17
|
+
- **Long-running work**: launch async/background runs and inspect them later. For mutation-capable work, bound the delivery slice and elapsed runtime, then request checkpoints after active tool work returns. Reserve hard turn and tool-call caps for explicitly read-only children.
|
|
18
|
+
- **Subagent control**: watch needs-attention signals and soft-interrupt only when a delegated run is genuinely blocked
|
|
19
|
+
- **Agent authoring**: create, update, or override project agents. Treat saved chain records as legacy inspection or migration inputs, not as a current authoring target.
|
|
20
|
+
|
|
21
|
+
## Tool vs Slash Commands
|
|
22
|
+
|
|
23
|
+
Agents use the `subagent(...)` tool with `workflowScript` for execution, and `action` for management, status, and control. Humans often use the slash-command layer instead:
|
|
24
|
+
|
|
25
|
+
- `/run` — launch a single agent
|
|
26
|
+
- `workflowScript` — the sole public surface for sequence, parallelism, branching, retries, and aggregation
|
|
27
|
+
- `/subagents` — interactive admin for inspecting agents and editing model, thinking, or system prompt
|
|
28
|
+
- `/subagents-stop [run-id]` — stop a current-session top-level async run; opens a selector when no id is given
|
|
29
|
+
- `/subagents-detach [run-id]` — detach an active foreground single-subagent run without terminating its child
|
|
30
|
+
- `/subagent-cost` — show parent plus child token usage and cost for the session
|
|
31
|
+
- `/subagents-fleet` — open the live fleet inspector with per-child controls; `Ctrl+Alt+F` opens it during an active foreground turn, `↑↓`/`jk` selects children, `PgUp`/`PgDn` scrolls transcript detail, `s` steers the selected live async child, and `D` stops its top-level async run after confirmation
|
|
32
|
+
- `/subagents-watchdog` — inspect or configure the opt-in adversarial change watchdog (model, on/off, recommend-model, check)
|
|
33
|
+
- `/subagents-doctor` — diagnose setup, discovery, async paths, and intercom bridge state
|
|
34
|
+
- `/subagents-models [agent]` — show the live runtime-loaded builtin model mapping
|
|
35
|
+
- `/subagents-profiles`, `/subagents-load-profile`, `/subagents-refresh-provider-models`, `/subagents-generate-profiles`, `/subagents-check-profile` — manage model profiles and provider catalogs
|
|
36
|
+
- `/prompt-workflow` — run a prompt template through native workflowScript execution
|
|
37
|
+
|
|
38
|
+
Prefer the tool when you are writing agent logic. Prefer the slash commands when
|
|
39
|
+
you are guiding a human through an interactive flow.
|
|
40
|
+
|
|
41
|
+
Packaged prompt shortcuts are also available for repeatable workflows. Treat them as reusable orchestration recipes, not just human slash commands. When the user asks for one of these shapes, or when the workflow clearly fits, apply the same pattern directly with `subagent(...)` and other tools:
|
|
42
|
+
- `/parallel-review` — fresh-context reviewers with distinct review angles, then synthesis
|
|
43
|
+
- `/review-loop` — parent-orchestrated worker, fresh-reviewer, and fix-worker cycles until clean or capped
|
|
44
|
+
- `/parallel-research` — combine `researcher` and `scout` for external evidence plus local code context
|
|
45
|
+
- `/gather-context-and-clarify` — scout/research first, then ask the user clarifying questions with `interview`
|
|
46
|
+
- `/parallel-cleanup` — two fresh-context reviewers (deslop + verbosity passes) for an adversarial cleanup review of the current diff
|
|
47
|
+
- `/council` — bounded advisor council for material decisions, plan critique, cross-exam, and parent-written decision memos
|
|
48
|
+
|
|
49
|
+
## Applying Prompt Techniques Without Slash Commands
|
|
50
|
+
|
|
51
|
+
The prompt templates in `prompts/` encode workflows the parent agent can run on demand. If the user provides a URL, issue, PR, plan, local file, screenshot, or freeform target, treat that target as the primary scope: read or fetch it before launching children, then include it explicitly in every child task. For targets outside the parent cwd, include the exact repository, explicit `cwd`, authority boundary, and expected output path in each child task. Do not depend on the parent conversation history when the recipe calls for fresh context.
|
|
52
|
+
|
|
53
|
+
### Council Mode technique
|
|
54
|
+
|
|
55
|
+
Use Council Mode when the user asks to convene advisors, debate a material decision, cross-examine recommendations, or critique and improve a plan with several model perspectives. This includes requests such as “run a council on this architecture,” “have Sol, Fable, and Kimi critique this plan,” or “get multiple oracles to debate the tradeoffs.” Read `../council-mode/SKILL.md` and follow its bounded parent-supervised protocol instead of launching ad hoc parallel oracle calls.
|
|
56
|
+
|
|
57
|
+
Council advisors are read-only. The parent assigns temporary roles such as architect, skeptic, operator, or product reviewer. User or project `council-*` profiles can pin models such as GPT 5.6 Sol, Fable, or Kimi while the `/council` request supplies the role for that run. The parent collects independent reports, optionally sends curated cross-exam packets, and writes the final memo. Do not treat the council as agent-to-agent chat, implementation authority, or a writer swarm.
|
|
58
|
+
|
|
59
|
+
### Parallel review technique
|
|
60
|
+
|
|
61
|
+
Use this when the user wants adversarial review of a diff, plan, issue, file, or implemented work. Launch fresh-context `reviewer` agents with distinct angles generated from the actual target. Common angles are correctness/regressions, tests/validation, and simplicity/maintainability; adapt for TypeScript, UI, security, docs, or large structural changes. Reviewers should inspect files and diffs directly, return concise evidence-backed findings with file/line references, and avoid edits unless the user explicitly asks for a writer pass. The parent synthesizes fixes worth doing now, optional improvements, and feedback to ignore/defer before applying anything.
|
|
62
|
+
|
|
63
|
+
### Proactive skill-specialist technique
|
|
64
|
+
|
|
65
|
+
Use this when `{ action: "list" }` reports proactive skill subagent suggestions and the user's task would benefit from perspectives the parent regularly uses. These suggestions are conservative: a skill is recommended only when it is available and referenced repeatedly by configured agents or saved chains. Treat the list as an opt-in hint for the current task, not a command to always fan out.
|
|
66
|
+
|
|
67
|
+
Default guardrails:
|
|
68
|
+
- Keep the fanout small: usually one or two skill-specialist children, never more than the listed recommendations or configured cap.
|
|
69
|
+
- Prefer `context: "fresh"` and include only the files, diff, plan, URL, or request details each child needs. Use forked context only when private/session history is essential and appropriate to share.
|
|
70
|
+
- Use read-only agents for analysis/review unless implementation was explicitly requested; do not create several writers in the same worktree.
|
|
71
|
+
- Skip proactive skill subagents for tiny questions, direct commands, highly private requests, or when the user asks not to delegate.
|
|
72
|
+
- Make cost and concurrency visible by using an ordinary `subagent(...)` call rather than hidden/background automation.
|
|
73
|
+
|
|
74
|
+
Example shape:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
subagent({
|
|
78
|
+
workflowScript: `
|
|
79
|
+
const results = await runs.all([
|
|
80
|
+
{ key: "deslop", agent: "reviewer", task: "Apply the available 'deslop' skill to review the current diff for concrete cleanup findings only. Do not modify files.", skill: "deslop" },
|
|
81
|
+
{ key: "accessibility", agent: "reviewer", task: "Apply the available 'accessibility' skill to review the UI changes for concrete issues only. Do not modify files.", skill: "accessibility" }
|
|
82
|
+
]);
|
|
83
|
+
return results.map(result => result.output);
|
|
84
|
+
`,
|
|
85
|
+
context: "fresh"
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Review-loop technique
|
|
90
|
+
|
|
91
|
+
Use this when the user wants implementation or current diff review to continue until reviewers stop finding fixes worth doing now. Keep the loop in the parent session: one async `worker` implements or fixes, fresh-context `reviewer` agents inspect the actual repo and diff, the parent synthesizes accepted fixes, and one async forked `worker` applies them. The parent can express the sequence up front as an async/background `workflowScript` when the workflow is known, or continue with explicit follow-up workflowScript runs after each async completion. For an initial workflow, pass `async: true` so the main chat is unblocked. Treat an async implementation worker handoff as an intermediate state, not final completion, unless the user explicitly asked for worker-only work, review-only output, or to stop after implementation. Stop when reviewers find no blockers or fixes worth doing now, remaining feedback is optional or deferred, an unapproved product/scope/architecture decision appears, or the max review-round cap is reached. Default to 3 review rounds unless the user sets a different cap. Do not loop for optional polish, and do not let children launch subagents or decide the loop outcome.
|
|
92
|
+
|
|
93
|
+
As a conservative orchestration policy, do not pass `turnBudget` or a hard `toolBudget` to an implementation worker, fix worker, reviewer with edit authority, or other mutation-capable child. The default tool budget blocks read/search tools rather than mutation tools, but count limits still do not measure delivery safety. Use a narrow task plus an outer elapsed deadline with enough margin, then request a checkpoint after the current tool returns. The checkpoint should report changed files, build/test state, remaining work, and commit or PR state. An elapsed timeout is not a mutation-safe boundary and must not be used as the checkpoint trigger.
|
|
94
|
+
|
|
95
|
+
### Parallel research technique
|
|
96
|
+
|
|
97
|
+
Use this when the question needs both external evidence and local implications. Combine `researcher` for official docs, specs, ecosystem behavior, recent changes, benchmarks, and primary sources with `scout` for repository files, patterns, constraints, tests, and likely integration points. Give each child a distinct angle: external evidence, local code context, and practical tradeoffs. Ask for source links or file ranges, confidence level, gaps, and decision implications. Do not ask these children to edit unless implementation was explicitly requested.
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
### Gather-context-and-clarify technique
|
|
102
|
+
|
|
103
|
+
Use this at the start of non-trivial work. Launch `scout` for local context and `researcher` only when external docs, recent sources, ecosystem context, or primary evidence would materially improve understanding. Ask children for concise findings plus remaining clarification questions. Then synthesize what is known and use `interview` to ask the unresolved questions needed for shared understanding before planning or implementing.
|
|
104
|
+
|
|
105
|
+
### Parallel cleanup technique
|
|
106
|
+
|
|
107
|
+
Use this after implementation when the user wants cleanup review or when a final pass would reduce AI-slop. Launch two fresh-context `reviewer` tasks with `output: false` and `progress: false`: one deslop pass and one verbosity pass. If the `deslop` or `verbosity-cleaner` skills are available, pass the relevant skill to that reviewer; otherwise inline the criteria. Both reviewers are review-only and should flag concrete issues with severity, file/line references, and smallest safe fixes. Phrase the constraint as “Do not modify project/source files; returning findings through the configured output artifact is allowed” when you use `output` or `outputMode: "file-only"`. The parent decides what to apply and asks before making changes unless cleanup was already authorized.
|
|
108
|
+
|
|
109
|
+
### Staged fix orchestration technique
|
|
110
|
+
|
|
111
|
+
Use this when a broad diff has known reviewer findings across several items and the user wants the parent to “orchestrate subagents like a boss.” Keep the active worktree safe with a three-stage `workflowScript`:
|
|
112
|
+
|
|
113
|
+
1. A parallel read-only planning fanout, one reviewer per issue cluster. Each child inspects the real diff and returns exact files, line refs, proposed fixes, and focused validation. They must not edit.
|
|
114
|
+
2. One writer worker. It receives the reviewer summaries as the awaited planning results (or their durable output paths) interpolated into its task, plus the parent’s accepted scope, stop rules, and verification contract. It is the only child allowed to edit the active worktree.
|
|
115
|
+
3. A parallel read-only validation fanout. Validators inspect the worker diff from fresh context with distinct angles, report pass/fail, remaining blockers, and missing verification.
|
|
116
|
+
|
|
117
|
+
Prefer `async: true`, `context: "fresh"` for reviewers/validators, `outputMode: "file-only"` for large summaries, and per-stage output names that will not collide. Use stable `runs` keys plus `phase` and `label` on each launch item to make async status readable, and hold each awaited result in an ordinary JavaScript variable when a later step needs that specific result — interpolate it (or the durable output path you declared for that child) into the later task text instead of passing a whole aggregate blob. Use this pattern instead of launching several writer workers into a dirty worktree. Include non-blocking suggestions in the writer prompt only when they are small, safe, and do not expand product scope; otherwise record them as deferred.
|
|
118
|
+
|
|
119
|
+
When one child returns a structured target list, use ordinary JavaScript to validate/filter it and map bounded entries into `runs.all`; do not use the removed chain fanout DSL.
|
|
120
|
+
|
|
121
|
+
Example shape:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
subagent({
|
|
125
|
+
async: true,
|
|
126
|
+
context: "fresh",
|
|
127
|
+
workflowScript: `
|
|
128
|
+
// Stage 1: parallel read-only planning fanout (stable keys, one per issue cluster)
|
|
129
|
+
const plans = await runs.all([
|
|
130
|
+
{ key: "deploy-plan", agent: "reviewer", phase: "Planning", label: "Deploy docs", task: "Plan fixes for deploy docs/workflow. Inspect the current diff. Do not modify project/source files; returning findings via the configured output artifact is allowed.", output: "plans/deploy.md", outputMode: "file-only" },
|
|
131
|
+
{ key: "scheduler-plan", agent: "reviewer", phase: "Planning", label: "Scheduler contract", task: "Plan fixes for scheduler contract. Inspect the current diff. Do not modify project/source files; returning findings via the configured output artifact is allowed.", output: "plans/scheduler.md", outputMode: "file-only" },
|
|
132
|
+
{ key: "sandbox-plan", agent: "reviewer", phase: "Planning", label: "Sandbox/security", task: "Plan fixes for sandbox/security. Inspect the current diff. Do not modify project/source files; returning findings via the configured output artifact is allowed.", output: "plans/sandbox.md", outputMode: "file-only" }
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
// Stage 2: single writer — the only child allowed to edit the active worktree.
|
|
136
|
+
// Under outputMode "file-only" the awaited .output is the saved-output
|
|
137
|
+
// reference, so pass the durable paths declared above to the writer.
|
|
138
|
+
const worker = await runs.run("apply-fixes", {
|
|
139
|
+
agent: "worker",
|
|
140
|
+
phase: "Implementation",
|
|
141
|
+
label: "Apply accepted fixes",
|
|
142
|
+
task: "Apply only the accepted fixes from these planning summaries. You are the sole writer for the active worktree. Run focused validation and report changed files, commands, failures, and remaining issues.\\n\\nDeploy plan: plans/deploy.md\\n\\nScheduler plan: plans/scheduler.md\\n\\nSandbox plan: plans/sandbox.md",
|
|
143
|
+
output: "worker/fixes.md",
|
|
144
|
+
outputMode: "file-only"
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Stage 3: parallel read-only validation fanout
|
|
148
|
+
const validations = await runs.all([
|
|
149
|
+
{ key: "validate-deploy-scheduler", agent: "reviewer", phase: "Validation", label: "Deploy/scheduler validation", task: "Validate the post-worker diff for deploy and scheduler fixes. Start from the worker result: " + worker.output + " (also worker/fixes.md). Do not modify project/source files; returning findings via the configured output artifact is allowed.", output: "validation/deploy-scheduler.md", outputMode: "file-only" },
|
|
150
|
+
{ key: "validate-sandbox", agent: "reviewer", phase: "Validation", label: "Sandbox validation", task: "Validate the post-worker diff for sandbox/security fixes. Start from the worker result: " + worker.output + " (also worker/fixes.md). Do not modify project/source files; returning findings via the configured output artifact is allowed.", output: "validation/sandbox.md", outputMode: "file-only" }
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
return { worker: worker.output, validations: validations.map(v => v.output) };
|
|
154
|
+
`
|
|
155
|
+
})
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Builtin Agents
|
|
159
|
+
|
|
160
|
+
Builtin agents load at the lowest priority. Project agents override user agents,
|
|
161
|
+
and user/project agents override builtins with the same name.
|
|
162
|
+
|
|
163
|
+
| Agent | Purpose | Model | Typical output / role |
|
|
164
|
+
|-------|---------|-------|------------------------|
|
|
165
|
+
| `scout` | Fast codebase recon | inherits default | Writes `context.md` handoff material |
|
|
166
|
+
| `worker` | Implementation and approved oracle handoffs | inherits default | Single-writer implementation with decision escalation |
|
|
167
|
+
| `reviewer` | Review specialist | inherits default | Default recipes are review-only; tools include edit/write when a fix pass is explicit |
|
|
168
|
+
| `researcher` | Web research brief generator | inherits default | Writes `research.md` |
|
|
169
|
+
| `delegate` | Lightweight generic delegate | inherits default | No fixed output; generic delegated work |
|
|
170
|
+
| `oracle` | Decision-consistency advisory review | inherits default | Advisory review, intercom coordination |
|
|
171
|
+
| `advisor` | Claude Code-compatible alias for `oracle` | inherits default | Same advisory role as `oracle` |
|
|
172
|
+
|
|
173
|
+
Builtin `worker` and `delegate` use strict tool allowlists and do not inherit ambient parent extension tools. To give a child an extension tool, name it in `tools` and load its provider via `extensions`, a path-like `tools` entry, or `subagentOnlyExtensions`. Custom agents without an `extensions` field follow `subagents.defaultExtensions` when set.
|
|
174
|
+
|
|
175
|
+
Builtin agents inherit the current Pi default model unless a run, user setting, project setting, or `subagents.defaultModel` overrides `model`. Set `subagents.defaultModel` when subagents should use a different default model than the parent session. Override builtin defaults before copying full agent files when a small tweak is enough.
|
|
176
|
+
|
|
177
|
+
Set `subagents.defaultThinking` to apply a shared thinking level to builtin, package, user, and project agents whose frontmatter leaves `thinking` unset. Project settings win over user settings; explicit frontmatter (including `thinking: false`), `agentOverrides.<name>.thinking`, and per-run overrides remain more specific. This setting affects child agents only and does not change the parent session's default thinking level.
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"subagents": {
|
|
182
|
+
"defaultThinking": "medium"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
For one run, use inline config:
|
|
188
|
+
|
|
189
|
+
```text
|
|
190
|
+
/run reviewer[model=anthropic/claude-sonnet-4] "Review this diff"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
For persistent tweaks, edit `subagents.agentOverrides` in user or project settings. User overrides apply everywhere. Project overrides apply only in that repo and win over user overrides. Use `/subagents-models` or `subagent({ action: "models" })` to inspect the live mapping after settings and overrides load.
|
|
194
|
+
|
|
195
|
+
Model ids do not have to be exact. Separator variations (`claude-haiku-4.5` vs `claude-haiku-4-5`), case (`Claude-Sonnet-4`), and optional trailing date stamps (`claude-haiku-4-5-20251001`) all resolve to the same registry model. Exact `provider/id` wins; a qualified `provider/model` never switches providers. To constrain subagents to a budget or compliance profile, set `subagents.modelScope: { enforce: true, allow: ["anthropic/*", "openai/gpt-5-*"] }` in user or project settings. Out-of-scope models you pass explicitly error and abort; models inherited from frontmatter, `subagents.defaultModel`, agent frontmatter, or the parent session only warn.
|
|
196
|
+
|
|
197
|
+
For model fleets, use the profile commands instead of hand-editing repeated overrides: `/subagents-refresh-provider-models <provider>`, `/subagents-generate-profiles <provider>`, `/subagents-load-profile <name>`, and `/subagents-check-profile <name>`. Profiles live under `~/.pi/agent/profiles/unipi subagents/` and replace only `settings.subagents` when loaded.
|
|
198
|
+
|
|
199
|
+
## Prompting role subagents
|
|
200
|
+
|
|
201
|
+
Builtin role agents inherit the current Pi default model unless you override them. When launching them, write the task prompt as a compact contract, not a long procedural script. Define the destination and let the role choose the efficient path.
|
|
202
|
+
|
|
203
|
+
A strong subagent prompt usually includes:
|
|
204
|
+
- **Goal**: the concrete outcome the child should produce.
|
|
205
|
+
- **Target**: repository, explicit `cwd`, branch/ref/head, and source seam when the target is not the parent cwd.
|
|
206
|
+
- **Authority boundary**: whether the child may read, edit, commit, push, comment, close, merge, publish, or release. Omit or forbid actions that are not approved.
|
|
207
|
+
- **Context/evidence**: relevant plan paths, files, diffs, decisions, or user constraints already approved.
|
|
208
|
+
- **Success criteria**: what must be true before the child can finish.
|
|
209
|
+
- **Hard constraints**: true invariants only, such as no edits for review-only tasks, one writer thread, child must not run subagents unless it is an explicitly assigned `tools: subagent` fanout child, or escalation for unapproved decisions.
|
|
210
|
+
- **Validation**: targeted checks to run, or the next-best check when validation is impossible.
|
|
211
|
+
- **Output**: the expected summary shape, artifact path, or finding format. Use repo-qualified durable output paths for cross-codebase waves.
|
|
212
|
+
- **Stop rules**: when to ask via `intercom` or `contact_supervisor`, when to stop after enough evidence, and when not to keep searching.
|
|
213
|
+
|
|
214
|
+
Give each role useful discovery anchors. Name source roots, filenames, symbols, types, methods, and paths for scouts. Give workers context files, plans, task paths, and named source seams before asking them to search. Give reviewers changed files, contracts, and any exhaustive-verification target. Tell oracle whether current source behavior, product/policy documents, plans, or inherited decisions are the evidence that matters.
|
|
215
|
+
|
|
216
|
+
Avoid carrying over old prompt habits that over-specify every step. Use `must`, `always`, and `never` for real invariants; for judgment calls, give decision rules. For example, tell a reviewer to inspect the staged diff directly and report only evidence-backed findings, rather than prescribing every file or command. Tell a researcher the retrieval budget: start with broad targeted searches, fetch only the strongest sources, search again only when a required fact is missing, then stop.
|
|
217
|
+
|
|
218
|
+
For implementation handoffs, name the approved scope and success criteria more clearly than the process. Good prompts say what to change, what not to change, where the evidence lives, how to validate, and when to escalate. They should not ask the child to create another subagent plan or continue the parent conversation.
|
|
219
|
+
|
|
220
|
+
Settings locations:
|
|
221
|
+
- User scope: `~/.pi/agent/settings.json`
|
|
222
|
+
- Project scope: `.pi/settings.json`
|
|
223
|
+
|
|
224
|
+
Direct settings example:
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"subagents": {
|
|
229
|
+
"agentOverrides": {
|
|
230
|
+
"reviewer": {
|
|
231
|
+
"model": "anthropic/claude-sonnet-4",
|
|
232
|
+
"thinking": "high",
|
|
233
|
+
"fallbackModels": ["openai/gpt-5-mini"],
|
|
234
|
+
"acceptanceRole": "read-only"
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Useful override fields: `description`, `model`, `fallbackModels`, `thinking`,
|
|
242
|
+
`systemPromptMode`, `inheritProjectContext`, `inheritSkills`, `defaultContext`,
|
|
243
|
+
`acceptanceRole`, `disabled`, `skills`, `tools`, `extensions`, and `systemPrompt`.
|
|
244
|
+
`description` replaces the discovered description for builtin and custom agents
|
|
245
|
+
in `list` output, which is useful for deployment-specific routing notes.
|
|
246
|
+
Use `acceptanceRole: false` to clear an override. Create a user or project
|
|
247
|
+
agent with the same name only when you want a substantially different agent.
|
|
248
|
+
|
|
249
|
+
### Recommended model tiering (optional)
|
|
250
|
+
|
|
251
|
+
When several providers are available, route agents by task shape instead of one model for everything:
|
|
252
|
+
|
|
253
|
+
1. **Fast workhorse** — cheapest capable model at low thinking for recon, lookups, and mechanical edits (for example on `scout`).
|
|
254
|
+
2. **Standard well-scoped** — mid-tier model at medium thinking for most delegations: routine multi-file edits, focused reviews, straightforward implementation (for example on `worker`, `reviewer`, `delegate`).
|
|
255
|
+
3. **Deep but bounded** — top reasoning model at high thinking only for hard tasks that arrive with explicit goals and completion criteria; these models loop on vague goals (for example on oracle-style agents).
|
|
256
|
+
4. **Taste and intent** — a model that reads human intent well for ambiguous work: UX/design judgment, product tradeoffs, planning from vague requirements, writing quality.
|
|
257
|
+
|
|
258
|
+
Routing rule: use tiers 1–3 when the task is well-scoped; use tier 4 when scoping or judging is the task itself. Give tier-4 agents cross-provider `fallbackModels` so subscription usage limits degrade gracefully; fallback triggers automatically on rate-limit and overload errors. Note that forked context over an Anthropic parent transcript with signed thinking blocks forces the child's thinking off, so intent-tier agents work best with fresh context.
|
|
259
|
+
|
|
260
|
+
If a provider rejects model IDs with thinking suffixes, use
|
|
261
|
+
`subagents.disableThinking: true` in user or project settings to clear bundled
|
|
262
|
+
builtin thinking defaults globally. A higher-precedence per-agent `thinking`
|
|
263
|
+
override can opt one builtin back in. Existing custom-agent frontmatter remains authoritative.
|
|
264
|
+
|
|
265
|
+
Set `subagents.defaultExtensions` to give agents without an `extensions` field a shared child extension allowlist. Omit it to preserve ambient extension discovery, set it to `[]` to disable ambient extensions by default, or use `agentOverrides.<name>.extensions` for one agent. Explicit custom-agent frontmatter still wins.
|
|
266
|
+
|
|
267
|
+
Tool description modes live in `~/.pi/agent/extensions/subagent/config.json`, not `subagents` settings. The default uses split prompt metadata: a short tool description plus active `promptSnippet` and `promptGuidelines`. Set `toolDescriptionMode` to `full` or `compact` to force one description string, or `custom` to read `subagent-tool-description.md` from the project config dir or agent dir; invalid custom files fall back to full mode and the safety guidance is still appended.
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/subagents — Acceptance gates
|
|
3
|
+
*
|
|
4
|
+
* Ported from pi-subagents src/runs/shared/acceptance.ts (core semantics):
|
|
5
|
+
* levels auto/none/attested/checked/verified, evidence kinds, criteria gates,
|
|
6
|
+
* verify commands run at the host, structured report parsing from child
|
|
7
|
+
* output, and the ledger (status/evidenceStatus/runtimeChecks/verifyRuns).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
export const ACCEPTANCE_LEVELS = ["auto", "none", "attested", "checked", "verified"] as const;
|
|
13
|
+
export type AcceptanceLevel = (typeof ACCEPTANCE_LEVELS)[number];
|
|
14
|
+
|
|
15
|
+
export const ACCEPTANCE_EVIDENCE_KINDS = [
|
|
16
|
+
"changed-files",
|
|
17
|
+
"tests-added",
|
|
18
|
+
"commands-run",
|
|
19
|
+
"validation-output",
|
|
20
|
+
"residual-risks",
|
|
21
|
+
"no-staged-files",
|
|
22
|
+
"diff-summary",
|
|
23
|
+
"review-findings",
|
|
24
|
+
"manual-notes",
|
|
25
|
+
] as const;
|
|
26
|
+
export type AcceptanceEvidenceKind = (typeof ACCEPTANCE_EVIDENCE_KINDS)[number];
|
|
27
|
+
|
|
28
|
+
const LEVEL_RANK: Record<AcceptanceLevel, number> = {
|
|
29
|
+
none: 0,
|
|
30
|
+
auto: 1,
|
|
31
|
+
attested: 1,
|
|
32
|
+
checked: 2,
|
|
33
|
+
verified: 3,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export interface AcceptanceCriterion {
|
|
37
|
+
id: string;
|
|
38
|
+
must: string;
|
|
39
|
+
evidence?: AcceptanceEvidenceKind[];
|
|
40
|
+
severity?: "required" | "recommended";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface VerifyCommand {
|
|
44
|
+
id: string;
|
|
45
|
+
command: string;
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface AcceptanceConfig {
|
|
50
|
+
level: AcceptanceLevel;
|
|
51
|
+
reason?: string;
|
|
52
|
+
criteria?: AcceptanceCriterion[];
|
|
53
|
+
evidence?: AcceptanceEvidenceKind[];
|
|
54
|
+
verify?: VerifyCommand[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AcceptanceLedger {
|
|
58
|
+
status: "not-required" | "claimed" | "attested" | "checked" | "verified" | "rejected";
|
|
59
|
+
evidenceStatus: string;
|
|
60
|
+
effectiveAcceptance: AcceptanceConfig;
|
|
61
|
+
criteria: AcceptanceCriterion[];
|
|
62
|
+
runtimeChecks: Array<{ id: string; status: "passed" | "failed"; message: string }>;
|
|
63
|
+
verifyRuns: Array<{ id: string; command: string; status: "passed" | "failed" | "timed-out"; output?: string }>;
|
|
64
|
+
failureMessage?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Validation (reference error strings)
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
/** Validate an acceptance input; returns error strings (empty = valid). */
|
|
72
|
+
export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"): string[] {
|
|
73
|
+
const errors: string[] = [];
|
|
74
|
+
if (input === undefined || input === false) return errors;
|
|
75
|
+
if (typeof input === "string") {
|
|
76
|
+
if (!ACCEPTANCE_LEVELS.includes(input as AcceptanceLevel)) {
|
|
77
|
+
errors.push(`${pathLabel} has invalid level '${input}'.`);
|
|
78
|
+
} else if (input === "none") {
|
|
79
|
+
errors.push(`${pathLabel} level "none" requires a reason; use { level: "none", reason: "..." }.`);
|
|
80
|
+
} else if (input === "verified") {
|
|
81
|
+
errors.push(`${pathLabel} level "verified" requires object form with at least one runtime verify command. Use level "checked" or provide a non-empty acceptance.verify array.`);
|
|
82
|
+
}
|
|
83
|
+
return errors;
|
|
84
|
+
}
|
|
85
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
86
|
+
errors.push(`${pathLabel} must be a string level, false, or an object.`);
|
|
87
|
+
return errors;
|
|
88
|
+
}
|
|
89
|
+
const value = input as Record<string, unknown>;
|
|
90
|
+
const allowedKeys = new Set(["level", "reason", "criteria", "evidence", "verify"]);
|
|
91
|
+
for (const key of Object.keys(value)) {
|
|
92
|
+
if (!allowedKeys.has(key)) errors.push(`${pathLabel}.${key} is not supported.`);
|
|
93
|
+
}
|
|
94
|
+
if (value.level !== undefined && (typeof value.level !== "string" || !ACCEPTANCE_LEVELS.includes(value.level as AcceptanceLevel))) {
|
|
95
|
+
errors.push(`${pathLabel}.level must be one of ${ACCEPTANCE_LEVELS.join(", ")}.`);
|
|
96
|
+
}
|
|
97
|
+
if (value.level === "none" && (typeof value.reason !== "string" || !(value.reason as string).trim())) {
|
|
98
|
+
errors.push(`${pathLabel}.reason is required when level is none.`);
|
|
99
|
+
}
|
|
100
|
+
if (value.criteria !== undefined && !Array.isArray(value.criteria)) errors.push(`${pathLabel}.criteria must be an array.`);
|
|
101
|
+
if (Array.isArray(value.criteria)) {
|
|
102
|
+
const ids = new Set<string>();
|
|
103
|
+
for (const [index, criterion] of value.criteria.entries()) {
|
|
104
|
+
const criterionPath = `${pathLabel}.criteria[${index}]`;
|
|
105
|
+
if (!criterion || typeof criterion !== "object" || Array.isArray(criterion)) {
|
|
106
|
+
errors.push(`${criterionPath} must be a string or an object.`);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const gate = criterion as Record<string, unknown>;
|
|
110
|
+
if (typeof gate.id !== "string" || !gate.id.trim()) errors.push(`${criterionPath}.id is required.`);
|
|
111
|
+
else {
|
|
112
|
+
const normalized = gate.id.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
113
|
+
if (ids.has(normalized)) errors.push(`${criterionPath}.id duplicates normalized criterion id '${normalized}'.`);
|
|
114
|
+
ids.add(normalized);
|
|
115
|
+
}
|
|
116
|
+
if (typeof gate.must !== "string" || !gate.must.trim()) errors.push(`${criterionPath}.must is required.`);
|
|
117
|
+
if (gate.evidence !== undefined && !Array.isArray(gate.evidence)) {
|
|
118
|
+
errors.push(`${criterionPath}.evidence must be an array.`);
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(gate.evidence)) {
|
|
121
|
+
for (const item of gate.evidence) {
|
|
122
|
+
if (typeof item !== "string" || !ACCEPTANCE_EVIDENCE_KINDS.includes(item as AcceptanceEvidenceKind)) {
|
|
123
|
+
errors.push(`${criterionPath}.evidence contains unsupported kind '${String(item)}'.`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (gate.severity !== undefined && gate.severity !== "required" && gate.severity !== "recommended") {
|
|
128
|
+
errors.push(`${criterionPath}.severity must be required or recommended.`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (value.level === "verified" && (!Array.isArray(value.verify) || value.verify.length === 0)) {
|
|
133
|
+
errors.push(`${pathLabel}.verify must contain at least one runtime command when level is verified.`);
|
|
134
|
+
} else if (value.verify !== undefined && !Array.isArray(value.verify)) {
|
|
135
|
+
errors.push(`${pathLabel}.verify must be an array.`);
|
|
136
|
+
}
|
|
137
|
+
if (Array.isArray(value.verify)) {
|
|
138
|
+
for (const [index, command] of value.verify.entries()) {
|
|
139
|
+
if (!command || typeof command !== "object" || Array.isArray(command)) {
|
|
140
|
+
errors.push(`${pathLabel}.verify[${index}] must be an object with id and command.`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const cmd = command as Record<string, unknown>;
|
|
144
|
+
if (typeof cmd.id !== "string" || !cmd.id.trim()) errors.push(`${pathLabel}.verify[${index}].id is required.`);
|
|
145
|
+
if (typeof cmd.command !== "string" || !cmd.command.trim()) errors.push(`${pathLabel}.verify[${index}].command is required.`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return errors;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Normalize shorthand forms into a full config. */
|
|
152
|
+
export function normalizeAcceptanceInput(input: unknown): AcceptanceConfig | undefined {
|
|
153
|
+
if (input === undefined || input === false) return undefined;
|
|
154
|
+
if (typeof input === "string") {
|
|
155
|
+
return { level: input as AcceptanceLevel };
|
|
156
|
+
}
|
|
157
|
+
const value = input as Record<string, unknown>;
|
|
158
|
+
return {
|
|
159
|
+
level: (value.level as AcceptanceLevel) ?? "auto",
|
|
160
|
+
...(typeof value.reason === "string" ? { reason: value.reason } : {}),
|
|
161
|
+
...(Array.isArray(value.criteria) ? { criteria: value.criteria as AcceptanceCriterion[] } : {}),
|
|
162
|
+
...(Array.isArray(value.evidence) ? { evidence: value.evidence as AcceptanceEvidenceKind[] } : {}),
|
|
163
|
+
...(Array.isArray(value.verify) ? { verify: value.verify as VerifyCommand[] } : {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Gate shorthand: one host-run verification command. */
|
|
168
|
+
export function normalizeGateAcceptance(gate: unknown): AcceptanceConfig | undefined {
|
|
169
|
+
if (typeof gate !== "string" || !gate.trim()) return undefined;
|
|
170
|
+
return {
|
|
171
|
+
level: "verified",
|
|
172
|
+
verify: [{ id: "gate", command: gate.trim() }],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// Report parsing + evaluation
|
|
178
|
+
// ============================================================================
|
|
179
|
+
|
|
180
|
+
export interface AcceptanceReport {
|
|
181
|
+
criteria?: Array<{ id: string; satisfied: boolean; notes?: string }>;
|
|
182
|
+
evidence?: Partial<Record<AcceptanceEvidenceKind, string>>;
|
|
183
|
+
residualRisks?: string[];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const REPORT_OPEN = "<acceptance-report>";
|
|
187
|
+
const REPORT_CLOSE = "</acceptance-report>";
|
|
188
|
+
|
|
189
|
+
/** Parse the structured acceptance report from child output. */
|
|
190
|
+
export function parseAcceptanceReport(output: string): { report?: AcceptanceReport; error?: string } {
|
|
191
|
+
const start = output.indexOf(REPORT_OPEN);
|
|
192
|
+
const end = output.indexOf(REPORT_CLOSE);
|
|
193
|
+
if (start === -1 || end === -1 || end < start) {
|
|
194
|
+
return { error: "Structured acceptance report not found." };
|
|
195
|
+
}
|
|
196
|
+
const body = output.slice(start + REPORT_OPEN.length, end).trim();
|
|
197
|
+
try {
|
|
198
|
+
const parsed = JSON.parse(body) as AcceptanceReport;
|
|
199
|
+
if (!parsed || typeof parsed !== "object") throw new Error("not an object");
|
|
200
|
+
return { report: parsed };
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return { error: `Invalid acceptance-report JSON: ${error instanceof Error ? error.message : String(error)}` };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Strip the report block from output shown to the parent. */
|
|
207
|
+
export function stripAcceptanceReport(output: string): string {
|
|
208
|
+
const start = output.indexOf(REPORT_OPEN);
|
|
209
|
+
const end = output.indexOf(REPORT_CLOSE);
|
|
210
|
+
if (start === -1 || end === -1) return output;
|
|
211
|
+
return (output.slice(0, start) + output.slice(end + REPORT_CLOSE.length)).trim();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function checkCriteriaSatisfied(
|
|
215
|
+
criteria: AcceptanceCriterion[],
|
|
216
|
+
report: AcceptanceReport,
|
|
217
|
+
): Array<{ id: string; status: "passed" | "failed"; message: string }> {
|
|
218
|
+
const checks: Array<{ id: string; status: "passed" | "failed"; message: string }> = [];
|
|
219
|
+
const reported = new Map((report.criteria ?? []).map((c) => [c.id, c]));
|
|
220
|
+
for (const criterion of criteria) {
|
|
221
|
+
const match = reported.get(criterion.id);
|
|
222
|
+
if (!match) {
|
|
223
|
+
checks.push({
|
|
224
|
+
id: `criterion:${criterion.id}`,
|
|
225
|
+
status: criterion.severity === "recommended" ? "passed" : "failed",
|
|
226
|
+
message: `Criterion '${criterion.id}' missing from child report.`,
|
|
227
|
+
});
|
|
228
|
+
} else if (!match.satisfied && criterion.severity !== "recommended") {
|
|
229
|
+
checks.push({
|
|
230
|
+
id: `criterion:${criterion.id}`,
|
|
231
|
+
status: "failed",
|
|
232
|
+
message: `Criterion '${criterion.id}' not satisfied${match.notes ? `: ${match.notes}` : "."}`,
|
|
233
|
+
});
|
|
234
|
+
} else {
|
|
235
|
+
checks.push({ id: `criterion:${criterion.id}`, status: "passed", message: criterion.must });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return checks;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Evaluate acceptance: parse the child's report, check criteria, run verify
|
|
243
|
+
* commands at the host, and produce the ledger.
|
|
244
|
+
*/
|
|
245
|
+
export async function evaluateAcceptance(input: {
|
|
246
|
+
acceptance: AcceptanceConfig;
|
|
247
|
+
output: string;
|
|
248
|
+
cwd: string;
|
|
249
|
+
signal?: AbortSignal;
|
|
250
|
+
}): Promise<AcceptanceLedger> {
|
|
251
|
+
const acceptance = input.acceptance;
|
|
252
|
+
const ledger: AcceptanceLedger = {
|
|
253
|
+
status: acceptance.level === "none" ? "not-required" : "claimed",
|
|
254
|
+
evidenceStatus: acceptance.level === "none" ? "not-required" : "claimed",
|
|
255
|
+
effectiveAcceptance: acceptance,
|
|
256
|
+
criteria: acceptance.criteria ?? [],
|
|
257
|
+
runtimeChecks: [],
|
|
258
|
+
verifyRuns: [],
|
|
259
|
+
};
|
|
260
|
+
if (acceptance.level === "none") return ledger;
|
|
261
|
+
|
|
262
|
+
// Attestation: structured report present? Gate-shorthand configs (verify
|
|
263
|
+
// commands without criteria) skip the report requirement — the host command
|
|
264
|
+
// IS the verification.
|
|
265
|
+
const gateOnly = (acceptance.verify?.length ?? 0) > 0 && (acceptance.criteria?.length ?? 0) === 0;
|
|
266
|
+
const parsed = parseAcceptanceReport(input.output);
|
|
267
|
+
if (parsed.report) {
|
|
268
|
+
ledger.status = "attested";
|
|
269
|
+
ledger.evidenceStatus = "attested";
|
|
270
|
+
} else if (!gateOnly && (LEVEL_RANK[acceptance.level] >= LEVEL_RANK.checked || acceptance.level === "attested")) {
|
|
271
|
+
ledger.runtimeChecks.push({ id: "attestation", status: "failed", message: parsed.error ?? "Structured acceptance report missing." });
|
|
272
|
+
ledger.status = "rejected";
|
|
273
|
+
ledger.evidenceStatus = "rejected";
|
|
274
|
+
ledger.failureMessage = `Acceptance rejected: ${parsed.error}`;
|
|
275
|
+
return ledger;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Criteria checks at checked+.
|
|
279
|
+
if (parsed.report && LEVEL_RANK[acceptance.level] >= LEVEL_RANK.checked) {
|
|
280
|
+
ledger.runtimeChecks = checkCriteriaSatisfied(ledger.criteria, parsed.report);
|
|
281
|
+
if (ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
282
|
+
ledger.status = "rejected";
|
|
283
|
+
ledger.evidenceStatus = "rejected";
|
|
284
|
+
ledger.failureMessage = `Acceptance rejected: ${ledger.runtimeChecks.find((c) => c.status === "failed")!.message}`;
|
|
285
|
+
return ledger;
|
|
286
|
+
}
|
|
287
|
+
ledger.status = "checked";
|
|
288
|
+
ledger.evidenceStatus = "checked";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Runtime verify commands at verified+.
|
|
292
|
+
if (LEVEL_RANK[acceptance.level] >= LEVEL_RANK.verified && (acceptance.level === "verified" || (acceptance.verify?.length ?? 0) > 0)) {
|
|
293
|
+
for (const command of acceptance.verify ?? []) {
|
|
294
|
+
if (input.signal?.aborted) break;
|
|
295
|
+
try {
|
|
296
|
+
const output = execSync(command.command, {
|
|
297
|
+
cwd: input.cwd,
|
|
298
|
+
encoding: "utf8",
|
|
299
|
+
timeout: command.timeoutMs ?? 120_000,
|
|
300
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
301
|
+
});
|
|
302
|
+
ledger.verifyRuns.push({ id: command.id, command: command.command, status: "passed", output: output.slice(-2000) });
|
|
303
|
+
} catch (error) {
|
|
304
|
+
const err = error as { status?: number; message?: string; killed?: boolean };
|
|
305
|
+
ledger.verifyRuns.push({
|
|
306
|
+
id: command.id,
|
|
307
|
+
command: command.command,
|
|
308
|
+
status: err.killed ? "timed-out" : "failed",
|
|
309
|
+
output: (err.message ?? "").slice(-2000),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (ledger.verifyRuns.some((run) => run.status === "failed" || run.status === "timed-out")) {
|
|
314
|
+
const failed = ledger.verifyRuns.find((r) => r.status !== "passed")!;
|
|
315
|
+
ledger.status = "rejected";
|
|
316
|
+
ledger.evidenceStatus = "rejected";
|
|
317
|
+
ledger.failureMessage = `Acceptance verification '${failed.id}' ${failed.status}.`;
|
|
318
|
+
return ledger;
|
|
319
|
+
}
|
|
320
|
+
if (!ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
321
|
+
ledger.status = "verified";
|
|
322
|
+
ledger.evidenceStatus = "verified";
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (ledger.status === "claimed") {
|
|
327
|
+
ledger.status = acceptance.level === "auto" ? "attested" : (acceptance.level as AcceptanceLedger["status"]);
|
|
328
|
+
ledger.evidenceStatus = ledger.status;
|
|
329
|
+
}
|
|
330
|
+
return ledger;
|
|
331
|
+
}
|