pi-cohort 7.0.2 → 7.1.1
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 +12 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/skills/handoff/SKILL.md +83 -0
- package/src/agents/agent-serializer.ts +1 -1
- package/src/runs/background/async-execution.ts +17 -12
- package/src/runs/foreground/chain-clarify.ts +5 -5
- package/src/runs/foreground/chain-execution.ts +15 -2
- package/src/runs/foreground/execution.ts +4 -3
- package/src/runs/foreground/subagent-executor.ts +22 -36
- package/src/runs/shared/pi-args.ts +2 -2
- package/src/shared/model-info.ts +9 -4
- package/src/shared/types.ts +4 -0
- package/prompts/handoff.md +0 -54
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [7.1.1] - 2026-09-24
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- A dispatch that names no model now runs the child on the parent session's current model and thinking level instead of the child's saved `defaultModel`; `thinking: off` is emitted as `:off`, and `max` is a recognized level. Precedence: per-call `model:` > agent `model` / `thinking` (after `agentOverrides`) > parent session > none. Pin `subagents.agentOverrides.<agent>.model` to keep a child on a fixed model. Agents that pin `model` but not `thinking` now also take the parent's thinking level; set `thinking` to keep a fixed level. Children now bill on the parent's model, and an agent with `extensions:` whose parent model comes from an extension provider fails as an unknown model would; give it a pinned model or `fallbackModels`.
|
|
8
|
+
|
|
9
|
+
## [7.1.0] - 2026-09-20
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `/handoff` no longer expands; `/skill:handoff [--out <path> | --key <stem>]` replaces it, writes to `<tmpdir>/pi-handoff/<primary>--<leaf>.md` by default and ends with `Handoff written: <path>`; `## Process state` is no longer produced here; argument-less `gauntlet-resume` lookup of that path lands on the pi-gauntlet side - until then resume takes the printed path. ([#18](https://github.com/jjuraszek/pi-cohort/issues/18), [pi-gauntlet#40](https://github.com/jjuraszek/pi-gauntlet/issues/40))
|
|
14
|
+
|
|
3
15
|
## [7.0.2] - 2026-09-20
|
|
4
16
|
|
|
5
17
|
### Changed
|
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ Run parallel reviewers: one for correctness, one for tests, and one for unnecess
|
|
|
117
117
|
|
|
118
118
|
That's the whole surface for day-to-day use. More phrasing patterns: [doc/commands.md](doc/commands.md#prompt-cookbook-appendix).
|
|
119
119
|
|
|
120
|
-
Review and delivery workflows live in pi-gauntlet; pi-cohort ships delegation primitives plus `/investigate` and `/handoff
|
|
120
|
+
Review and delivery workflows live in pi-gauntlet; pi-cohort ships delegation primitives plus `/investigate` and `/skill:handoff [--out <path> | --key <stem>]` (the brief lands in `<tmpdir>/pi-handoff/<primary>--<leaf>.md` by default; contract in [doc/handoff-template.md](doc/handoff-template.md)).
|
|
121
121
|
|
|
122
122
|
## Architecture
|
|
123
123
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cohort",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.1.1",
|
|
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",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: handoff
|
|
3
|
+
description: Use when the context window is nearly full and the work must continue in a fresh session.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Handoff brief
|
|
7
|
+
|
|
8
|
+
Write a handoff brief for a fresh session. The brief is the contract in `doc/handoff-template.md` of pi-cohort: section names and order are fixed. This file carries every rule a producer needs; a caller that follows it reads nothing else. Nothing is written into the repository.
|
|
9
|
+
|
|
10
|
+
## Argument
|
|
11
|
+
|
|
12
|
+
The argument text, if any, follows this block. Tokenize it shell-like (unquoted tokens end at whitespace; `".."`/`'..'` keep whitespace; `--opt=value` is one token). An option's value is the next token; a next token that is itself `--out`, `--key`, or an `--out=`/`--key=` form, or no next token, or an empty `--opt=` value, means the option has no value. `--out <path>` names the output file (relative resolves against cwd); `--key <stem>` names the file inside the shared mailbox directory; every other token is ignored.
|
|
13
|
+
|
|
14
|
+
Argument failures end the reply with the line and write nothing:
|
|
15
|
+
|
|
16
|
+
- an option with an empty or missing value -> `Handoff not written: <option> has no value`
|
|
17
|
+
- an option given twice -> `Handoff not written: <option> given twice`
|
|
18
|
+
- both options -> `Handoff not written: --out and --key are exclusive`
|
|
19
|
+
|
|
20
|
+
## Rules
|
|
21
|
+
|
|
22
|
+
- Scratch dir, invocation-unique so overlapping invocations never read each other's snapshot: `SCRATCH=$(node -p "require('fs').mkdtempSync(require('path').join(require('os').tmpdir(), 'pi-handoff-'))")`. If `node` is missing -> `Handoff not written: cannot resolve tmpdir`.
|
|
23
|
+
- The snapshot dispatch: `async: false` and `context: "fresh"` at the top level; `reads: false`; absolute `output` under `$SCRATCH`. If it returns an async handle, a `forceTopLevelAsync` setting is active: report that in the reply, do not poll, relaunch, or continue the child, and write the brief with `## Repo state: unavailable (async handle returned)`.
|
|
24
|
+
- The child is read-only against the repository; its only write is its `output` file.
|
|
25
|
+
- You write `## Intent`, `## Decisions`, `## Open questions`, and `## Skills loaded` from your own transcript, before the call and after it returns. Do not fork a child for this: forking a near-full transcript is the cost this handoff avoids.
|
|
26
|
+
|
|
27
|
+
## Run worktree
|
|
28
|
+
|
|
29
|
+
Before the snapshot dispatch, determine the run worktree from your own transcript and interpolate it into the scout task as `Run worktree: <abs path>` or `Run worktree: none`:
|
|
30
|
+
|
|
31
|
+
- A flow-level worktree the run created (any `git worktree add <path>` you ran, or a `Worktree ready at <path>` report from the skill that set up the worktree) or was told to work in (a resume brief's `worktree: yes <path>`, or a user instruction).
|
|
32
|
+
- Never inferred from the session cwd; a created or assigned worktree still qualifies when it happens to equal the cwd. Never a per-task worktree from `tasks[].worktree: true` or `worktree: true` dispatches - those are ephemeral.
|
|
33
|
+
- Two flow-level worktrees in the transcript: the candidate is the one most recently used by later work (a dispatch `cwd`, a `git -C <path>`, or a `cd <path>`); if that does not distinguish them, the most recent creation or assignment. The other goes into `## Open questions` as `Also seen: <path> - not recorded as the run worktree`.
|
|
34
|
+
- No flow-level worktree created or named: `Run worktree: none`.
|
|
35
|
+
|
|
36
|
+
## Repo snapshot
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
subagent({ async: false, context: "fresh", tasks: [
|
|
40
|
+
{ agent: "scout", cwd: "<cwd>", reads: false, output: "<SCRATCH>/repo.md",
|
|
41
|
+
task: "Read-only; write only to your output path. Run worktree: <abs path|none>. Steps: (1) linked set: git worktree list --porcelain | awk '/^worktree /{print substr($0,10)}' - the first line is the primary checkout, the rest are the linked worktrees; a relative candidate resolves against the primary toplevel, not cwd. (2) candidate `none` or line absent -> target is cwd, go to (4). Otherwise if the candidate is relative, prefix it with the primary toplevel (first linked-set line) first; C=$(git -C <candidate> rev-parse --show-toplevel); valid when that succeeds and C equals one of the linked lines exactly (a line after the first; equality with the first, primary line is invalid); anything else (failure, not listed, the primary itself, prunable) is invalid -> target is cwd and emit the open-question line in (5). (3) valid -> target is $C and `worktree: yes $C`. (4) target cwd -> T=$(git rev-parse --show-toplevel); `worktree: yes $T` when $T equals one of the linked lines exactly (a line after the first; the first, primary line gives `worktree: no`), else `worktree: no`. (5) report each field on its own line, `unavailable` when a command fails, every git command as `git -C <target>`: toplevel (git -C <target> rev-parse --show-toplevel); the worktree line from (3) or (4); branch (git -C <target> branch --show-current, `detached` when empty); HEAD SHA; base (origin/HEAD short name, else origin/main or origin/master if present, else `base: unknown`); dirty (git -C <target> status --porcelain, or `clean`); diff-stat (git -C <target> diff --stat <base>...HEAD; `diff-stat: unavailable` when base is unknown); test command (<target>/package.json scripts or <target>/AGENTS.md). Invalid candidate: after the fields add the single line `open-question: producer named <candidate as submitted> as the run worktree; it is not a linked worktree of this repo`. cwd not a git repo -> the single line `not a git repo`, plus the same open-question line if a candidate was submitted." }
|
|
42
|
+
]})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Outcomes: the fields become `## Repo state`; any `open-question:` line goes into `## Open questions` without its prefix and never into `## Repo state`; `not a git repo` -> `## Repo state: not a git repo`; child failed, file missing or empty, or async handle -> `## Repo state: unavailable (<one-line reason>)`. The brief is written in every outcome - the transcript-derived sections are the point of a handoff.
|
|
46
|
+
|
|
47
|
+
## Destination
|
|
48
|
+
|
|
49
|
+
Resolve after the snapshot (the default key needs its `worktree:` line). Precedence: `--out`, then `--key`, then the default key.
|
|
50
|
+
|
|
51
|
+
- `--out <path>`: that path, made absolute. Its parent directory must exist, else `Handoff not written: parent directory <dir> does not exist`; never fall back to the default when the caller named a path.
|
|
52
|
+
- `--key <stem>`: an exact caller-owned basename stem, written verbatim (no encoding) with `.md` appended - `--key foo.md` gives `foo.md.md`. Valid stem: non-empty after trimming, not `.` or `..`, none of `/`, `\`, NUL, control characters, or `<>:"|?*`, and not starting with `<primary>--` (the default-key prefix). Invalid -> `Handoff not written: --key is not a valid file name stem`; prefixed -> `Handoff not written: --key is reserved for the default key`. Destination `path.join(<tmpdir>, 'pi-handoff', <stem> + '.md')`.
|
|
53
|
+
- Default key `<primary>--<leaf>` at `<tmpdir>/pi-handoff/<primary>--<leaf>.md`, overwritten on each run:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
TMP=$(node -p "require('os').tmpdir()")
|
|
57
|
+
PRIMARY=$(basename "$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")")
|
|
58
|
+
# LEAF: basename of the run worktree when the snapshot says `worktree: yes <path>`;
|
|
59
|
+
# else `git branch --show-current`; else `detached`
|
|
60
|
+
enc() { printf '%s' "$1" | sed 's/[^A-Za-z0-9._-]/-/g'; }
|
|
61
|
+
OUT="$TMP/pi-handoff/$(enc "$PRIMARY")--$(enc "$LEAF").md"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Outside a git repo the key is `no-repo--<encoded cwd basename>`. The encoder `[^A-Za-z0-9._-]` -> `-` applies to the two generated components only, never to a `--key` value. Examples: primary checkout on `main` with no run worktree -> `pi-cohort--main.md`; run worktree `.worktrees/gh-18-handoff-skill` -> `pi-cohort--gh-18-handoff-skill.md`; branch `feat/x` -> `pi-cohort--feat-x.md`.
|
|
65
|
+
- For `--key` and the default, `mkdir -p "$TMP/pi-handoff"` first. Paths are joined with `/`; `os.tmpdir()` covers Windows (`%LOCALAPPDATA%\Temp`).
|
|
66
|
+
|
|
67
|
+
## Brief
|
|
68
|
+
|
|
69
|
+
```markdown
|
|
70
|
+
# Handoff: <one line>
|
|
71
|
+
## Intent
|
|
72
|
+
## Repo state
|
|
73
|
+
## Decisions
|
|
74
|
+
## Open questions
|
|
75
|
+
## Skills loaded
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- `## Repo state`: the snapshot fields, or one of the heading variants above.
|
|
79
|
+
- `## Decisions`: bullets; rejected alternatives marked `rejected:`.
|
|
80
|
+
- `## Open questions`: bullets, including any copied `open-question:` line and any `Also seen:` line.
|
|
81
|
+
- `## Skills loaded`: the frontmatter `name` of every skill whose SKILL.md body is in your context (a `<skill>` block or a tool result on a `*/SKILL.md` path); it always includes `handoff`.
|
|
82
|
+
|
|
83
|
+
Write nothing after `## Skills loaded`: a caller that follows this skill and continues in the same session may append its own `##` sections there. Then `rm -rf "$SCRATCH"`, echo the brief, and end the reply with the line `Handoff written: <abs path>`.
|
|
@@ -42,7 +42,7 @@ export function serializeAgent(config: AgentConfig): string {
|
|
|
42
42
|
if (config.model) lines.push(`model: ${config.model}`);
|
|
43
43
|
const fallbackModelsValue = joinComma(config.fallbackModels);
|
|
44
44
|
if (fallbackModelsValue) lines.push(`fallbackModels: ${fallbackModelsValue}`);
|
|
45
|
-
if (config.thinking
|
|
45
|
+
if (config.thinking) lines.push(`thinking: ${config.thinking}`);
|
|
46
46
|
lines.push(`systemPromptMode: ${config.systemPromptMode}`);
|
|
47
47
|
lines.push(`inheritProjectContext: ${config.inheritProjectContext ? "true" : "false"}`);
|
|
48
48
|
lines.push(`inheritSkills: ${config.inheritSkills ? "true" : "false"}`);
|
|
@@ -51,6 +51,10 @@ interface AsyncExecutionContext {
|
|
|
51
51
|
cwd: string;
|
|
52
52
|
currentSessionId: string;
|
|
53
53
|
currentModelProvider?: string;
|
|
54
|
+
/** Parent session model as provider/id, captured at dispatch */
|
|
55
|
+
parentModel?: string;
|
|
56
|
+
/** Parent session thinking level, captured at dispatch */
|
|
57
|
+
parentThinking?: string;
|
|
54
58
|
executionBackend?: string;
|
|
55
59
|
}
|
|
56
60
|
|
|
@@ -355,8 +359,10 @@ export function executeAsyncChain(
|
|
|
355
359
|
taskTemplate = taskTemplate.replace(/\{chain_dir\}/g, runnerCwd);
|
|
356
360
|
const task = injectSingleOutputInstruction(`${readInstructions.prefix}${taskTemplate}${progressInstructions.suffix}`, outputPath);
|
|
357
361
|
|
|
358
|
-
const
|
|
359
|
-
const
|
|
362
|
+
const primaryRaw = behavior.model ?? a.model ?? ctx.parentModel;
|
|
363
|
+
const thinking = a.thinking ?? ctx.parentThinking;
|
|
364
|
+
const primaryModel = resolveModelCandidate(primaryRaw, availableModels, ctx.currentModelProvider);
|
|
365
|
+
const model = applyThinkingSuffix(primaryModel, thinking);
|
|
360
366
|
return {
|
|
361
367
|
agent: s.agent,
|
|
362
368
|
task,
|
|
@@ -366,9 +372,9 @@ export function executeAsyncChain(
|
|
|
366
372
|
structured: Boolean(s.outputSchema),
|
|
367
373
|
cwd: stepCwd,
|
|
368
374
|
model,
|
|
369
|
-
thinking: resolveEffectiveThinking(model,
|
|
370
|
-
modelCandidates: buildModelCandidates(
|
|
371
|
-
applyThinkingSuffix(candidate,
|
|
375
|
+
thinking: resolveEffectiveThinking(model, thinking),
|
|
376
|
+
modelCandidates: buildModelCandidates(primaryRaw, a.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
|
|
377
|
+
applyThinkingSuffix(candidate, thinking),
|
|
372
378
|
),
|
|
373
379
|
tools: a.tools,
|
|
374
380
|
extensions: a.extensions,
|
|
@@ -670,10 +676,9 @@ export function executeAsyncSingle(
|
|
|
670
676
|
const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
|
|
671
677
|
if (validationError) return formatAsyncStartError("single", validationError);
|
|
672
678
|
const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath);
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
);
|
|
679
|
+
const primaryRaw = params.modelOverride ?? agentConfig.model ?? ctx.parentModel;
|
|
680
|
+
const thinking = agentConfig.thinking ?? ctx.parentThinking;
|
|
681
|
+
const model = applyThinkingSuffix(resolveModelCandidate(primaryRaw, availableModels, ctx.currentModelProvider), thinking);
|
|
677
682
|
let spawnResult: { pid?: number; error?: string } = {};
|
|
678
683
|
try {
|
|
679
684
|
spawnResult = spawnRunner(
|
|
@@ -685,9 +690,9 @@ export function executeAsyncSingle(
|
|
|
685
690
|
task: taskWithOutputInstruction,
|
|
686
691
|
cwd: runnerCwd,
|
|
687
692
|
model,
|
|
688
|
-
thinking: resolveEffectiveThinking(model,
|
|
689
|
-
modelCandidates: buildModelCandidates(
|
|
690
|
-
applyThinkingSuffix(candidate,
|
|
693
|
+
thinking: resolveEffectiveThinking(model, thinking),
|
|
694
|
+
modelCandidates: buildModelCandidates(primaryRaw, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
|
|
695
|
+
applyThinkingSuffix(candidate, thinking),
|
|
691
696
|
),
|
|
692
697
|
tools: agentConfig.tools,
|
|
693
698
|
extensions: agentConfig.extensions,
|
|
@@ -11,7 +11,7 @@ import { matchesKey, visibleWidth, truncateToWidth } from "@earendil-works/pi-tu
|
|
|
11
11
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
12
12
|
import type { ResolvedStepBehavior } from "../../shared/settings.ts";
|
|
13
13
|
import { resolveModelCandidate, splitThinkingSuffix } from "../shared/model-fallback.ts";
|
|
14
|
-
import { findModelInfo, getSupportedThinkingLevels, type ModelInfo, type ThinkingLevel } from "../../shared/model-info.ts";
|
|
14
|
+
import { findModelInfo, getSupportedThinkingLevels, splitKnownThinkingSuffix, type ModelInfo, type ThinkingLevel } from "../../shared/model-info.ts";
|
|
15
15
|
|
|
16
16
|
type ClarifyMode = 'single' | 'parallel' | 'chain';
|
|
17
17
|
|
|
@@ -696,9 +696,8 @@ export class ChainClarifyComponent implements Component {
|
|
|
696
696
|
const currentModel = this.getEffectiveBehavior(stepIndex).model;
|
|
697
697
|
if (!currentModel) return;
|
|
698
698
|
|
|
699
|
-
const { baseModel } =
|
|
700
|
-
|
|
701
|
-
this.updateBehavior(stepIndex, "model", newModel);
|
|
699
|
+
const { baseModel } = splitKnownThinkingSuffix(currentModel);
|
|
700
|
+
this.updateBehavior(stepIndex, "model", `${baseModel}:${level}`);
|
|
702
701
|
}
|
|
703
702
|
|
|
704
703
|
private filterSkills(): void {
|
|
@@ -1009,7 +1008,8 @@ export class ChainClarifyComponent implements Component {
|
|
|
1009
1008
|
"low": "Light reasoning",
|
|
1010
1009
|
"medium": "Moderate reasoning",
|
|
1011
1010
|
"high": "Deep reasoning",
|
|
1012
|
-
"xhigh": "
|
|
1011
|
+
"xhigh": "Extra-deep reasoning (ultrathink)",
|
|
1012
|
+
"max": "Provider maximum reasoning",
|
|
1013
1013
|
};
|
|
1014
1014
|
|
|
1015
1015
|
const levels = this.getAvailableThinkingLevels(this.editingStep!);
|
|
@@ -8,7 +8,7 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
|
8
8
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
10
10
|
import { ChainClarifyComponent, type ChainClarifyResult, type BehaviorOverride } from "./chain-clarify.ts";
|
|
11
|
-
import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
|
|
11
|
+
import { parentModelFullId, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
|
|
12
12
|
import {
|
|
13
13
|
resolveChainTemplates,
|
|
14
14
|
createChainDir,
|
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
resolveChildMaxSubagentDepth,
|
|
59
59
|
} from "../../shared/types.ts";
|
|
60
60
|
import { resolveModelCandidate } from "../shared/model-fallback.ts";
|
|
61
|
+
import { applyThinkingSuffix } from "../shared/pi-args.ts";
|
|
61
62
|
import { validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
62
63
|
import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
|
|
63
64
|
import { ChainOutputValidationError, outputEntryFromResult, resolveOutputReferences, validateChainOutputBindings } from "../shared/chain-outputs.ts";
|
|
@@ -278,6 +279,8 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
278
279
|
modelOverride: effectiveModel,
|
|
279
280
|
availableModels: input.availableModels,
|
|
280
281
|
preferredModelProvider: input.ctx.model?.provider,
|
|
282
|
+
parentModel: parentModelFullId(input.ctx.model),
|
|
283
|
+
parentThinking: input.ctx.thinkingLevel,
|
|
281
284
|
skills: behavior.skills === false ? [] : behavior.skills,
|
|
282
285
|
structuredOutput: structuredRuntime,
|
|
283
286
|
acceptance: task.acceptance,
|
|
@@ -512,6 +515,14 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
512
515
|
resolveStepBehavior(config, stepOverrides[i]!, chainSkills),
|
|
513
516
|
);
|
|
514
517
|
const flatTemplates = templates as string[];
|
|
518
|
+
// Seed what dispatch will resolve so the screen shows it and the thinking selector is enabled;
|
|
519
|
+
// untouched steps still reach runSync with no override and inherit there.
|
|
520
|
+
const parentModel = parentModelFullId(ctx.model);
|
|
521
|
+
const seededBehaviors = resolvedBehaviors.map((behavior, i) => {
|
|
522
|
+
const model = behavior.model ?? parentModel;
|
|
523
|
+
if (!model) return behavior;
|
|
524
|
+
return { ...behavior, model: applyThinkingSuffix(model, agentConfigs[i]!.thinking ?? ctx.thinkingLevel) };
|
|
525
|
+
});
|
|
515
526
|
|
|
516
527
|
const result = await ctx.ui.custom<ChainClarifyResult>(
|
|
517
528
|
(tui, theme, _kb, done) =>
|
|
@@ -522,7 +533,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
522
533
|
flatTemplates,
|
|
523
534
|
originalTask,
|
|
524
535
|
chainDir,
|
|
525
|
-
|
|
536
|
+
seededBehaviors,
|
|
526
537
|
availableModels,
|
|
527
538
|
ctx.model?.provider,
|
|
528
539
|
availableSkills,
|
|
@@ -1030,6 +1041,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
1030
1041
|
modelOverride: effectiveModel,
|
|
1031
1042
|
availableModels,
|
|
1032
1043
|
preferredModelProvider: ctx.model?.provider,
|
|
1044
|
+
parentModel: parentModelFullId(ctx.model),
|
|
1045
|
+
parentThinking: ctx.thinkingLevel,
|
|
1033
1046
|
skills: behavior.skills === false ? [] : behavior.skills,
|
|
1034
1047
|
structuredOutput: structuredRuntime,
|
|
1035
1048
|
acceptance: seqStep.acceptance,
|
|
@@ -684,11 +684,12 @@ export async function runSync(
|
|
|
684
684
|
}
|
|
685
685
|
|
|
686
686
|
const candidates = buildModelCandidates(
|
|
687
|
-
options.modelOverride ?? agent.model,
|
|
687
|
+
options.modelOverride ?? agent.model ?? options.parentModel,
|
|
688
688
|
agent.fallbackModels,
|
|
689
689
|
options.availableModels,
|
|
690
690
|
options.preferredModelProvider,
|
|
691
691
|
);
|
|
692
|
+
const effectiveAgent: AgentConfig = { ...agent, thinking: agent.thinking ?? options.parentThinking };
|
|
692
693
|
const attemptedModels: string[] = [];
|
|
693
694
|
const modelAttempts: ModelAttempt[] = [];
|
|
694
695
|
const aggregateUsage = emptyUsage();
|
|
@@ -751,7 +752,7 @@ export async function runSync(
|
|
|
751
752
|
owner: externalOwner,
|
|
752
753
|
attemptId: `${childId}-attempt-${i}`,
|
|
753
754
|
runtimeCwd,
|
|
754
|
-
agent,
|
|
755
|
+
agent: effectiveAgent,
|
|
755
756
|
model: candidate,
|
|
756
757
|
task: taskWithAcceptance,
|
|
757
758
|
options,
|
|
@@ -762,7 +763,7 @@ export async function runSync(
|
|
|
762
763
|
}
|
|
763
764
|
} else {
|
|
764
765
|
result = await (dependencies.runNativeAttempt ?? runSingleAttempt)(
|
|
765
|
-
runtimeCwd,
|
|
766
|
+
runtimeCwd, effectiveAgent, taskWithAcceptance, candidate, options, shared,
|
|
766
767
|
);
|
|
767
768
|
}
|
|
768
769
|
lastResult = result;
|
|
@@ -6,7 +6,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
6
6
|
import type { AgentConfig, AgentScope } from "../../agents/agents.ts";
|
|
7
7
|
import { getArtifactsDir } from "../../shared/artifacts.ts";
|
|
8
8
|
import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
|
|
9
|
-
import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
|
|
9
|
+
import { parentModelFullId, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
|
|
10
10
|
import { executeChain } from "./chain-execution.ts";
|
|
11
11
|
import { resolveExecutionAgentScope } from "../../agents/agent-scope.ts";
|
|
12
12
|
import { handleManagementAction } from "../../agents/agent-management.ts";
|
|
@@ -210,6 +210,18 @@ function nestedResolutionScopeForExecutor(deps: ExecutorDeps): NestedRunResoluti
|
|
|
210
210
|
};
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
function buildAsyncContext(ctx: ExtensionContext, deps: ExecutorDeps, cwd: string) {
|
|
214
|
+
return {
|
|
215
|
+
pi: deps.pi,
|
|
216
|
+
cwd,
|
|
217
|
+
currentSessionId: deps.state.currentSessionId!,
|
|
218
|
+
currentModelProvider: ctx.model?.provider,
|
|
219
|
+
parentModel: parentModelFullId(ctx.model),
|
|
220
|
+
parentThinking: ctx.thinkingLevel,
|
|
221
|
+
executionBackend: deps.config.executionBackend,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
213
225
|
function foregroundStatusResult(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): AgentToolResult<Details> {
|
|
214
226
|
let nestedWarning: string | undefined;
|
|
215
227
|
try {
|
|
@@ -591,13 +603,7 @@ async function resumeAsyncRun(input: {
|
|
|
591
603
|
agent: target.agent,
|
|
592
604
|
task: buildRevivedAsyncTask(target, followUp),
|
|
593
605
|
agentConfig,
|
|
594
|
-
ctx:
|
|
595
|
-
pi: input.deps.pi,
|
|
596
|
-
cwd: input.requestCwd,
|
|
597
|
-
currentSessionId: input.deps.state.currentSessionId,
|
|
598
|
-
currentModelProvider: input.ctx.model?.provider,
|
|
599
|
-
executionBackend: input.deps.config.executionBackend,
|
|
600
|
-
},
|
|
606
|
+
ctx: buildAsyncContext(input.ctx, input.deps, input.requestCwd),
|
|
601
607
|
cwd: effectiveCwd,
|
|
602
608
|
maxOutput: input.params.maxOutput,
|
|
603
609
|
artifactsDir: input.deps.tempArtifactsDir,
|
|
@@ -974,13 +980,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
974
980
|
};
|
|
975
981
|
}
|
|
976
982
|
const id = randomUUID();
|
|
977
|
-
const asyncCtx =
|
|
978
|
-
pi: deps.pi,
|
|
979
|
-
cwd: ctx.cwd,
|
|
980
|
-
currentSessionId: deps.state.currentSessionId!,
|
|
981
|
-
currentModelProvider: ctx.model?.provider,
|
|
982
|
-
executionBackend: deps.config.executionBackend,
|
|
983
|
-
};
|
|
983
|
+
const asyncCtx = buildAsyncContext(ctx, deps, ctx.cwd);
|
|
984
984
|
const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
|
|
985
985
|
const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
|
|
986
986
|
const currentProvider = ctx.model?.provider;
|
|
@@ -1164,13 +1164,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1164
1164
|
};
|
|
1165
1165
|
}
|
|
1166
1166
|
const id = randomUUID();
|
|
1167
|
-
const asyncCtx =
|
|
1168
|
-
pi: deps.pi,
|
|
1169
|
-
cwd: ctx.cwd,
|
|
1170
|
-
currentSessionId: deps.state.currentSessionId!,
|
|
1171
|
-
currentModelProvider: ctx.model?.provider,
|
|
1172
|
-
executionBackend: deps.config.executionBackend,
|
|
1173
|
-
};
|
|
1167
|
+
const asyncCtx = buildAsyncContext(ctx, deps, ctx.cwd);
|
|
1174
1168
|
const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, params.context);
|
|
1175
1169
|
return executeAsyncChain(id, {
|
|
1176
1170
|
chain: asyncChain,
|
|
@@ -1432,6 +1426,8 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
1432
1426
|
modelOverride: input.modelOverrides[index],
|
|
1433
1427
|
availableModels: input.availableModels,
|
|
1434
1428
|
preferredModelProvider: input.ctx.model?.provider,
|
|
1429
|
+
parentModel: parentModelFullId(input.ctx.model),
|
|
1430
|
+
parentThinking: input.ctx.thinkingLevel,
|
|
1435
1431
|
skills: effectiveSkills === false ? [] : effectiveSkills,
|
|
1436
1432
|
acceptance: task.acceptance,
|
|
1437
1433
|
acceptanceContext: { mode: "parallel" },
|
|
@@ -1605,13 +1601,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1605
1601
|
};
|
|
1606
1602
|
}
|
|
1607
1603
|
const id = randomUUID();
|
|
1608
|
-
const asyncCtx =
|
|
1609
|
-
pi: deps.pi,
|
|
1610
|
-
cwd: ctx.cwd,
|
|
1611
|
-
currentSessionId: deps.state.currentSessionId!,
|
|
1612
|
-
currentModelProvider: ctx.model?.provider,
|
|
1613
|
-
executionBackend: deps.config.executionBackend,
|
|
1614
|
-
};
|
|
1604
|
+
const asyncCtx = buildAsyncContext(ctx, deps, ctx.cwd);
|
|
1615
1605
|
const parallelTasks = tasks.map((t, i) => {
|
|
1616
1606
|
const taskText = params.context === "fork" ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
|
|
1617
1607
|
const progress = taskDisallowsFileUpdates(taskText) ? false : behaviorOverrides[i]?.progress;
|
|
@@ -1883,13 +1873,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1883
1873
|
};
|
|
1884
1874
|
}
|
|
1885
1875
|
const id = randomUUID();
|
|
1886
|
-
const asyncCtx =
|
|
1887
|
-
pi: deps.pi,
|
|
1888
|
-
cwd: ctx.cwd,
|
|
1889
|
-
currentSessionId: deps.state.currentSessionId!,
|
|
1890
|
-
currentModelProvider: ctx.model?.provider,
|
|
1891
|
-
executionBackend: deps.config.executionBackend,
|
|
1892
|
-
};
|
|
1876
|
+
const asyncCtx = buildAsyncContext(ctx, deps, ctx.cwd);
|
|
1893
1877
|
return executeAsyncSingle(id, {
|
|
1894
1878
|
agent: params.agent!,
|
|
1895
1879
|
task: params.context === "fork" ? wrapForkTask(task) : task,
|
|
@@ -1993,6 +1977,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1993
1977
|
modelOverride,
|
|
1994
1978
|
availableModels,
|
|
1995
1979
|
preferredModelProvider: currentProvider,
|
|
1980
|
+
parentModel: parentModelFullId(ctx.model),
|
|
1981
|
+
parentThinking: ctx.thinkingLevel,
|
|
1996
1982
|
skills: effectiveSkills,
|
|
1997
1983
|
acceptance: params.acceptance,
|
|
1998
1984
|
acceptanceContext: { mode: "single" },
|
|
@@ -6,7 +6,7 @@ import { encodeNestedPathEnv, parseNestedPathEnv, type NestedPathEntry } from ".
|
|
|
6
6
|
import { STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, STRUCTURED_OUTPUT_TOOL_NAME } from "./structured-output.ts";
|
|
7
7
|
import type { JsonSchemaObject } from "../../shared/types.ts";
|
|
8
8
|
|
|
9
|
-
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
9
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
10
10
|
const TASK_ARG_LIMIT = 8000;
|
|
11
11
|
const PROMPT_RUNTIME_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-prompt-runtime.ts");
|
|
12
12
|
const FANOUT_CHILD_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "extension", "fanout-child.ts");
|
|
@@ -73,7 +73,7 @@ export function runDirEnv(asyncDir: string): Record<string, string> {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
export function applyThinkingSuffix(model: string | undefined, thinking: string | undefined): string | undefined {
|
|
76
|
-
if (!model || !thinking
|
|
76
|
+
if (!model || !thinking) return model;
|
|
77
77
|
const colonIdx = model.lastIndexOf(":");
|
|
78
78
|
if (colonIdx !== -1 && THINKING_LEVELS.includes(model.substring(colonIdx + 1))) return model;
|
|
79
79
|
return `${model}:${thinking}`;
|
package/src/shared/model-info.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
1
|
+
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
2
2
|
export type ThinkingLevel = typeof THINKING_LEVELS[number];
|
|
3
3
|
export type ThinkingLevelMap = Partial<Record<ThinkingLevel, string | null>>;
|
|
4
4
|
|
|
@@ -63,16 +63,21 @@ export function findModelInfo(model: string | undefined, availableModels: ModelI
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
export function getSupportedThinkingLevels(model: ModelInfo | undefined): ThinkingLevel[] {
|
|
66
|
-
if (!model) return
|
|
66
|
+
if (!model) return THINKING_LEVELS.filter((level) => level !== "max");
|
|
67
67
|
if (model.reasoning === false) return ["off"];
|
|
68
68
|
|
|
69
|
-
if (!model.thinkingLevelMap) return
|
|
69
|
+
if (!model.thinkingLevelMap) return THINKING_LEVELS.filter((level) => level !== "max");
|
|
70
70
|
|
|
71
71
|
const levels = THINKING_LEVELS.filter((level) => {
|
|
72
72
|
const mapped = model.thinkingLevelMap?.[level];
|
|
73
73
|
if (mapped === null) return false;
|
|
74
|
-
if (level === "xhigh") return mapped !== undefined;
|
|
74
|
+
if (level === "xhigh" || level === "max") return mapped !== undefined;
|
|
75
75
|
return true;
|
|
76
76
|
});
|
|
77
77
|
return levels;
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
/** Provider-qualified id of the parent session model; undefined when either part is missing (SDK hosts, minimal test contexts). */
|
|
81
|
+
export function parentModelFullId(model: { provider?: string; id?: string } | undefined): string | undefined {
|
|
82
|
+
return model?.provider && model.id ? `${model.provider}/${model.id}` : undefined;
|
|
83
|
+
}
|
package/src/shared/types.ts
CHANGED
|
@@ -776,6 +776,10 @@ export interface RunSyncOptions {
|
|
|
776
776
|
availableModels?: Array<{ provider: string; id: string; fullId: string }>;
|
|
777
777
|
/** Current parent-session provider to prefer for ambiguous bare model ids */
|
|
778
778
|
preferredModelProvider?: string;
|
|
779
|
+
/** Parent session model as provider/id; used when neither the call nor the agent names a model */
|
|
780
|
+
parentModel?: string;
|
|
781
|
+
/** Parent session thinking level; used when the agent sets no `thinking` */
|
|
782
|
+
parentThinking?: string;
|
|
779
783
|
/** Skills to inject (overrides agent default if provided) */
|
|
780
784
|
skills?: string[];
|
|
781
785
|
structuredOutput?: {
|
package/prompts/handoff.md
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Use when the context window is nearly full and the work must continue in a fresh session."
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Write a handoff brief for a fresh session. The template is the contract in `doc/handoff-template.md` of pi-cohort; section names are fixed.
|
|
6
|
-
|
|
7
|
-
Optional `--out <path>` (relative resolves against cwd):
|
|
8
|
-
|
|
9
|
-
$@
|
|
10
|
-
|
|
11
|
-
## Rules
|
|
12
|
-
|
|
13
|
-
- `DIR=$(mktemp -d)`. The dispatch: `async: false` and `context: "fresh"` at the top level; `reads: false`; absolute `output` under `$DIR`. If it returns an async handle, a `forceTopLevelAsync` setting is active: stop and report it. Do not poll, relaunch, or continue.
|
|
14
|
-
- The child is read-only against the repository; its only write is its `output` file.
|
|
15
|
-
- You write `## Intent`, `## Decisions`, `## Open questions`, and `## Skills loaded` from your own transcript, before the call and after it returns. Do not fork a child for this: forking a near-full transcript is the cost this handoff avoids.
|
|
16
|
-
- Write nothing into the repository; the brief goes to `--out` or `$DIR/handoff.md`.
|
|
17
|
-
|
|
18
|
-
## Run worktree
|
|
19
|
-
|
|
20
|
-
Before the snapshot dispatch, determine the run worktree from your own transcript and interpolate it into the scout task as `Run worktree: <abs path>` or `Run worktree: none`:
|
|
21
|
-
|
|
22
|
-
- A flow-level worktree the run created (any `git worktree add <path>` you ran, or a `Worktree ready at <path>` report from `/skill:using-git-worktrees`) or was told to work in (a resume brief's `worktree: yes <path>`, or a user instruction).
|
|
23
|
-
- Never inferred from the session cwd; a created or assigned worktree still qualifies when it happens to equal the cwd. Never a per-task worktree from `tasks[].worktree: true` or `worktree: true` dispatches - those are ephemeral.
|
|
24
|
-
- Two flow-level worktrees in the transcript: the candidate is the one most recently used by later work (a dispatch `cwd`, a `git -C <path>`, or a `cd <path>`); if that does not distinguish them, the most recent creation or assignment. The other goes into `## Open questions` as `Also seen: <path> - not recorded as the run worktree`.
|
|
25
|
-
- No flow-level worktree created or named: `Run worktree: none`.
|
|
26
|
-
|
|
27
|
-
After the scout returns, copy any `open-question:` line from `repo.md` into `## Open questions` without the `open-question:` prefix; it never appears in `## Repo state`.
|
|
28
|
-
|
|
29
|
-
## Repo snapshot
|
|
30
|
-
|
|
31
|
-
```
|
|
32
|
-
subagent({ async: false, context: "fresh", tasks: [
|
|
33
|
-
{ agent: "scout", cwd: "<cwd>", reads: false, output: "<DIR>/repo.md",
|
|
34
|
-
task: "Read-only; write only to your output path. Run worktree: <abs path|none>. Steps: (1) linked set: git worktree list --porcelain | awk '/^worktree /{print substr($0,10)}' - the first line is the primary checkout, the rest are the linked worktrees; a relative candidate resolves against the primary toplevel, not cwd. (2) candidate `none` or line absent -> target is cwd, go to (4). Otherwise if the candidate is relative, prefix it with the primary toplevel (first linked-set line) first; C=$(git -C <candidate> rev-parse --show-toplevel); valid when that succeeds and C equals one of the linked lines exactly (a line after the first; equality with the first, primary line is invalid); anything else (failure, not listed, the primary itself, prunable) is invalid -> target is cwd and emit the open-question line in (5). (3) valid -> target is $C and `worktree: yes $C`. (4) target cwd -> T=$(git rev-parse --show-toplevel); `worktree: yes $T` when $T equals one of the linked lines exactly (a line after the first; the first, primary line gives `worktree: no`), else `worktree: no`. (5) report each field on its own line, `unavailable` when a command fails, every git command as `git -C <target>`: toplevel (git -C <target> rev-parse --show-toplevel); the worktree line from (3) or (4); branch (git -C <target> branch --show-current, `detached` when empty); HEAD SHA; base (origin/HEAD short name, else origin/main or origin/master if present, else `base: unknown`); dirty (git -C <target> status --porcelain, or `clean`); diff-stat (git -C <target> diff --stat <base>...HEAD; `diff-stat: unavailable` when base is unknown); test command (<target>/package.json scripts or <target>/AGENTS.md). Invalid candidate: after the fields add the single line `open-question: producer named <candidate as submitted> as the run worktree; it is not a linked worktree of this repo`. cwd not a git repo -> the single line `not a git repo`, plus the same open-question line if a candidate was submitted." }
|
|
35
|
-
]})
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Process state
|
|
39
|
-
|
|
40
|
-
Include `## Process state` only when both `phase_tracker` and `plan_tracker` tools exist AND `phase_tracker status` shows a phase `in_progress`. Then: `phase_tracker status` and `plan_tracker status` outputs verbatim; `Active task: <task name>` (the task you are working on; `none` when no plan is active or no task is active); the line `Gate history not restored - re-validate before advancing.`. Tools absent, all phases pending, or hotfix flow -> omit the section.
|
|
41
|
-
|
|
42
|
-
## Brief
|
|
43
|
-
|
|
44
|
-
```markdown
|
|
45
|
-
# Handoff: <one line>
|
|
46
|
-
## Intent
|
|
47
|
-
## Repo state (the snapshot fields; `## Repo state: not a git repo` when so)
|
|
48
|
-
## Decisions (bullets; rejected alternatives marked `rejected:`)
|
|
49
|
-
## Open questions
|
|
50
|
-
## Skills loaded (frontmatter `name` of every skill whose SKILL.md body is in context - a `<skill>` block or a tool result on a `*/SKILL.md` path; `## Skills loaded: none` when there are none)
|
|
51
|
-
## Process state (only per the rule above)
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Write it, print the path and the brief.
|