pi-cohort 5.0.0 → 5.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.1.0] - 2026-08-06
4
+
5
+ ### Changed
6
+
7
+ - subagent tool schema shrunk from 18.0KB to 12.4KB serialized: the acceptance
8
+ override shape (previously inlined 5x, 44% of the schema) now appears in full
9
+ only at top-level `acceptance`; the 4 nested sites (tasks items, chain steps,
10
+ parallel members, dynamic fanout template) carry a compact stub. Gate-level
11
+ `evidence`/`severity` dropped from the schema (still accepted at runtime).
12
+ Deep acceptance validation moved to the executor boundary with per-site path
13
+ labels. Nested `acceptance: false` is no longer schema-accepted - use
14
+ `level: "none"`. Unknown acceptance keys are now rejected everywhere,
15
+ including saved `.chain.json` chains that previously loaded with the key
16
+ silently ignored. ([#6](https://github.com/jjuraszek/pi-cohort/issues/6))
17
+
18
+ ## [5.0.1] - 2026-08-06
19
+
20
+ ### Changed
21
+
22
+ - subagent tool schema dieted from 23.2KB to <=18KB serialized (description
23
+ trimming only; all accepted input shapes unchanged, guarded by a baseline
24
+ shape fixture). `config` field reference moved to the pi-cohort skill's
25
+ `reference/config-fields.md`. Schema size-budget test added.
26
+
3
27
  ## [5.0.0] - 2026-08-06
4
28
 
5
29
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cohort",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "Delegate Pi work to focused child agents: code review, scouting, implementation, parallel audits, saved chains, and background jobs.",
5
5
  "author": "Jacek Juraszek",
6
6
  "license": "MIT",
@@ -503,6 +503,8 @@ If intercom messages do not show up, run `subagent({ action: "doctor" })` or `/c
503
503
 
504
504
  The `subagent(...)` tool also supports management actions.
505
505
 
506
+ Full config field reference: `reference/config-fields.md` (sibling of this file).
507
+
506
508
  ### List available agents and chains
507
509
 
508
510
  ```typescript
@@ -0,0 +1,51 @@
1
+ # subagent `config` field reference
2
+
3
+ Fields accepted by `subagent({ action: "create" | "update", config: {...} })`.
4
+ `config` may be an object or a JSON string. Presence of `steps` makes it a chain.
5
+ Scope: this is the management create/update path (`parseStepList`), which accepts exactly the step fields below and requires `outputSchema` to be a file path; file-authored `.chain.md` chains support additional step fields (parallel, expand, collect, concurrency, failFast, worktree, acceptance, inline outputSchema) - see SKILL.md chain authoring.
6
+
7
+ ## Agent config
8
+
9
+ | Field | Type | Meaning | Default |
10
+ |---|---|---|---|
11
+ | `name` | string | Required (create). Letters, numbers, spaces, hyphens. | - |
12
+ | `description` | string | Required (create). | - |
13
+ | `package` | string | Optional namespace; runtime name becomes `package.name`. | none |
14
+ | `scope` | `"user"` \| `"project"` | Where the definition file lands. | `"user"` |
15
+ | `systemPrompt` | string \| false | Persona body; false or "" clears. | `""` |
16
+ | `systemPromptMode` | `"append"` \| `"replace"` | How systemPrompt combines with the base prompt. | per-agent default |
17
+ | `model` | string \| false | Model override; false/"" clears. | inherit |
18
+ | `fallbackModels` | string (csv) \| string[] \| false | Models tried in order on failure; false/"" clears. | none |
19
+ | `tools` | string (csv) \| false | Tool allowlist; false/"" clears. MCP-direct tools rejected. | all |
20
+ | `skills` | string (csv) \| false | Skills injected into the agent; false/"" clears. | none |
21
+ | `extensions` | string (csv) \| "" \| false | Extension list; "" means empty list, false clears. | inherit |
22
+ | `thinking` | string \| false | Thinking level; false/"" clears. | inherit |
23
+ | `inheritProjectContext` | boolean | Inject project AGENTS.md context. | per-agent default |
24
+ | `inheritSkills` | boolean | Inherit parent-visible skills. | per-agent default |
25
+ | `defaultContext` | `"fresh"` \| `"fork"` \| false | Default context mode; false/"" clears. | `"fresh"` |
26
+ | `output` | string \| false | Default output filename; false/"" clears. | none |
27
+ | `reads` | string (csv) \| false | Default files read before running; false/"" clears. | none |
28
+ | `progress` | boolean | Default progress.md tracking. | off |
29
+ | `maxSubagentDepth` | integer >= 0 \| false | Nested subagent depth cap; false/"" clears. | inherit |
30
+ | `completionGuard` | boolean | Enable completion guard for this agent. | inherit |
31
+
32
+ ## Chain config
33
+
34
+ Top-level: `name`, `description`, `package`, `scope` as above, plus required `steps` (non-empty array).
35
+
36
+ Per step:
37
+
38
+ | Field | Type | Meaning |
39
+ |---|---|---|
40
+ | `agent` | string | Required, non-empty. |
41
+ | `task` | string | Task template; defaults to `""`. |
42
+ | `phase` | string | Phase/group label. |
43
+ | `label` | string | User-facing label. |
44
+ | `as` | string | Identifier for `{outputs.name}` in later steps. |
45
+ | `outputSchema` | string | Schema FILE PATH (saved chains take a path, not an inline object). |
46
+ | `output` | string \| false | Output file path, or false to disable. |
47
+ | `outputMode` | `"inline"` \| `"file-only"` | Output return mode. |
48
+ | `reads` | string[] \| false | Files to read before the step. |
49
+ | `model` | string | Model override. |
50
+ | `skills` | string[] \| false | Skills to inject (NOTE: plural `skills` here, unlike the execution-time `skill` param). |
51
+ | `progress` | boolean | progress.md tracking. |
@@ -5,13 +5,23 @@
5
5
  import { Type } from "typebox";
6
6
  import { SUBAGENT_ACTIONS } from "../shared/types.ts";
7
7
 
8
+ // Descriptor-preserving clone: typebox 1.3.11 metadata ("~kind", "~unsafe",
9
+ // "~optional") lives in NON-ENUMERABLE string-keyed properties. Object spread
10
+ // would drop them, and Type.Optional on a plain clone would re-add "~optional"
11
+ // as an enumerable key, leaking "~optional":true into the serialized JSON.
12
+ const brief = <T extends object>(schema: T, description: string): T =>
13
+ Object.defineProperties(
14
+ Object.defineProperties(Object.create(null), Object.getOwnPropertyDescriptors(schema)),
15
+ { description: { value: description, enumerable: true, writable: true, configurable: true } },
16
+ ) as T;
17
+
8
18
  const SkillOverride = Type.Unsafe({
9
19
  anyOf: [
10
20
  { type: "array", items: { type: "string" } },
11
21
  { type: "boolean" },
12
22
  { type: "string" },
13
23
  ],
14
- description: "Skill name(s) to inject (comma-separated), array of strings, or boolean (false disables, true uses default)",
24
+ description: "CSV/array/boolean (false disables, true default).",
15
25
  });
16
26
 
17
27
  const OutputOverride = Type.Unsafe({
@@ -24,7 +34,7 @@ const OutputOverride = Type.Unsafe({
24
34
 
25
35
  const OutputModeOverride = Type.String({
26
36
  enum: ["inline", "file-only"],
27
- description: "Return saved output inline (default) or only a concise file reference. file-only requires output to be a path.",
37
+ description: "inline (default) or file-only (needs output path).",
28
38
  });
29
39
 
30
40
  const ReadsOverride = Type.Unsafe({
@@ -38,7 +48,7 @@ const ReadsOverride = Type.Unsafe({
38
48
  const JsonSchemaObject = Type.Unsafe({
39
49
  type: "object",
40
50
  additionalProperties: true,
41
- description: "JSON Schema object for strict structured output. Non-object roots are rejected.",
51
+ description: "Structured output; non-object rejected.",
42
52
  });
43
53
 
44
54
  const AcceptanceEvidenceKind = Type.String({
@@ -55,12 +65,13 @@ const AcceptanceEvidenceKind = Type.String({
55
65
  ],
56
66
  });
57
67
 
68
+ // Gate-level `evidence`/`severity` are accepted at runtime (normalizeCriteria) but omitted
69
+ // from the schema: dead flexibility from a tool call, and the evidence enum is heavy.
70
+ // additionalProperties stays open so inputs carrying them are not provider-rejected.
58
71
  const AcceptanceGateSchema = Type.Object({
59
72
  id: Type.String(),
60
73
  must: Type.String(),
61
- evidence: Type.Optional(Type.Array(AcceptanceEvidenceKind)),
62
- severity: Type.Optional(Type.String({ enum: ["required", "recommended"] })),
63
- }, { additionalProperties: false });
74
+ }, { additionalProperties: true });
64
75
 
65
76
  const AcceptanceVerifyCommandSchema = Type.Object({
66
77
  id: Type.String(),
@@ -108,108 +119,123 @@ const AcceptanceOverride = Type.Unsafe({
108
119
  additionalProperties: false,
109
120
  },
110
121
  ],
111
- description: "Optional acceptance policy. Omitted means auto-inferred; verified requires configured runtime commands.",
122
+ description: "Acceptance: auto if omitted; verified needs cmds.",
123
+ });
124
+
125
+ // Nested acceptance sites carry a compact stub instead of the full AcceptanceOverride:
126
+ // inlined 5x it was 44% of the serialized schema. The full shape is documented once at
127
+ // top-level `acceptance`; deep validation happens at the executor boundary
128
+ // (validateAcceptanceInput), not provider-side. The object branch keeps a non-empty
129
+ // `properties` because some providers reject bare {type:"object"} in function
130
+ // declarations - the same wall that rules out $defs/$ref (see issue #6).
131
+ // {const:false} is intentionally absent: deprecated shorthand, use level:"none".
132
+ const AcceptanceOverrideStub = Type.Unsafe({
133
+ anyOf: [
134
+ { type: "string", enum: ["auto", "none", "attested", "checked", "verified", "reviewed"] },
135
+ { type: "object", additionalProperties: true, properties: { level: { type: "string" } } },
136
+ ],
137
+ description: "Acceptance override; same shape as top-level acceptance.",
112
138
  });
113
139
 
114
140
  const TaskItem = Type.Object({
115
141
  agent: Type.String(),
116
142
  task: Type.String(),
117
143
  cwd: Type.Optional(Type.String()),
118
- count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
119
- output: Type.Optional(OutputOverride),
120
- outputMode: Type.Optional(OutputModeOverride),
144
+ count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat N times (same settings)." })),
145
+ output: Type.Optional(brief(OutputOverride, "Output file path, or false.")),
146
+ outputMode: Type.Optional(brief(OutputModeOverride, "Default: inline.")),
121
147
  reads: Type.Optional(ReadsOverride),
122
- progress: Type.Optional(Type.Boolean({ description: "Omit or set false to disable progress.md tracking for this task; true enables it." })),
123
- model: Type.Optional(Type.String({ description: "Override model for this task (e.g. 'google/gemini-3-pro')" })),
124
- skill: Type.Optional(SkillOverride),
125
- acceptance: Type.Optional(AcceptanceOverride),
148
+ progress: Type.Optional(Type.Boolean({ description: "true enables progress.md tracking; omit or false disables." })),
149
+ model: Type.Optional(Type.String()),
150
+ skill: Type.Optional(brief(SkillOverride, "Skill override.")),
151
+ acceptance: Type.Optional(AcceptanceOverrideStub),
126
152
  });
127
153
 
128
154
  // Parallel task item (within a parallel step)
129
155
  const ParallelTaskSchema = Type.Object({
130
156
  agent: Type.String(),
131
- task: Type.Optional(Type.String({ description: "Task template with {task}, {previous}, {chain_dir} variables. Defaults to {previous}." })),
132
- phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
133
- label: Type.Optional(Type.String({ description: "Optional user-facing label for this parallel task." })),
134
- as: Type.Optional(Type.String({ description: "Optional safe identifier used as {outputs.name} in later chain steps." })),
135
- outputSchema: Type.Optional(JsonSchemaObject),
157
+ task: Type.Optional(Type.String({ description: "{task},{previous},{chain_dir} template; defaults to {previous}." })),
158
+ phase: Type.Optional(Type.String()),
159
+ label: Type.Optional(Type.String()),
160
+ as: Type.Optional(Type.String({ description: "Identifier for {outputs.name}." })),
161
+ outputSchema: Type.Optional(brief(JsonSchemaObject, "Output JSON Schema.")),
136
162
  cwd: Type.Optional(Type.String()),
137
- count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
138
- output: Type.Optional(OutputOverride),
139
- outputMode: Type.Optional(OutputModeOverride),
140
- reads: Type.Optional(ReadsOverride),
141
- progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
142
- skill: Type.Optional(SkillOverride),
143
- model: Type.Optional(Type.String({ description: "Override model for this task" })),
144
- acceptance: Type.Optional(AcceptanceOverride),
163
+ count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat N times." })),
164
+ output: Type.Optional(brief(OutputOverride, "Output file path, or false.")),
165
+ outputMode: Type.Optional(brief(OutputModeOverride, "Default: inline.")),
166
+ reads: Type.Optional(brief(ReadsOverride, "Reads first, or false.")),
167
+ progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking." })),
168
+ model: Type.Optional(Type.String()),
169
+ skill: Type.Optional(brief(SkillOverride, "Skill override.")),
170
+ acceptance: Type.Optional(AcceptanceOverrideStub),
145
171
  });
146
172
 
147
173
  const DynamicExpandSchema = Type.Object({
148
174
  from: Type.Object({
149
- output: Type.String({ description: "Prior named structured output to expand from." }),
150
- path: Type.String({ description: "JSON Pointer into the structured output, e.g. /items." }),
175
+ output: Type.String({ description: "Named prior output." }),
176
+ path: Type.String({ description: "JSON Pointer into it." }),
151
177
  }, { additionalProperties: false }),
152
- item: Type.Optional(Type.String({ description: "Template variable name for each item. Defaults to item." })),
153
- key: Type.Optional(Type.String({ description: "JSON Pointer relative to each item for stable child ids." })),
154
- maxItems: Type.Optional(Type.Integer({ minimum: 0, description: "Required fanout bound unless configured globally." })),
155
- onEmpty: Type.Optional(Type.String({ enum: ["skip", "fail"], description: "Empty input behavior. Defaults to skip." })),
178
+ item: Type.Optional(Type.String({ description: "Defaults to item." })),
179
+ key: Type.Optional(Type.String({ description: "Pointer per item; stable child id." })),
180
+ maxItems: Type.Optional(Type.Integer({ minimum: 0, description: "Required unless chain.dynamicFanout.maxItems is configured." })),
181
+ onEmpty: Type.Optional(Type.String({ enum: ["skip", "fail"], description: "Defaults to skip." })),
156
182
  }, { additionalProperties: false });
157
183
 
158
184
  const DynamicParallelTemplateSchema = Type.Object({
159
185
  agent: Type.String(),
160
- task: Type.Optional(Type.String({ description: "Task template with {item}, {item.path}, {task}, {previous}, {chain_dir}, and {outputs.name} variables." })),
161
- phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
162
- label: Type.Optional(Type.String({ description: "Optional user-facing label; item templates are supported." })),
163
- outputSchema: Type.Optional(JsonSchemaObject),
186
+ task: Type.Optional(Type.String({ description: "{item},{item.path},{task},{previous},{chain_dir},{outputs.name} template." })),
187
+ phase: Type.Optional(Type.String()),
188
+ label: Type.Optional(Type.String({ description: "Label; item templates supported." })),
189
+ outputSchema: Type.Optional(brief(JsonSchemaObject, "Output JSON Schema.")),
164
190
  cwd: Type.Optional(Type.String()),
165
- output: Type.Optional(OutputOverride),
166
- outputMode: Type.Optional(OutputModeOverride),
167
- reads: Type.Optional(ReadsOverride),
168
- progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
169
- skill: Type.Optional(SkillOverride),
170
- model: Type.Optional(Type.String({ description: "Override model for this task" })),
171
- acceptance: Type.Optional(AcceptanceOverride),
191
+ output: Type.Optional(brief(OutputOverride, "Output file path, or false.")),
192
+ outputMode: Type.Optional(brief(OutputModeOverride, "Default: inline.")),
193
+ reads: Type.Optional(brief(ReadsOverride, "Reads first, or false.")),
194
+ progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking." })),
195
+ model: Type.Optional(Type.String()),
196
+ skill: Type.Optional(brief(SkillOverride, "Skill override.")),
197
+ acceptance: Type.Optional(AcceptanceOverrideStub),
172
198
  }, { additionalProperties: false });
173
199
 
174
200
  const DynamicCollectSchema = Type.Object({
175
- as: Type.String({ description: "Safe output name for the ordered collected result array." }),
176
- outputSchema: Type.Optional(JsonSchemaObject),
201
+ as: Type.String({ description: "Collected result array name." }),
202
+ outputSchema: Type.Optional(brief(JsonSchemaObject, "Output JSON Schema.")),
177
203
  }, { additionalProperties: false });
178
204
 
179
205
  // Flattened so chain steps do not need an object-shape anyOf/oneOf union.
180
206
  const ChainItem = Type.Object({
181
- agent: Type.Optional(Type.String({ description: "Sequential step agent name" })),
207
+ agent: Type.Optional(Type.String()),
182
208
  task: Type.Optional(Type.String({
183
- description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder, {outputs.name}=prior named output. Required for first step, defaults to '{previous}' for subsequent steps."
209
+ description: "{task},{previous},{chain_dir},{outputs.name}; required first else '{previous}'."
184
210
  })),
185
- phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
186
- label: Type.Optional(Type.String({ description: "Optional user-facing label for this chain step." })),
187
- as: Type.Optional(Type.String({ description: "Optional safe identifier used as {outputs.name} in later chain steps." })),
211
+ phase: Type.Optional(Type.String({ description: "Phase/group label (status/graph)." })),
212
+ label: Type.Optional(Type.String({ description: "Chain step label." })),
213
+ as: Type.Optional(Type.String({ description: "Id for {outputs.name} in later steps." })),
188
214
  outputSchema: Type.Optional(JsonSchemaObject),
189
215
  cwd: Type.Optional(Type.String()),
190
- output: Type.Optional(OutputOverride),
191
- outputMode: Type.Optional(OutputModeOverride),
192
- reads: Type.Optional(ReadsOverride),
216
+ output: Type.Optional(brief(OutputOverride, "Output file path, or false.")),
217
+ outputMode: Type.Optional(brief(OutputModeOverride, "Default: inline.")),
218
+ reads: Type.Optional(brief(ReadsOverride, "Reads first, or false.")),
193
219
  progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
194
- skill: Type.Optional(SkillOverride),
195
- model: Type.Optional(Type.String({ description: "Override model for this step" })),
196
- acceptance: Type.Optional(AcceptanceOverride),
220
+ model: Type.Optional(Type.String()),
221
+ skill: Type.Optional(brief(SkillOverride, "Skill override.")),
222
+ acceptance: Type.Optional(AcceptanceOverrideStub),
197
223
  parallel: Type.Optional(Type.Unsafe({
198
224
  anyOf: [
199
- Type.Array(ParallelTaskSchema, { minItems: 1, description: "Tasks to run in parallel" }),
225
+ Type.Array(ParallelTaskSchema, { minItems: 1 }),
200
226
  DynamicParallelTemplateSchema,
201
227
  ],
202
- description: "Static parallel tasks array, or a single dynamic fanout child template when expand/collect are present.",
228
+ description: "Static array, or dynamic fanout template (expand/collect).",
203
229
  })),
204
230
  expand: Type.Optional(DynamicExpandSchema),
205
231
  collect: Type.Optional(DynamicCollectSchema),
206
232
  concurrency: Type.Optional(Type.Number({ description: "Max concurrent tasks (default: 4)" })),
207
233
  failFast: Type.Optional(Type.Boolean({ description: "Stop on first failure (default: false)" })),
208
234
  worktree: Type.Optional(Type.Boolean({
209
- description: "Create isolated git worktrees for each parallel task."
235
+ description: "Isolated worktree per task."
210
236
  })),
211
237
  }, {
212
- description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, or {expand, parallel: {...}, collect} for dynamic fanout.",
238
+ description: "{agent,task?} seq; {parallel:[..]} concurrent; {expand,parallel,collect} fanout.",
213
239
  additionalProperties: false,
214
240
  allOf: [
215
241
  { if: { required: ["expand"] }, then: { required: ["parallel", "collect"], properties: { parallel: { type: "object" } } } },
@@ -219,44 +245,44 @@ const ChainItem = Type.Object({
219
245
  });
220
246
 
221
247
  const ControlOverrides = Type.Object({
222
- enabled: Type.Optional(Type.Boolean({ description: "Enable/disable subagent control attention tracking for this run" })),
223
- needsAttentionAfterMs: Type.Optional(Type.Integer({ minimum: 1, description: "No-observed-activity window before a run needs attention" })),
224
- activeNoticeAfterMs: Type.Optional(Type.Integer({ minimum: 1, description: "Active-long-running notice threshold by elapsed ms (default: 240000)" })),
225
- inFlightSilenceCeilingMs: Type.Optional(Type.Integer({ minimum: 1, description: "How long a silent in-flight turn stays calm (active_long_running) before re-escalating to needs_attention (default: 600000)" })),
226
- inFlightSilenceKillMs: Type.Optional(Type.Integer({ minimum: 1, description: "Hard cap: SIGTERM a child whose in-flight turn has been silent this long (default: 1800000; clamped to sit above the needs_attention escalation)" })),
227
- activeNoticeAfterTurns: Type.Optional(Type.Integer({ minimum: 1, description: "Optional active-long-running notice threshold by assistant turns (disabled by default)" })),
228
- activeNoticeAfterTokens: Type.Optional(Type.Integer({ minimum: 1, description: "Optional active-long-running notice threshold by total tokens (disabled by default)" })),
229
- failedToolAttemptsBeforeAttention: Type.Optional(Type.Integer({ minimum: 1, description: "Consecutive mutating-tool failures before escalating to needs_attention (default: 3)" })),
248
+ enabled: Type.Optional(Type.Boolean({ description: "Toggle attention tracking." })),
249
+ needsAttentionAfterMs: Type.Optional(Type.Integer({ minimum: 1, description: "No-activity window before needs_attention." })),
250
+ activeNoticeAfterMs: Type.Optional(Type.Integer({ minimum: 1, description: "Elapsed-ms notice threshold (default: 240000)." })),
251
+ inFlightSilenceCeilingMs: Type.Optional(Type.Integer({ minimum: 1, description: "Silent ms before escalation (default: 600000)." })),
252
+ inFlightSilenceKillMs: Type.Optional(Type.Integer({ minimum: 1, description: "SIGTERM child (default: 1800000, clamped above needs_attention)." })),
253
+ activeNoticeAfterTurns: Type.Optional(Type.Integer({ minimum: 1, description: "Notice by assistant turns (off by default)." })),
254
+ activeNoticeAfterTokens: Type.Optional(Type.Integer({ minimum: 1, description: "Notice by total tokens (off by default)." })),
255
+ failedToolAttemptsBeforeAttention: Type.Optional(Type.Integer({ minimum: 1, description: "Mutating-tool failures before needs_attention (default: 3)." })),
230
256
  notifyOn: Type.Optional(Type.Array(Type.String({ enum: ["active_long_running", "needs_attention"] }), {
231
- description: "Control event types that should notify the parent/orchestrator. Defaults to active_long_running and needs_attention.",
257
+ description: "To parent (default: active_long_running, needs_attention).",
232
258
  })),
233
259
  notifyChannels: Type.Optional(Type.Array(Type.String({ enum: ["event", "async", "intercom"] }), {
234
- description: "Notification channels to use when available. Defaults to event, async, and intercom.",
260
+ description: "Default: event, async, intercom.",
235
261
  })),
236
262
  });
237
263
 
238
264
  export const SubagentParams = Type.Object({
239
- agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode) or target for management get/update/delete" })),
240
- task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })),
265
+ agent: Type.Optional(Type.String({ description: "SINGLE mode agent, or management target." })),
266
+ task: Type.Optional(Type.String({ description: "Task (SINGLE mode; optional if self-contained)." })),
241
267
  // Management action (when present, tool operates in management mode)
242
268
  action: Type.Optional(Type.String({
243
269
  enum: [...SUBAGENT_ACTIONS],
244
270
  description: "Management/control action. Omit for execution mode."
245
271
  })),
246
272
  id: Type.Optional(Type.String({
247
- description: "Run id or prefix for action='status', action='interrupt', or action='resume'."
273
+ description: "Run id/prefix for status/interrupt/resume."
248
274
  })),
249
275
  runId: Type.Optional(Type.String({
250
- description: "Target run ID for action='interrupt' or action='resume'. Defaults to the most recently active controllable run for interrupt. Prefer id for new calls."
276
+ description: "Run ID (interrupt/resume); defaults to latest. Prefer id."
251
277
  })),
252
278
  dir: Type.Optional(Type.String({
253
- description: "Async run directory for action='status' or action='resume'."
279
+ description: "Async run dir for status/resume."
254
280
  })),
255
- index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child." })),
256
- message: Type.Optional(Type.String({ description: "Follow-up message for action='resume'. Use index to choose a child from multi-child runs." })),
281
+ index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based index for per-child actions." })),
282
+ message: Type.Optional(Type.String({ description: "Follow-up message for resume." })),
257
283
  // Chain identifier for management (can't reuse 'chain' — that's the execution array)
258
284
  chainName: Type.Optional(Type.String({
259
- description: "Chain name for get/update/delete management actions"
285
+ description: "Chain name."
260
286
  })),
261
287
  // Agent/chain configuration for create/update (nested to avoid conflicts with execution fields)
262
288
  config: Type.Optional(Type.Unsafe({
@@ -264,32 +290,30 @@ export const SubagentParams = Type.Object({
264
290
  { type: "object", additionalProperties: true },
265
291
  { type: "string" },
266
292
  ],
267
- description: "Agent or chain config for create/update. Agent: name, package (optional namespace; runtime name becomes package.name), description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext ('fresh'|'fork'), model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, package, description, scope, steps (array of {agent, task?, output?, outputMode?, reads?, model?, skill?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
293
+ description: "Create/update config; fields: pi-cohort skill reference/config-fields.md; string=JSON."
268
294
  })),
269
- tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?, output?, outputMode?, reads?, progress?}, ...]" })),
270
- concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Top-level PARALLEL mode only: max concurrent tasks. Defaults to config.parallel.concurrency or 4." })),
295
+ tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode tasks: [{agent, task, ...}]." })),
296
+ concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Default: config.parallel.concurrency or 4." })),
271
297
  worktree: Type.Optional(Type.Boolean({
272
- description: "Create isolated git worktrees for each parallel task. " +
273
- "Prevents filesystem conflicts. Requires clean git state. " +
274
- "Per-worktree diffs included in output."
298
+ description: "Isolated worktree per task; clean git; diffs in output."
275
299
  })),
276
- chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: sequential pipeline where each step's response becomes {previous} for the next. Use {task}, {previous}, {chain_dir} in task templates." })),
300
+ chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN: sequential; step response becomes {previous}." })),
277
301
  context: Type.Optional(Type.String({
278
302
  enum: ["fresh", "fork"],
279
- description: "'fresh' or 'fork' to branch from parent session. If omitted, any requested agent with defaultContext: 'fork' makes the whole invocation forked; otherwise the default is 'fresh'.",
303
+ description: "fresh (default) or fork; defaultContext:fork forces whole invocation.",
280
304
  })),
281
- chainDir: Type.Optional(Type.String({ description: "Persistent directory for chain artifacts. Default: a user-scoped temp directory under <tmpdir>/ (auto-cleaned after 24h)" })),
282
- async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
283
- agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
305
+ chainDir: Type.Optional(Type.String({ description: "Default temp <tmpdir>/, 24h clean." })),
306
+ async: Type.Optional(Type.Boolean({ description: "Background run (default: false, or per config)." })),
307
+ agentScope: Type.Optional(Type.String({ description: "user/project/both (default both, project wins)." })),
284
308
  cwd: Type.Optional(Type.String()),
285
- artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: true)" })),
286
- includeProgress: Type.Optional(Type.Boolean({ description: "Include full progress in result (default: false)" })),
287
- share: Type.Optional(Type.Boolean({ description: "Upload session to GitHub Gist for sharing (default: false)" })),
309
+ artifacts: Type.Optional(Type.Boolean({ description: "(default: true)." })),
310
+ includeProgress: Type.Optional(Type.Boolean({ description: "(default: false)." })),
311
+ share: Type.Optional(Type.Boolean({ description: "Upload session to GitHub Gist (default: false)." })),
288
312
  sessionDir: Type.Optional(
289
- Type.String({ description: "Directory to store session logs (default: temp; enables sessions even if share=false)" }),
313
+ Type.String({ description: "Default temp; enables sessions even if share=false." }),
290
314
  ),
291
315
  // Clarification TUI
292
- clarify: Type.Optional(Type.Boolean({ description: "Show TUI to preview/edit before execution. Omitted or false launches directly. Explicit clarify: true keeps the run foreground for the clarify UI when supported; chains containing parallel steps skip the UI." })),
316
+ clarify: Type.Optional(Type.Boolean({ description: "Preview/edit TUI; omitted/false runs directly; true=foreground (if supported); parallel-step chains skip it." })),
293
317
  control: Type.Optional(ControlOverrides),
294
318
  // Solo agent overrides
295
319
  output: Type.Optional(Type.Unsafe({
@@ -297,10 +321,10 @@ export const SubagentParams = Type.Object({
297
321
  { type: "string" },
298
322
  { type: "boolean" },
299
323
  ],
300
- description: "Omit or set false to disable file output; true uses the agent-configured filename; a string sets the output path. Relative paths resolve against cwd.",
324
+ description: "Omit or false disables; true=agent-configured filename; string=path (rel. cwd).",
301
325
  })),
302
326
  outputMode: Type.Optional(OutputModeOverride),
303
327
  skill: Type.Optional(SkillOverride),
304
- model: Type.Optional(Type.String({ description: "Override model for single agent (e.g. 'anthropic/claude-sonnet-4')" })),
328
+ model: Type.Optional(Type.String({ description: "Model override, e.g. 'anthropic/claude-sonnet-4'." })),
305
329
  acceptance: Type.Optional(AcceptanceOverride),
306
330
  });
@@ -17,6 +17,7 @@ import { recordSyncCost, renderGrandTotal, sumNestedCost } from "../../extension
17
17
  import { resolveModelCandidate } from "../shared/model-fallback.ts";
18
18
  import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
19
19
  import { recordRun } from "../shared/run-history.ts";
20
+ import { validateAcceptanceInput } from "../shared/acceptance.ts";
20
21
  import {
21
22
  buildChainInstructions,
22
23
  writeInitialProgressFile,
@@ -858,6 +859,33 @@ function validateExecutionInput(
858
859
  }
859
860
  }
860
861
 
862
+ // Nested acceptance sites only carry a compact stub in the tool schema (see
863
+ // AcceptanceOverrideStub in extension/schemas.ts); this is the deep-validation boundary.
864
+ const acceptanceErrors: string[] = [
865
+ ...validateAcceptanceInput(params.acceptance, "acceptance"),
866
+ ];
867
+ for (const [i, task] of (params.tasks ?? []).entries()) {
868
+ acceptanceErrors.push(...validateAcceptanceInput(task.acceptance, `tasks[${i}].acceptance`));
869
+ }
870
+ for (const [i, rawStep] of (params.chain ?? []).entries()) {
871
+ const step = rawStep as ChainStep;
872
+ acceptanceErrors.push(...validateAcceptanceInput((rawStep as { acceptance?: unknown }).acceptance, `chain[${i}].acceptance`));
873
+ if (isParallelStep(step)) {
874
+ for (const [j, member] of step.parallel.entries()) {
875
+ acceptanceErrors.push(...validateAcceptanceInput(member.acceptance, `chain[${i}].parallel[${j}].acceptance`));
876
+ }
877
+ } else if (isDynamicParallelStep(step)) {
878
+ acceptanceErrors.push(...validateAcceptanceInput(step.parallel.acceptance, `chain[${i}].parallel.acceptance`));
879
+ }
880
+ }
881
+ if (acceptanceErrors.length > 0) {
882
+ return {
883
+ content: [{ type: "text", text: `Invalid acceptance input:\n${acceptanceErrors.map((error) => `- ${error}`).join("\n")}` }],
884
+ isError: true,
885
+ details: { mode: hasChain ? "chain" as const : hasTasks ? "parallel" as const : "single" as const, results: [] },
886
+ };
887
+ }
888
+
861
889
  return null;
862
890
  }
863
891
 
@@ -131,6 +131,8 @@ function explicitAcceptanceCanDisable(explicit: AcceptanceConfig): boolean {
131
131
  return explicit.level === "none" && typeof explicit.reason === "string" && explicit.reason.trim().length > 0;
132
132
  }
133
133
 
134
+ const KNOWN_ACCEPTANCE_KEYS = new Set(["level", "criteria", "evidence", "verify", "review", "stopRules", "reason"]);
135
+
134
136
  export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"): string[] {
135
137
  const errors: string[] = [];
136
138
  if (input === undefined) return errors;
@@ -144,6 +146,11 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
144
146
  return errors;
145
147
  }
146
148
  const value = input as Record<string, unknown>;
149
+ for (const key of Object.keys(value)) {
150
+ if (!KNOWN_ACCEPTANCE_KEYS.has(key)) {
151
+ errors.push(`${pathLabel}.${key} is not a recognized acceptance field (known: level, criteria, evidence, verify, review, stopRules, reason).`);
152
+ }
153
+ }
147
154
  if (value.level !== undefined && (typeof value.level !== "string" || !VALID_LEVELS.has(value.level as AcceptanceLevel))) {
148
155
  errors.push(`${pathLabel}.level must be one of auto, none, attested, checked, verified, reviewed.`);
149
156
  }