pi-gauntlet 7.26.0 → 7.27.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,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v7.27.0 - 2026-10-22
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **The phase widget labels the in-progress phase from the dispatches it observes.** A `subagent` call of a known persona (`scout`, `context-builder`, `spec-council-*`, `spec-reviewer`, `plan-writer`, `implementer`, `code-reviewer`, `conformance-reviewer`) or a `plan_check` call sets the phase's substep while the dispatch runs - `verify(conformance)`, `implement(implement+sr)` - and the result clears it; a repeated label in the same phase renders a widget-only ordinal, `verify(conformance #2)`. Skills are unchanged, `status` and the resume brief show the bare label, and session replay restores only labels set by an explicit `substep` call. Vocabulary in `doc/configuration.md`.
|
|
8
|
+
|
|
3
9
|
## v7.26.0 - 2026-10-22
|
|
4
10
|
|
|
5
11
|
### Changed
|
|
@@ -6,6 +6,8 @@ import { dirname, join } from "node:path";
|
|
|
6
6
|
import {
|
|
7
7
|
CONTEXT_DRAFT_MARKER,
|
|
8
8
|
checkSubstep,
|
|
9
|
+
dispatchAgents,
|
|
10
|
+
dispatchLabel,
|
|
9
11
|
closureGateBlocks,
|
|
10
12
|
closureModelGuardApplies,
|
|
11
13
|
flowGuardApplies,
|
|
@@ -80,6 +82,57 @@ test("checkSubstep: non-in_progress statuses -> error naming actual status", ()
|
|
|
80
82
|
test("phaseLabel: with and without substep", () => {
|
|
81
83
|
assert.equal(phaseLabel("brainstorm", "gather"), "brainstorm(gather)");
|
|
82
84
|
assert.equal(phaseLabel("brainstorm", undefined), "brainstorm");
|
|
85
|
+
assert.equal(phaseLabel("brainstorm", undefined, 2), "brainstorm");
|
|
86
|
+
for (const count of [undefined, 0, 1, 2]) {
|
|
87
|
+
assert.equal(phaseLabel("verify", "conformance", count), count === 2 ? "verify(conformance #2)" : "verify(conformance)");
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("dispatchAgents: walks dispatch shapes in order and ignores non-string agents", () => {
|
|
92
|
+
const cases: [string, unknown, string[]][] = [
|
|
93
|
+
["single", { agent: "scout" }, ["scout"]],
|
|
94
|
+
["tasks", { tasks: [{ agent: "scout" }, { agent: "context-builder" }] }, ["scout", "context-builder"]],
|
|
95
|
+
["recursive chain", { chain: [{ chain: [{ agent: "plan-writer" }] }] }, ["plan-writer"]],
|
|
96
|
+
["root and nested", { agent: "root", tasks: [{ agent: "task" }], chain: [{ agent: "step", parallel: [{ agent: "implementer" }, { agent: "spec-reviewer" }] }] }, ["root", "task", "step", "implementer", "spec-reviewer"]],
|
|
97
|
+
["bare top-level array", [{ agent: "scout" }], []],
|
|
98
|
+
["nested array ignored", { tasks: [[{ agent: "scout" }]] }, []],
|
|
99
|
+
["object nodes", { tasks: { agent: "context-builder" } }, ["context-builder"]],
|
|
100
|
+
["malformed", { agent: 42, tasks: [null, "scout", { agent: false }, { agent: "unknown" }] }, ["unknown"]],
|
|
101
|
+
["null", null, []],
|
|
102
|
+
];
|
|
103
|
+
for (const [name, input, expected] of cases) assert.deepEqual(dispatchAgents(input), expected, name);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("dispatchLabel: maps the vocabulary and combined implementation dispatch", () => {
|
|
107
|
+
const cases: [string, unknown, string][] = [
|
|
108
|
+
["subagent", { agent: "scout" }, "gather"],
|
|
109
|
+
["subagent", { agent: "context-builder" }, "gather"],
|
|
110
|
+
["subagent", { agent: "spec-council-design" }, "council"],
|
|
111
|
+
["subagent", { agent: "spec-council-security" }, "council"],
|
|
112
|
+
["subagent", { agent: "spec-reviewer" }, "spec-review"],
|
|
113
|
+
["subagent", { agent: "plan-writer" }, "plan-write"],
|
|
114
|
+
["plan_check", {}, "plan-check"],
|
|
115
|
+
["subagent", { agent: "implementer" }, "implement"],
|
|
116
|
+
["subagent", { agent: "code-reviewer" }, "code-review"],
|
|
117
|
+
["subagent", { agent: "conformance-reviewer" }, "conformance"],
|
|
118
|
+
["subagent", { chain: [{ parallel: [{ agent: "implementer" }] }, { parallel: [{ agent: "spec-reviewer" }] }] }, "implement+sr"],
|
|
119
|
+
["subagent", { tasks: [{ agent: "worker" }, { agent: "scout" }] }, "gather"],
|
|
120
|
+
["subagent", { tasks: [{ agent: "scout" }, { agent: "code-reviewer" }] }, "gather"],
|
|
121
|
+
["subagent", { tasks: [{ agent: "code-reviewer" }, { agent: "scout" }] }, "code-review"],
|
|
122
|
+
["subagent", { tasks: [{ agent: "scout" }, { agent: "spec-reviewer" }, { agent: "implementer" }] }, "implement+sr"],
|
|
123
|
+
];
|
|
124
|
+
for (const [tool, input, expected] of cases) assert.equal(dispatchLabel(tool, input), expected);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("dispatchLabel: ignores management, unmapped agents, other tools and malformed input", () => {
|
|
128
|
+
const inputs: unknown[] = [
|
|
129
|
+
{ agent: "worker" }, { agent: "spec-summarizer" }, { agent: "ad-hoc" },
|
|
130
|
+
{ agent: "spec-council" }, { agent: "implementer-extra" },
|
|
131
|
+
{ action: "list", agent: "scout" }, { action: undefined, agent: "scout" },
|
|
132
|
+
undefined, null, 1, "scout", {}, { agent: 1 }, { tasks: [null, false] },
|
|
133
|
+
];
|
|
134
|
+
for (const input of inputs) assert.equal(dispatchLabel("subagent", input), undefined);
|
|
135
|
+
assert.equal(dispatchLabel("tool_result", { agent: "scout" }), undefined);
|
|
83
136
|
});
|
|
84
137
|
|
|
85
138
|
test("parseGitCommand: branch subcommands with and without global flags", () => {
|
|
@@ -31,8 +31,54 @@ export function checkSubstep(phaseStatus: string): SubstepCheck {
|
|
|
31
31
|
return { ok: false, error: `substep requires an in_progress phase (status is ${phaseStatus})` };
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export
|
|
35
|
-
|
|
34
|
+
export const DISPATCH_LABELS = {
|
|
35
|
+
scout: "gather",
|
|
36
|
+
"context-builder": "gather",
|
|
37
|
+
"spec-council-": "council",
|
|
38
|
+
"spec-reviewer": "spec-review",
|
|
39
|
+
"plan-writer": "plan-write",
|
|
40
|
+
plan_check: "plan-check",
|
|
41
|
+
implementer: "implement",
|
|
42
|
+
"code-reviewer": "code-review",
|
|
43
|
+
"conformance-reviewer": "conformance",
|
|
44
|
+
"implement+sr": "implement+sr",
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
export function dispatchAgents(input: unknown): string[] {
|
|
48
|
+
const agents: string[] = [];
|
|
49
|
+
const collect = (node: unknown): void => {
|
|
50
|
+
if (!node || typeof node !== "object") return;
|
|
51
|
+
const object = node as Record<string, unknown>;
|
|
52
|
+
if (typeof object.agent === "string") agents.push(object.agent);
|
|
53
|
+
for (const key of ["tasks", "chain", "parallel"] as const) {
|
|
54
|
+
const value = object[key];
|
|
55
|
+
if (Array.isArray(value)) for (const item of value) collect(item);
|
|
56
|
+
else if (value && typeof value === "object") collect(value);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
collect(input);
|
|
60
|
+
return agents;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function dispatchLabel(toolName: string, input: unknown): string | undefined {
|
|
64
|
+
if (toolName === "plan_check") return DISPATCH_LABELS.plan_check;
|
|
65
|
+
if (toolName !== "subagent") return undefined;
|
|
66
|
+
if (input && typeof input === "object" && "action" in input) return undefined;
|
|
67
|
+
const agents = dispatchAgents(input);
|
|
68
|
+
if (agents.includes("implementer") && agents.includes("spec-reviewer")) return DISPATCH_LABELS["implement+sr"];
|
|
69
|
+
for (const agent of agents) {
|
|
70
|
+
for (const name of ["scout", "context-builder", "spec-reviewer", "plan-writer", "implementer", "code-reviewer", "conformance-reviewer"] as const) {
|
|
71
|
+
if (agent === name) return DISPATCH_LABELS[name];
|
|
72
|
+
}
|
|
73
|
+
if (agent.startsWith("spec-council-")) return DISPATCH_LABELS["spec-council-"];
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function phaseLabel(name: string, substep?: string, count?: number): string {
|
|
79
|
+
if (!substep) return name;
|
|
80
|
+
const ordinal = count !== undefined && count >= 2 ? ` #${count}` : "";
|
|
81
|
+
return `${name}(${substep}${ordinal})`;
|
|
36
82
|
}
|
|
37
83
|
|
|
38
84
|
// Same statement-start anchor as the branch guards in phase-tracker.ts. Exported
|
|
@@ -46,17 +46,17 @@ type Status = "pending" | "in_progress" | "complete" | "skipped";
|
|
|
46
46
|
const phases = (overrides: Partial<Record<Phase, Status>> = {}) =>
|
|
47
47
|
Object.fromEntries(PHASES.map((phase) => [phase, { status: overrides[phase] ?? "pending" }])) as Record<
|
|
48
48
|
Phase,
|
|
49
|
-
{ status: Status }
|
|
49
|
+
{ status: Status; substep?: string }
|
|
50
50
|
>;
|
|
51
51
|
|
|
52
|
-
const phaseResult = (action: string, state: Record<Phase, { status: Status }>) => ({
|
|
52
|
+
const phaseResult = (action: string, state: Record<Phase, { status: Status; substep?: string }>) => ({
|
|
53
53
|
type: "message",
|
|
54
54
|
message: { role: "toolResult", toolName: "phase_tracker", details: { action, phases: state } },
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
const assistant = (stopReason = "stop") => ({ type: "message", message: { role: "assistant", stopReason } });
|
|
58
58
|
|
|
59
|
-
const enteredBranch = (state: Record<Phase, { status: Status }>, extra: unknown[] = []) => [
|
|
59
|
+
const enteredBranch = (state: Record<Phase, { status: Status; substep?: string }>, extra: unknown[] = []) => [
|
|
60
60
|
phaseResult("start", phases({ brainstorm: "in_progress" })),
|
|
61
61
|
phaseResult("complete", state),
|
|
62
62
|
...extra,
|
|
@@ -71,15 +71,19 @@ const resumedBranch = (rest: Partial<Record<Phase, Status>>) => [
|
|
|
71
71
|
|
|
72
72
|
type TestModel = { provider: string; id: string };
|
|
73
73
|
|
|
74
|
-
function harness(options: { cwd?: string; branch?: unknown[]; idle?: boolean; beforeSettled?: (setIdle: (idle: boolean) => void) => void; sendThrows?: boolean; model?: TestModel; thinkingLevel?: string; registry?: Record<string, TestModel>; setModelResult?: boolean | Error } = {}) {
|
|
74
|
+
function harness(options: { hasUI?: boolean; cwd?: string; branch?: unknown[]; idle?: boolean; beforeSettled?: (setIdle: (idle: boolean) => void) => void; sendThrows?: boolean; model?: TestModel; thinkingLevel?: string; registry?: Record<string, TestModel>; setModelResult?: boolean | Error } = {}) {
|
|
75
75
|
const handlers = new Map<string, ((event: unknown, ctx: unknown) => unknown)[]>();
|
|
76
76
|
const tools: { name: string; executionMode?: string; parameters?: any; execute: (...args: any[]) => unknown }[] = [];
|
|
77
77
|
const sent: { message: any; options: any }[] = [];
|
|
78
|
+
const widgets: (string | undefined)[] = [];
|
|
78
79
|
let idle = options.idle ?? true;
|
|
79
80
|
let branch = options.branch ?? [];
|
|
80
81
|
const ctx = {
|
|
81
82
|
cwd: options.cwd ?? tempCwd(),
|
|
82
|
-
hasUI: false,
|
|
83
|
+
hasUI: options.hasUI ?? false,
|
|
84
|
+
ui: { setWidget: (name: string, factory: any) => {
|
|
85
|
+
if (name === "phase_tracker") widgets.push(factory?.({}, { fg: (_color: string, text: string) => text }).text);
|
|
86
|
+
} },
|
|
83
87
|
isIdle: () => idle,
|
|
84
88
|
sessionManager: { getBranch: () => branch },
|
|
85
89
|
model: options.model,
|
|
@@ -122,7 +126,7 @@ function harness(options: { cwd?: string; branch?: unknown[]; idle?: boolean; be
|
|
|
122
126
|
for (const handler of handlers.get(name) ?? []) results.push(await handler(event, ctx));
|
|
123
127
|
return results;
|
|
124
128
|
};
|
|
125
|
-
return { pi, emit, emitEvent, sent, tools, ctx, setIdle: (next: boolean) => (idle = next), setBranch: (next: unknown[]) => (branch = next) };
|
|
129
|
+
return { pi, emit, emitEvent, sent, tools, ctx, widgets, setIdle: (next: boolean) => (idle = next), setBranch: (next: unknown[]) => (branch = next) };
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
const settle = async (h: ReturnType<typeof harness>) => {
|
|
@@ -2064,3 +2068,335 @@ test("open-task nudge: silent on an isError result, a degraded settings read, an
|
|
|
2064
2068
|
process.env.PI_SUBAGENT_DEPTH = "0";
|
|
2065
2069
|
}
|
|
2066
2070
|
});
|
|
2071
|
+
|
|
2072
|
+
for (const action of ["complete", "skip"] as const) {
|
|
2073
|
+
for (const isError of [false, true]) {
|
|
2074
|
+
test(`live dispatch clears after ${action} of a different phase, error=${isError}`, async () => {
|
|
2075
|
+
const h = harness({ hasUI: true });
|
|
2076
|
+
const tool = h.tools.find(t => t.name === "phase_tracker")!;
|
|
2077
|
+
await startPhase(h, "verify");
|
|
2078
|
+
await h.emitEvent("tool_call", subagentCall("review", { agent: "code-reviewer" }));
|
|
2079
|
+
const result = await tool.execute("transition", { action, phase: "ship", reason: "not needed" }, undefined, undefined, h.ctx) as any;
|
|
2080
|
+
assert.equal(result.details.error, undefined);
|
|
2081
|
+
assert.match(h.widgets.at(-1)!, /verify\(code-review\)/);
|
|
2082
|
+
await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "review", isError, details: {}, content: [] });
|
|
2083
|
+
assert.doesNotMatch(h.widgets.at(-1)!, /verify\(/);
|
|
2084
|
+
await h.emitEvent("tool_call", subagentCall("next", { agent: "code-reviewer" }));
|
|
2085
|
+
assert.ok(h.widgets.at(-1)!.includes(`verify(code-review${isError ? "" : " #2"})`));
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
for (const isError of [false, true]) {
|
|
2091
|
+
test(`live dispatch survives a deferred start of a different phase, error=${isError}`, async () => {
|
|
2092
|
+
const h = harness({
|
|
2093
|
+
hasUI: true,
|
|
2094
|
+
cwd: tempCwd({ piGauntlet: { phaseModels: { ship: "p/m" } } }),
|
|
2095
|
+
model: M("old"),
|
|
2096
|
+
registry: { "p/m": M("m") },
|
|
2097
|
+
});
|
|
2098
|
+
let entered!: () => void;
|
|
2099
|
+
let release!: () => void;
|
|
2100
|
+
const modelEntered = new Promise<void>(resolve => { entered = resolve; });
|
|
2101
|
+
const modelReleased = new Promise<void>(resolve => { release = resolve; });
|
|
2102
|
+
h.pi.setModel = async () => {
|
|
2103
|
+
entered();
|
|
2104
|
+
await modelReleased;
|
|
2105
|
+
return true;
|
|
2106
|
+
};
|
|
2107
|
+
const shipStart = startPhase(h, "ship");
|
|
2108
|
+
await modelEntered;
|
|
2109
|
+
await startPhase(h, "verify");
|
|
2110
|
+
await h.emitEvent("tool_call", subagentCall("review", { agent: "code-reviewer" }));
|
|
2111
|
+
release();
|
|
2112
|
+
assert.equal((await shipStart).status, "in_progress");
|
|
2113
|
+
assert.match(h.widgets.at(-1)!, /verify\(code-review\)/);
|
|
2114
|
+
await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "review", isError, details: {}, content: [] });
|
|
2115
|
+
assert.doesNotMatch(h.widgets.at(-1)!, /verify\(/);
|
|
2116
|
+
const tool = h.tools.find(t => t.name === "phase_tracker")!;
|
|
2117
|
+
const status = await tool.execute("status", { action: "status" }, undefined, undefined, h.ctx) as any;
|
|
2118
|
+
assert.equal(status.details.phases.ship.status, "in_progress");
|
|
2119
|
+
await h.emitEvent("tool_call", subagentCall("next", { agent: "code-reviewer" }));
|
|
2120
|
+
assert.ok(h.widgets.at(-1)!.includes(`verify(code-review${isError ? "" : " #2"})`));
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
test("live dispatch labels repaint and keep ordinals out of status snapshots", async () => {
|
|
2125
|
+
const h = harness({ hasUI: true });
|
|
2126
|
+
const tool = h.tools.find(t => t.name === "phase_tracker")!;
|
|
2127
|
+
const start = await tool.execute("start", { action: "start", phase: "verify" }, undefined, undefined, h.ctx) as any;
|
|
2128
|
+
await h.emitEvent("tool_call", subagentCall("a", { agent: "conformance-reviewer" }));
|
|
2129
|
+
assert.match(h.widgets.at(-1)!, /verify\(conformance\)/);
|
|
2130
|
+
assert.equal(start.details.phases.verify.substep, undefined);
|
|
2131
|
+
await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "a", details: {}, content: [] });
|
|
2132
|
+
assert.doesNotMatch(h.widgets.at(-1)!, /verify\(/);
|
|
2133
|
+
await h.emitEvent("tool_call", subagentCall("b", { agent: "conformance-reviewer" }));
|
|
2134
|
+
assert.match(h.widgets.at(-1)!, /verify\(conformance #2\)/);
|
|
2135
|
+
const status = await tool.execute("status", { action: "status" }, undefined, undefined, h.ctx) as any;
|
|
2136
|
+
assert.match(status.content[0].text, /verify\(conformance\)/);
|
|
2137
|
+
assert.doesNotMatch(status.content[0].text, /#2/);
|
|
2138
|
+
const theme = { fg: (_color: string, text: string) => text };
|
|
2139
|
+
const renderer = (tool as any).renderResult;
|
|
2140
|
+
assert.doesNotMatch(renderer(status, {}, theme).text, /#2/);
|
|
2141
|
+
const explicit = await tool.execute("explicit", { action: "substep", phase: "verify", substep: "conformance" }, undefined, undefined, h.ctx) as any;
|
|
2142
|
+
assert.equal(renderer(explicit, {}, theme).text, "→ verify(conformance)");
|
|
2143
|
+
});
|
|
2144
|
+
|
|
2145
|
+
for (const row of [
|
|
2146
|
+
{ phase: "implement", input: { chain: [{ parallel: [{ agent: "implementer" }] }, { parallel: [{ agent: "spec-reviewer" }] }] }, label: "implement+sr" },
|
|
2147
|
+
{ phase: "implement", input: { agent: "code-reviewer" }, label: "code-review" },
|
|
2148
|
+
{ phase: "implement", input: { agent: "implementer" }, label: "implement" },
|
|
2149
|
+
{ phase: "implement", input: { agent: "spec-reviewer" }, label: "spec-review" },
|
|
2150
|
+
{ phase: "plan", toolName: "plan_check", input: {}, label: "plan-check" },
|
|
2151
|
+
{ phase: "verify", toolName: "plan_check", input: {}, label: "plan-check" },
|
|
2152
|
+
]) {
|
|
2153
|
+
test(`live vocabulary: ${row.phase}(${row.label})`, async () => {
|
|
2154
|
+
const h = harness({ hasUI: true });
|
|
2155
|
+
await startPhase(h, row.phase);
|
|
2156
|
+
await h.emitEvent("tool_call", { toolName: row.toolName ?? "subagent", toolCallId: "a", input: row.input });
|
|
2157
|
+
assert.ok(h.widgets.at(-1)!.includes(`${row.phase}(${row.label})`));
|
|
2158
|
+
const paints = h.widgets.length;
|
|
2159
|
+
for (const input of [{ agent: "worker" }, { action: "status", agent: "implementer" }]) {
|
|
2160
|
+
await h.emitEvent("tool_call", subagentCall("ignored", input));
|
|
2161
|
+
}
|
|
2162
|
+
await h.emitEvent("tool_execution_end", { toolName: "plan_tracker", toolCallId: "reopen", result: { details: { tasks: [{ status: "in_progress" }] } } });
|
|
2163
|
+
assert.ok(h.widgets.at(-1)!.includes(`${row.phase}(${row.label})`));
|
|
2164
|
+
assert.equal(h.widgets.length, paints + 1);
|
|
2165
|
+
await h.emitEvent("tool_execution_end", { toolName: row.toolName ?? "subagent", toolCallId: "a", result: { details: {} } });
|
|
2166
|
+
assert.ok(!h.widgets.at(-1)!.includes(`${row.phase}(`));
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
for (const agents of [
|
|
2171
|
+
["code-reviewer", "implementer", "code-reviewer", "conformance-reviewer"],
|
|
2172
|
+
["conformance-reviewer", "implementer", "conformance-reviewer", "code-reviewer", "code-reviewer", "code-reviewer"],
|
|
2173
|
+
]) {
|
|
2174
|
+
test(`live repair sequence: ${agents.join(" -> ")}`, async () => {
|
|
2175
|
+
const h = harness({ hasUI: true });
|
|
2176
|
+
await startPhase(h, "verify");
|
|
2177
|
+
const counts: Record<string, number> = {};
|
|
2178
|
+
for (const [i, agent] of agents.entries()) {
|
|
2179
|
+
const label = ({ "code-reviewer": "code-review", implementer: "implement", "conformance-reviewer": "conformance" })[agent]!;
|
|
2180
|
+
const n = counts[label] = (counts[label] ?? 0) + 1;
|
|
2181
|
+
await h.emitEvent("tool_call", subagentCall(String(i), { tasks: [{ agent }] }));
|
|
2182
|
+
assert.ok(h.widgets.at(-1)!.includes(`verify(${label}${n > 1 ? ` #${n}` : ""})`));
|
|
2183
|
+
await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: String(i), details: {}, content: [] });
|
|
2184
|
+
}
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
for (const row of [
|
|
2189
|
+
{ name: "normal", isError: false, details: {}, refund: false },
|
|
2190
|
+
{ name: "executed error", isError: true, details: { results: [] }, refund: false },
|
|
2191
|
+
{ name: "empty error", isError: true, details: {}, refund: true },
|
|
2192
|
+
]) {
|
|
2193
|
+
for (const endOnly of [false, true]) {
|
|
2194
|
+
test(`live terminal: ${row.name}, execution-end-only=${endOnly}`, async () => {
|
|
2195
|
+
const h = harness({ hasUI: true });
|
|
2196
|
+
await startPhase(h, "verify");
|
|
2197
|
+
await h.emitEvent("tool_call", subagentCall("a", { agent: "code-reviewer" }));
|
|
2198
|
+
const paints = h.widgets.length;
|
|
2199
|
+
await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "other", details: {}, content: [] });
|
|
2200
|
+
assert.equal(h.widgets.length, paints);
|
|
2201
|
+
const terminal = { toolName: "subagent", toolCallId: "a", isError: row.isError, details: row.details, content: [] };
|
|
2202
|
+
if (!endOnly) await h.emitEvent("tool_result", terminal);
|
|
2203
|
+
await h.emitEvent("tool_execution_end", { ...terminal, result: { details: row.details } });
|
|
2204
|
+
assert.equal(h.widgets.length, paints + 1);
|
|
2205
|
+
assert.doesNotMatch(h.widgets.at(-1)!, /verify\(/);
|
|
2206
|
+
await h.emitEvent("tool_execution_end", { ...terminal, result: { details: row.details } });
|
|
2207
|
+
assert.equal(h.widgets.length, paints + 1);
|
|
2208
|
+
await h.emitEvent("tool_call", subagentCall("b", { agent: "code-reviewer" }));
|
|
2209
|
+
assert.ok(h.widgets.at(-1)!.includes(`verify(code-review${row.refund ? "" : " #2"})`));
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
test("live overlap forgets older calls and explicit overwrite survives cleanup", async () => {
|
|
2215
|
+
const h = harness({ hasUI: true });
|
|
2216
|
+
await startPhase(h, "verify");
|
|
2217
|
+
const tool = h.tools.find(t => t.name === "phase_tracker")!;
|
|
2218
|
+
const explicit = (substep: string) => tool.execute("explicit", { action: "substep", phase: "verify", substep }, undefined, undefined, h.ctx);
|
|
2219
|
+
await explicit("before");
|
|
2220
|
+
for (const id of ["a", "b"]) await h.emitEvent("tool_call", subagentCall(id, { agent: "code-reviewer", async: true }));
|
|
2221
|
+
assert.match(h.widgets.at(-1)!, /verify\(code-review #2\)/);
|
|
2222
|
+
const paints = h.widgets.length;
|
|
2223
|
+
await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "a", details: {}, content: [] });
|
|
2224
|
+
assert.equal(h.widgets.length, paints);
|
|
2225
|
+
await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "b", details: {}, content: [] });
|
|
2226
|
+
assert.doesNotMatch(h.widgets.at(-1)!, /verify\(/);
|
|
2227
|
+
await h.emitEvent("tool_call", subagentCall("c", { agent: "code-reviewer" }));
|
|
2228
|
+
const snapshot = await explicit("custom") as any;
|
|
2229
|
+
await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "c", isError: true, details: {}, content: [] });
|
|
2230
|
+
assert.match(h.widgets.at(-1)!, /verify\(custom\)/);
|
|
2231
|
+
assert.equal(snapshot.details.phases.verify.substep, "custom");
|
|
2232
|
+
await h.emitEvent("tool_call", subagentCall("d", { agent: "code-reviewer" }));
|
|
2233
|
+
assert.match(h.widgets.at(-1)!, /verify\(code-review #3\)/);
|
|
2234
|
+
});
|
|
2235
|
+
|
|
2236
|
+
for (const action of ["start", "complete", "skip", "reset"]) {
|
|
2237
|
+
test(`live transition ${action} drops terminal ownership and restart resets counts`, async () => {
|
|
2238
|
+
const h = harness({ hasUI: true });
|
|
2239
|
+
const tool = h.tools.find(t => t.name === "phase_tracker")!;
|
|
2240
|
+
await startPhase(h, "verify");
|
|
2241
|
+
await h.emitEvent("tool_call", subagentCall("old", { agent: "code-reviewer" }));
|
|
2242
|
+
const rejected = await tool.execute("bad", { action: "start", phase: "plan" }, undefined, undefined, h.ctx) as any;
|
|
2243
|
+
assert.ok(rejected.details.error);
|
|
2244
|
+
assert.match(h.widgets.at(-1)!, /verify\(code-review\)/);
|
|
2245
|
+
await tool.execute("transition", { action, phase: "verify", reason: "test" }, undefined, undefined, h.ctx);
|
|
2246
|
+
if (action === "reset") assert.equal(h.widgets.at(-1), undefined);
|
|
2247
|
+
if (action !== "start") await tool.execute("restart", { action: "start", phase: "verify", force: true }, undefined, undefined, h.ctx);
|
|
2248
|
+
await tool.execute("label", { action: "substep", phase: "verify", substep: "custom" }, undefined, undefined, h.ctx);
|
|
2249
|
+
const paints = h.widgets.length;
|
|
2250
|
+
await h.emitEvent("tool_execution_end", { toolName: "subagent", toolCallId: "old", result: { details: {} } });
|
|
2251
|
+
assert.equal(h.widgets.length, paints);
|
|
2252
|
+
assert.match(h.widgets.at(-1)!, /verify\(custom\)/);
|
|
2253
|
+
await h.emitEvent("tool_call", subagentCall("new", { agent: "code-reviewer" }));
|
|
2254
|
+
assert.match(h.widgets.at(-1)!, /verify\(code-review\)/);
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
test("live plan backstop clears ownership only on actual implement completion", async () => {
|
|
2259
|
+
const h = harness({ hasUI: true });
|
|
2260
|
+
await startPhase(h, "implement");
|
|
2261
|
+
await h.emitEvent("tool_call", subagentCall("old", { agent: "code-reviewer" }));
|
|
2262
|
+
for (const tasks of [undefined, [{ status: "in_progress" }]]) {
|
|
2263
|
+
await h.emitEvent("tool_execution_end", { toolName: "plan_tracker", result: { details: { tasks } } });
|
|
2264
|
+
assert.match(h.widgets.at(-1)!, /implement\(code-review\)/);
|
|
2265
|
+
}
|
|
2266
|
+
await h.emitEvent("tool_execution_end", { toolName: "plan_tracker", result: { details: { tasks: [{ status: "complete" }] } } });
|
|
2267
|
+
await startPhase(h, "verify");
|
|
2268
|
+
const tool = h.tools.find(t => t.name === "phase_tracker")!;
|
|
2269
|
+
await tool.execute("label", { action: "substep", phase: "verify", substep: "custom" }, undefined, undefined, h.ctx);
|
|
2270
|
+
await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "old", details: {}, content: [] });
|
|
2271
|
+
assert.match(h.widgets.at(-1)!, /verify\(custom\)/);
|
|
2272
|
+
await h.emitEvent("tool_call", subagentCall("new", { agent: "code-reviewer" }));
|
|
2273
|
+
assert.match(h.widgets.at(-1)!, /verify\(code-review\)/);
|
|
2274
|
+
});
|
|
2275
|
+
|
|
2276
|
+
test("live observation is dormant without a phase and in a subagent child", async () => {
|
|
2277
|
+
const dormant = harness({ hasUI: true });
|
|
2278
|
+
await dormant.emitEvent("tool_call", { toolName: "plan_check", toolCallId: "a", input: {} });
|
|
2279
|
+
assert.equal(dormant.widgets.length, 0);
|
|
2280
|
+
const prior = process.env.PI_SUBAGENT_DEPTH;
|
|
2281
|
+
let child: ReturnType<typeof harness>;
|
|
2282
|
+
try {
|
|
2283
|
+
process.env.PI_SUBAGENT_DEPTH = "1";
|
|
2284
|
+
child = harness({ hasUI: true });
|
|
2285
|
+
} finally {
|
|
2286
|
+
if (prior === undefined) delete process.env.PI_SUBAGENT_DEPTH;
|
|
2287
|
+
else process.env.PI_SUBAGENT_DEPTH = prior;
|
|
2288
|
+
}
|
|
2289
|
+
await startPhase(child, "verify");
|
|
2290
|
+
const paints = child.widgets.length;
|
|
2291
|
+
await child.emitEvent("tool_call", subagentCall("a", { agent: "conformance-reviewer" }));
|
|
2292
|
+
await child.emitEvent("tool_call", { toolName: "plan_check", toolCallId: "b", input: {} });
|
|
2293
|
+
assert.equal(child.widgets.length, paints);
|
|
2294
|
+
});
|
|
2295
|
+
|
|
2296
|
+
for (const cap of [false, true]) {
|
|
2297
|
+
test(`live blocked ${cap ? "cap" : "lone"} call creates no label or ordinal`, async () => {
|
|
2298
|
+
const cwd = tempCwd({ piGauntlet: { closureReview: { enforce: true, maxFixRounds: cap ? 0 : 3 } } });
|
|
2299
|
+
const h = harness({ hasUI: true, cwd, branch: verifyBranch([subagentResult(["conformance-reviewer"])]) });
|
|
2300
|
+
await h.emit("session_start");
|
|
2301
|
+
const paints = h.widgets.length;
|
|
2302
|
+
assert.equal((await firstCallResult(h, "blocked", cap ? implementerWave() : loneImplementer()))?.block, true);
|
|
2303
|
+
await h.emitEvent("tool_execution_end", { toolName: "subagent", toolCallId: "blocked", isError: true, result: { details: {} } });
|
|
2304
|
+
assert.equal(h.widgets.length, paints);
|
|
2305
|
+
writeFileSync(join(cwd, ".pi", "settings.json"), JSON.stringify({ piGauntlet: { closureReview: { enforce: false } } }));
|
|
2306
|
+
await firstCallResult(h, "allowed", implementerWave());
|
|
2307
|
+
assert.match(h.widgets.at(-1)!, /verify\(implement\)/);
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
const replaySnapshot = (phase: Phase, substep?: string, status: Status = "in_progress") => {
|
|
2312
|
+
const state = phases({ [phase]: status });
|
|
2313
|
+
if (substep !== undefined) state[phase].substep = substep;
|
|
2314
|
+
return state;
|
|
2315
|
+
};
|
|
2316
|
+
|
|
2317
|
+
const explicitReplay = (phase: Phase, label: string) => [
|
|
2318
|
+
phaseResult("start", replaySnapshot(phase)),
|
|
2319
|
+
phaseResult("substep", replaySnapshot(phase, label)),
|
|
2320
|
+
];
|
|
2321
|
+
|
|
2322
|
+
for (const row of [
|
|
2323
|
+
{ name: "derived status only", branch: [phaseResult("start", replaySnapshot("verify")), phaseResult("status", replaySnapshot("verify", "conformance"))], phase: "verify", expected: undefined },
|
|
2324
|
+
{ name: "explicit before derived status", branch: [...explicitReplay("verify", "gather"), phaseResult("status", replaySnapshot("verify", "council"))], phase: "verify", expected: "gather" },
|
|
2325
|
+
...([{ phase: "brainstorm", label: "gather", agent: "scout" }, { phase: "plan", label: "plan-write", agent: "plan-writer" }] as const).map(r => ({
|
|
2326
|
+
name: `unfinished ${r.agent}`, phase: r.phase, expected: r.label,
|
|
2327
|
+
branch: [...explicitReplay(r.phase, r.label), assistant(), { type: "message", message: { role: "assistant", content: [{ type: "toolCall", id: "unfinished", name: "subagent", arguments: { agent: r.agent } }] } }],
|
|
2328
|
+
})),
|
|
2329
|
+
{ name: "null or omitted clear", phase: "verify", expected: undefined, branch: [...explicitReplay("verify", "gather"), phaseResult("substep", replaySnapshot("verify")), phaseResult("status", replaySnapshot("verify", "council"))] },
|
|
2330
|
+
...["start", "complete", "skip"].map(action => ({ name: `same phase ${action}`, phase: "verify", expected: undefined, branch: [...explicitReplay("verify", "gather"), phaseResult(action, replaySnapshot("verify", undefined, action === "complete" ? "complete" : action === "skip" ? "skipped" : "in_progress"))] })),
|
|
2331
|
+
...["complete", "skip"].map(action => {
|
|
2332
|
+
const state = replaySnapshot("verify", "gather");
|
|
2333
|
+
state.ship.status = action === "complete" ? "complete" : "skipped";
|
|
2334
|
+
return { name: `other phase ${action}`, phase: "verify", expected: "gather", branch: [...explicitReplay("verify", "gather"), phaseResult(action, state)] };
|
|
2335
|
+
}),
|
|
2336
|
+
{ name: "reset then start", phase: "verify", expected: undefined, branch: [...explicitReplay("verify", "gather"), phaseResult("reset", phases()), phaseResult("start", replaySnapshot("verify"))] },
|
|
2337
|
+
...["substep", "start", "complete", "skip", "reset"].map(action => {
|
|
2338
|
+
const rejected = phaseResult(action, phases());
|
|
2339
|
+
Object.assign(rejected.message.details, { error: "rejected" });
|
|
2340
|
+
return { name: `rejected ${action}`, phase: "verify", expected: "gather", branch: [...explicitReplay("verify", "gather"), rejected] };
|
|
2341
|
+
}),
|
|
2342
|
+
{ name: "task-driven completion", phase: "implement", expected: undefined, expectedStatus: "complete", branch: [...explicitReplay("implement", "custom"), taskSnapshot([{ name: "task", status: "complete" }])] },
|
|
2343
|
+
{ name: "custom after clear", phase: "verify", expected: "custom", branch: [...explicitReplay("verify", "gather"), phaseResult("substep", replaySnapshot("verify")), phaseResult("substep", replaySnapshot("verify", "custom"))] },
|
|
2344
|
+
]) {
|
|
2345
|
+
test(`replay explicit labels: ${row.name}`, async () => {
|
|
2346
|
+
const original = structuredClone(row.branch);
|
|
2347
|
+
const h = harness({ hasUI: true, branch: row.branch });
|
|
2348
|
+
const tool = h.tools.find(t => t.name === "phase_tracker")!;
|
|
2349
|
+
await h.emit("session_start");
|
|
2350
|
+
const status = await tool.execute("status", { action: "status" }, undefined, undefined, h.ctx) as any;
|
|
2351
|
+
assert.equal(status.details.phases[row.phase].substep, row.expected);
|
|
2352
|
+
if ("expectedStatus" in row) assert.equal(status.details.phases[row.phase].status, row.expectedStatus);
|
|
2353
|
+
if (row.expected) {
|
|
2354
|
+
assert.ok(h.widgets.at(-1)!.includes(`${row.phase}(${row.expected})`));
|
|
2355
|
+
assert.ok(status.content[0].text.includes(`${row.phase}(${row.expected})`));
|
|
2356
|
+
} else {
|
|
2357
|
+
assert.ok(!h.widgets.at(-1)!.includes(`${row.phase}(`));
|
|
2358
|
+
}
|
|
2359
|
+
assert.deepEqual(row.branch, original);
|
|
2360
|
+
if (status.details.phases[row.phase].status !== "in_progress") await startPhase(h, row.phase);
|
|
2361
|
+
const restarted = await tool.execute("status", { action: "status" }, undefined, undefined, h.ctx) as any;
|
|
2362
|
+
assert.equal(restarted.details.phases[row.phase].substep, row.expected);
|
|
2363
|
+
await h.emitEvent("tool_call", subagentCall("new", { agent: "code-reviewer" }));
|
|
2364
|
+
await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "new", details: {}, content: [] });
|
|
2365
|
+
assert.deepEqual(row.branch, original);
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
for (const event of ["session_start", "session_switch", "session_fork", "session_tree"]) {
|
|
2370
|
+
for (const kind of ["explicit", "derived", "empty"]) {
|
|
2371
|
+
test(`replay ${event} resets live ownership and ordinals: ${kind}`, async () => {
|
|
2372
|
+
const h = harness({ hasUI: true });
|
|
2373
|
+
const tool = h.tools.find(t => t.name === "phase_tracker")!;
|
|
2374
|
+
await startPhase(h, "verify");
|
|
2375
|
+
await h.emitEvent("tool_call", subagentCall("first", { agent: "code-reviewer" }));
|
|
2376
|
+
await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "first", details: {}, content: [] });
|
|
2377
|
+
await h.emitEvent("tool_call", subagentCall("old", { agent: "code-reviewer" }));
|
|
2378
|
+
assert.match(h.widgets.at(-1)!, /verify\(code-review #2\)/);
|
|
2379
|
+
const branch = kind === "empty" ? [assistant()] : kind === "explicit" ? explicitReplay("verify", "custom") : [phaseResult("start", replaySnapshot("verify")), phaseResult("status", replaySnapshot("verify", "code-review"))];
|
|
2380
|
+
const original = structuredClone(branch);
|
|
2381
|
+
h.setBranch(branch);
|
|
2382
|
+
await h.emit(event);
|
|
2383
|
+
if (kind === "empty") assert.equal(h.widgets.at(-1), undefined);
|
|
2384
|
+
else if (kind === "explicit") assert.match(h.widgets.at(-1)!, /verify\(custom\)/);
|
|
2385
|
+
else assert.doesNotMatch(h.widgets.at(-1)!, /verify\(/);
|
|
2386
|
+
const status = await tool.execute("status", { action: "status" }, undefined, undefined, h.ctx) as any;
|
|
2387
|
+
assert.equal(status.details.phases.verify.substep, kind === "explicit" ? "custom" : undefined);
|
|
2388
|
+
assert.deepEqual(branch, original);
|
|
2389
|
+
const paints = h.widgets.length;
|
|
2390
|
+
await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "old", details: {}, content: [] });
|
|
2391
|
+
await h.emitEvent("tool_execution_end", { toolName: "subagent", toolCallId: "old", result: { details: {} } });
|
|
2392
|
+
assert.equal(h.widgets.length, paints);
|
|
2393
|
+
if (kind === "empty") await startPhase(h, "verify");
|
|
2394
|
+
await h.emitEvent("tool_call", subagentCall("next", { agent: "code-reviewer" }));
|
|
2395
|
+
assert.match(h.widgets.at(-1)!, /verify\(code-review\)/);
|
|
2396
|
+
await h.emitEvent("tool_result", { toolName: "subagent", toolCallId: "next", details: {}, content: [] });
|
|
2397
|
+
await h.emitEvent("tool_call", subagentCall("again", { agent: "code-reviewer" }));
|
|
2398
|
+
assert.match(h.widgets.at(-1)!, /verify\(code-review #2\)/);
|
|
2399
|
+
assert.deepEqual(branch, original);
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
@@ -31,6 +31,7 @@ import { checkoutOf, gitSync } from "./lib/checkout.ts";
|
|
|
31
31
|
import { checkPlan, sha256, type FsPort, type PlanCheckFinding } from "./lib/plan-check.ts";
|
|
32
32
|
import {
|
|
33
33
|
checkSubstep,
|
|
34
|
+
dispatchLabel,
|
|
34
35
|
findMarkerFile,
|
|
35
36
|
flowGuardApplies,
|
|
36
37
|
implementExemptDirs,
|
|
@@ -354,10 +355,11 @@ function hasActivity(phases: PhaseMap): boolean {
|
|
|
354
355
|
return PHASES.some((p) => phases[p].status !== "pending");
|
|
355
356
|
}
|
|
356
357
|
|
|
357
|
-
function formatWidget(phases: PhaseMap, theme: Theme): string {
|
|
358
|
+
function formatWidget(phases: PhaseMap, dispatchCounts: Record<Phase, Record<string, number>>, theme: Theme): string {
|
|
358
359
|
const parts = PHASES.map((p) => {
|
|
359
360
|
const icon = phaseIcon(phases[p].status, theme);
|
|
360
|
-
const
|
|
361
|
+
const substep = phases[p].status === "in_progress" ? phases[p].substep : undefined;
|
|
362
|
+
const labeled = phaseLabel(p, substep, dispatchCounts[p][substep ?? ""] ?? 0);
|
|
361
363
|
const name = phases[p].status === "skipped" ? theme.fg("dim", labeled) : labeled;
|
|
362
364
|
return `${icon} ${name}`;
|
|
363
365
|
});
|
|
@@ -422,6 +424,31 @@ async function applyPhaseModel(
|
|
|
422
424
|
export default function (pi: ExtensionAPI) {
|
|
423
425
|
let phases: PhaseMap = emptyPhases();
|
|
424
426
|
let conformanceDispatched = false;
|
|
427
|
+
let dispatchCounts = Object.fromEntries(PHASES.map((p) => [p, {}])) as Record<Phase, Record<string, number>>;
|
|
428
|
+
let liveDispatch: { toolCallId: string; phase: Phase; label: string } | undefined;
|
|
429
|
+
const observeDispatch = (toolName: string, input: unknown, toolCallId: string): boolean => {
|
|
430
|
+
if (isSubagentChild) return false;
|
|
431
|
+
const label = dispatchLabel(toolName, input);
|
|
432
|
+
const phase = PHASES.find((p) => phases[p].status === "in_progress");
|
|
433
|
+
if (label === undefined || phase === undefined) return false;
|
|
434
|
+
dispatchCounts[phase][label] = (dispatchCounts[phase][label] ?? 0) + 1;
|
|
435
|
+
phases = { ...phases, [phase]: { ...phases[phase], substep: label } };
|
|
436
|
+
liveDispatch = { toolCallId, phase, label };
|
|
437
|
+
return true;
|
|
438
|
+
};
|
|
439
|
+
const isNonDispatchResult = (isError: boolean | undefined, details: unknown): boolean =>
|
|
440
|
+
isError === true && details !== null && typeof details === "object" && Object.keys(details).length === 0;
|
|
441
|
+
const finishDispatch = (toolName: string, toolCallId: string, isError: boolean | undefined, details: unknown): boolean => {
|
|
442
|
+
if ((toolName !== "subagent" && toolName !== "plan_check") || liveDispatch?.toolCallId !== toolCallId) return false;
|
|
443
|
+
const { phase, label } = liveDispatch;
|
|
444
|
+
if (isNonDispatchResult(isError, details)) dispatchCounts[phase][label] -= 1;
|
|
445
|
+
if (phases[phase].substep === label) {
|
|
446
|
+
const { substep: _substep, ...entry } = phases[phase];
|
|
447
|
+
phases = { ...phases, [phase]: entry };
|
|
448
|
+
}
|
|
449
|
+
liveDispatch = undefined;
|
|
450
|
+
return true;
|
|
451
|
+
};
|
|
425
452
|
let fixRounds = 0;
|
|
426
453
|
let fixRoundCredits = 0;
|
|
427
454
|
let gauntletEntered = false;
|
|
@@ -493,6 +520,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
493
520
|
tasks.every((t) => t.status === "complete" || t.status === "skipped")
|
|
494
521
|
) {
|
|
495
522
|
phases = { ...phases, implement: { status: "complete" } };
|
|
523
|
+
liveDispatch = undefined;
|
|
496
524
|
firedGuards.clear();
|
|
497
525
|
}
|
|
498
526
|
};
|
|
@@ -507,6 +535,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
507
535
|
|
|
508
536
|
const reconstructState = (ctx: ExtensionContext) => {
|
|
509
537
|
phases = emptyPhases();
|
|
538
|
+
dispatchCounts = Object.fromEntries(PHASES.map((p) => [p, {}])) as Record<Phase, Record<string, number>>;
|
|
539
|
+
liveDispatch = undefined;
|
|
540
|
+
let explicitSubstep: Partial<Record<Phase, string>> = {};
|
|
510
541
|
conformanceDispatched = false;
|
|
511
542
|
fixRounds = 0;
|
|
512
543
|
fixRoundCredits = 0;
|
|
@@ -533,6 +564,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
533
564
|
if (msg.toolName === "phase_tracker") {
|
|
534
565
|
const details = msg.details as PhaseTrackerDetails | undefined;
|
|
535
566
|
if (details && !details.error) {
|
|
567
|
+
if (details.action === "substep") {
|
|
568
|
+
const phase = PHASES.find((p) => details.phases[p].status === "in_progress");
|
|
569
|
+
if (phase !== undefined) {
|
|
570
|
+
const label = details.phases[phase].substep;
|
|
571
|
+
if (label !== undefined) explicitSubstep[phase] = label;
|
|
572
|
+
else delete explicitSubstep[phase];
|
|
573
|
+
}
|
|
574
|
+
} else if (details.action === "start" || details.action === "complete" || details.action === "skip") {
|
|
575
|
+
const changed = PHASES.filter((p) => phases[p].status !== details.phases[p].status);
|
|
576
|
+
if (changed.length === 0 && details.action === "start") {
|
|
577
|
+
const phase = PHASES.find((p) => details.phases[p].status === "in_progress");
|
|
578
|
+
if (phase !== undefined) delete explicitSubstep[phase];
|
|
579
|
+
}
|
|
580
|
+
for (const phase of changed) delete explicitSubstep[phase];
|
|
581
|
+
} else if (details.action === "reset") {
|
|
582
|
+
explicitSubstep = {};
|
|
583
|
+
}
|
|
536
584
|
phases = details.phases;
|
|
537
585
|
if (details.action === "grant_fix_rounds" && typeof details.rounds === "number") fixRoundCredits = details.rounds;
|
|
538
586
|
gauntletEntered = nextGauntletEntered(gauntletEntered, details.action, details.phases.brainstorm.status);
|
|
@@ -561,9 +609,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
561
609
|
observeCadence(msg.details);
|
|
562
610
|
} else if (msg.toolName === "plan_tracker") {
|
|
563
611
|
const details = msg.details as { tasks?: { status: string }[]; error?: string } | undefined;
|
|
564
|
-
if (details && !details.error)
|
|
612
|
+
if (details && !details.error) {
|
|
613
|
+
const wasImplementing = phases.implement.status === "in_progress";
|
|
614
|
+
applyPlanActivity(details.tasks);
|
|
615
|
+
if (wasImplementing && phases.implement.status === "complete") delete explicitSubstep.implement;
|
|
616
|
+
}
|
|
565
617
|
}
|
|
566
618
|
}
|
|
619
|
+
phases = Object.fromEntries(PHASES.map((phase) => {
|
|
620
|
+
const { substep: _substep, ...entry } = phases[phase];
|
|
621
|
+
const label = explicitSubstep[phase];
|
|
622
|
+
return [phase, label === undefined ? entry : { ...entry, substep: label }];
|
|
623
|
+
})) as PhaseMap;
|
|
567
624
|
};
|
|
568
625
|
|
|
569
626
|
const updateWidget = (ctx: ExtensionContext) => {
|
|
@@ -572,7 +629,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
572
629
|
ctx.ui.setWidget("phase_tracker", undefined);
|
|
573
630
|
} else {
|
|
574
631
|
ctx.ui.setWidget("phase_tracker", (_tui, theme) => {
|
|
575
|
-
return new Text(formatWidget(phases, theme), 0, 0);
|
|
632
|
+
return new Text(formatWidget(phases, dispatchCounts, theme), 0, 0);
|
|
576
633
|
});
|
|
577
634
|
}
|
|
578
635
|
};
|
|
@@ -657,6 +714,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
657
714
|
}
|
|
658
715
|
}
|
|
659
716
|
|
|
717
|
+
if (observeDispatch(event.toolName, event.input, event.toolCallId)) updateWidget(ctx);
|
|
718
|
+
|
|
660
719
|
// Flow guards apply only to write/edit/bash while a guard phase is active.
|
|
661
720
|
// Cheap in-memory gate BEFORE any settings load: phase state is in-memory, so an
|
|
662
721
|
// event no guard inspects (any read-only tool, or any tool in a dormant session)
|
|
@@ -782,6 +841,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
782
841
|
});
|
|
783
842
|
|
|
784
843
|
pi.on("tool_result", async (event, ctx) => {
|
|
844
|
+
if (finishDispatch(event.toolName, event.toolCallId, event.isError, event.details)) updateWidget(ctx);
|
|
785
845
|
if (event.toolName === "subagent") {
|
|
786
846
|
observeFixWave(event.details, event.isError, ctx);
|
|
787
847
|
if (qualifiesAsClosureDispatch(event.details)) conformanceDispatched = true;
|
|
@@ -818,6 +878,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
818
878
|
});
|
|
819
879
|
|
|
820
880
|
pi.on("tool_execution_end", async (event, ctx) => {
|
|
881
|
+
const resultDetails = (event.result as { details?: unknown } | undefined)?.details;
|
|
882
|
+
if (finishDispatch(event.toolName, event.toolCallId, event.isError, resultDetails)) updateWidget(ctx);
|
|
821
883
|
if (event.toolName !== "plan_tracker" || event.isError) return;
|
|
822
884
|
const details = (event.result as { details?: { tasks?: { status: string }[]; error?: string } } | undefined)
|
|
823
885
|
?.details;
|
|
@@ -1032,6 +1094,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1032
1094
|
const target = resolved.phaseModels[phase];
|
|
1033
1095
|
if (target) startLines.push(...(await applyPhaseModel(pi, ctx, phase, target)));
|
|
1034
1096
|
}
|
|
1097
|
+
if (liveDispatch?.phase === params.phase) liveDispatch = undefined;
|
|
1098
|
+
dispatchCounts[params.phase] = {};
|
|
1035
1099
|
phases = { ...phases, [params.phase]: transitionPhaseState("in_progress") as PhaseState };
|
|
1036
1100
|
// A rewind must not inherit the prior verify's conformance latch.
|
|
1037
1101
|
if (params.phase === "implement") {
|
|
@@ -1102,6 +1166,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1102
1166
|
};
|
|
1103
1167
|
}
|
|
1104
1168
|
}
|
|
1169
|
+
if (liveDispatch?.phase === params.phase) liveDispatch = undefined;
|
|
1105
1170
|
phases = { ...phases, [params.phase]: transitionPhaseState("complete") as PhaseState };
|
|
1106
1171
|
firedGuards.clear();
|
|
1107
1172
|
updateWidget(ctx);
|
|
@@ -1134,6 +1199,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1134
1199
|
};
|
|
1135
1200
|
}
|
|
1136
1201
|
const reason = params.reason;
|
|
1202
|
+
if (liveDispatch?.phase === params.phase) liveDispatch = undefined;
|
|
1137
1203
|
phases = { ...phases, [params.phase]: transitionPhaseState("skipped", reason) as PhaseState };
|
|
1138
1204
|
firedGuards.clear();
|
|
1139
1205
|
updateWidget(ctx);
|
|
@@ -1180,6 +1246,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1180
1246
|
}
|
|
1181
1247
|
|
|
1182
1248
|
case "reset": {
|
|
1249
|
+
liveDispatch = undefined;
|
|
1250
|
+
dispatchCounts = Object.fromEntries(PHASES.map((p) => [p, {}])) as Record<Phase, Record<string, number>>;
|
|
1183
1251
|
phases = Object.fromEntries(
|
|
1184
1252
|
PHASES.map((p) => [p, transitionPhaseState("pending")]),
|
|
1185
1253
|
) as PhaseMap;
|
package/package.json
CHANGED