@phi-code-admin/phi-code 0.80.0 → 0.82.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,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.82.0] - 2026-06-14
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Opt-in parallel EXPLORE fan-out** (`/plan --fanout <description>`). Before
|
|
8
|
+
phase 1, the orchestrator runs up to **2 concurrent read-only** sub-explorers,
|
|
9
|
+
each with a narrow mandate (architecture & reusable patterns, impacted files,
|
|
10
|
+
risks & constraints), and merges their findings into the EXPLORE context. This
|
|
11
|
+
applies the perspective-diverse pattern to the front of the pipeline so the
|
|
12
|
+
downstream phases start from a fuller, more accurate map.
|
|
13
|
+
|
|
14
|
+
Guardrails (the fan-out is best-effort and never breaks a run): default OFF
|
|
15
|
+
(single-agent EXPLORE unchanged); read-only tool set only (no write/edit/bash);
|
|
16
|
+
hard concurrency cap of 2 against the single rate-limited key; **adaptive
|
|
17
|
+
fallback to sequential** the moment a sub-explorer reports a 429 / rate limit;
|
|
18
|
+
a per-explorer timeout that kills the process; and full graceful degradation to
|
|
19
|
+
the normal EXPLORE on any error, empty result, or rate limit. Experimental: not
|
|
20
|
+
yet validated against a live proxy at scale, hence opt-in.
|
|
21
|
+
|
|
22
|
+
## [0.81.0] - 2026-06-14
|
|
23
|
+
|
|
24
|
+
Marathon increment 4 (prompt-only, low risk).
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **Memory discipline.** The per-turn reminder now tells the model to (1) save only
|
|
29
|
+
non-obvious, cross-session facts (not what git already records) and (2)
|
|
30
|
+
verify-before-trust: a recalled note describes a PAST state, so confirm it against
|
|
31
|
+
the live code before acting, and the current observation wins on conflict.
|
|
32
|
+
- **REVIEW final sweep.** On large diffs, the reviewer re-reads the diff once more
|
|
33
|
+
as if fresh and adds only genuinely new bugs (no padding).
|
|
34
|
+
- **Run Recipe.** EXPLORE records a `## Run Recipe` (build/run command, port, ready
|
|
35
|
+
signal) in the brief; TEST uses it to actually run the code and to tell a real
|
|
36
|
+
failure (FAIL) from a stale launch recipe (BLOCKED).
|
|
37
|
+
|
|
3
38
|
## [0.80.0] - 2026-06-14
|
|
4
39
|
|
|
5
40
|
Marathon increment 3: context protection, productivity commands, and the
|
package/extensions/phi/memory.ts
CHANGED
|
@@ -665,9 +665,15 @@ REMINDER (project rule, applies every turn):
|
|
|
665
665
|
project context, prior decisions, and saved learnings are accessible
|
|
666
666
|
ONLY via this tool.
|
|
667
667
|
2. AFTER completing significant work, call \`memory_write\` to save what
|
|
668
|
-
you did and learned.
|
|
669
|
-
|
|
670
|
-
|
|
668
|
+
you did and learned. Save only NON-OBVIOUS, cross-session facts and user
|
|
669
|
+
preferences; do NOT save what the repo or git history already records.
|
|
670
|
+
3. Saved notes describe a PAST state. Before you act on a recalled fact,
|
|
671
|
+
verify it against the CURRENT code (read/grep that the path or symbol still
|
|
672
|
+
exists). If it conflicts, the live observation wins. Do not delete a stale
|
|
673
|
+
note; note the divergence.
|
|
674
|
+
|
|
675
|
+
The memory_search and memory_write calls are not optional. Skipping them
|
|
676
|
+
violates project rules.
|
|
671
677
|
</system-reminder>
|
|
672
678
|
|
|
673
679
|
`;
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
isTransientError,
|
|
29
29
|
parsePhaseVerdict,
|
|
30
30
|
} from "./providers/orchestrator-helpers.js";
|
|
31
|
+
import { defaultExplorerSpecs, READONLY_EXPLORER_TOOLS, runExploreFanout } from "./providers/explore-fanout.js";
|
|
31
32
|
|
|
32
33
|
// ─── Types ───────────────────────────────────────────────────────────────
|
|
33
34
|
|
|
@@ -414,6 +415,7 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
414
415
|
- Requirements: specific features needed
|
|
415
416
|
- Tech decisions: frameworks, patterns to use
|
|
416
417
|
- Constraints: what to NOT break
|
|
418
|
+
- Run Recipe: from package.json scripts / Makefile / Dockerfile / README, a \`## Run Recipe\` with the build command, run command, port/URL, and ready signal (the TEST phase uses this to actually run the code)
|
|
417
419
|
5. **MANDATORY:** Write your findings to \`.phi/plans/explore-${ts}.md\` using the \`write\` tool. This file is READ by Phase 2 PLAN — if you skip it, the plan agent has no context. Do NOT skip this step.
|
|
418
420
|
|
|
419
421
|
**LAST ACTION (MANDATORY):** Call \`memory_write\` to save your exploration findings for downstream agents.
|
|
@@ -580,6 +582,8 @@ $ <command actually executed>
|
|
|
580
582
|
- Error: ... -> Fix: ...
|
|
581
583
|
\`\`\`
|
|
582
584
|
|
|
585
|
+
**Launch:** if the brief contains a "## Run Recipe" (build command, run command, port/URL, ready signal), use it. Distinguish a real failure (FAIL) from a stale launch recipe (BLOCKED) and report the recipe that actually works.
|
|
586
|
+
|
|
583
587
|
**Verdict rules:** PASS only if you OBSERVED every feature working at runtime. When in doubt, FAIL. No partial pass (3/4 working = FAIL). Use BLOCKED only when you could not run anything (broken launch recipe, missing env, provider down) and say what is needed.
|
|
584
588
|
|
|
585
589
|
**CRITICAL RULES:**
|
|
@@ -640,6 +644,8 @@ After testing, use \`memory_write\` to save test results, bugs found, and lesson
|
|
|
640
644
|
- path/file.ts:123 - <what must change and why>
|
|
641
645
|
\`\`\`
|
|
642
646
|
|
|
647
|
+
**Final sweep (large diffs only):** before writing the verdict, re-read the diff once more as if you had never seen it; add ONLY bugs not already in your list. If there are none, do not pad.
|
|
648
|
+
|
|
643
649
|
**Verdict rules:** VERDICT: FAIL if there is ANY CONFIRMED correctness/security finding (it goes under BLOCKING). VERDICT: PASS only if BLOCKING is empty. Do NOT pad: if the code is clean, return PASS with no findings.
|
|
644
650
|
|
|
645
651
|
After your review, use \`memory_write\` ONCE to save:
|
|
@@ -1132,10 +1138,13 @@ Tag the note with relevant keywords for vector search.
|
|
|
1132
1138
|
return;
|
|
1133
1139
|
}
|
|
1134
1140
|
|
|
1135
|
-
|
|
1141
|
+
// Opt-in: `--fanout` runs a small read-only parallel exploration before
|
|
1142
|
+
// phase 1 (default OFF — single-agent EXPLORE, the safe behaviour).
|
|
1143
|
+
const wantFanout = /(^|\s)--fanout\b/.test(rawArgs);
|
|
1144
|
+
const description = rawArgs.replace(/(^|\s)--fanout\b/g, " ").trim();
|
|
1136
1145
|
|
|
1137
1146
|
if (!description) {
|
|
1138
|
-
ctx.ui.notify(`**Usage:** \`/plan <project description>\` (or \`/plan --resume\` to continue a checkpointed run)
|
|
1147
|
+
ctx.ui.notify(`**Usage:** \`/plan <project description>\` (or \`/plan --resume\` to continue a checkpointed run, \`/plan --fanout <desc>\` for parallel exploration)
|
|
1139
1148
|
|
|
1140
1149
|
**Examples:**
|
|
1141
1150
|
/plan Build a REST API for user authentication with JWT
|
|
@@ -1183,6 +1192,35 @@ Tag the note with relevant keywords for vector search.
|
|
|
1183
1192
|
|
|
1184
1193
|
// Record orchestration start time for final summary
|
|
1185
1194
|
phaseStartTime = Date.now();
|
|
1195
|
+
|
|
1196
|
+
// Opt-in parallel EXPLORE fan-out (read-only, <=2 concurrent, sequential
|
|
1197
|
+
// fallback on rate limit). Best-effort: any failure degrades to the normal
|
|
1198
|
+
// single-agent EXPLORE below by simply not enriching the instruction.
|
|
1199
|
+
if (wantFanout) {
|
|
1200
|
+
try {
|
|
1201
|
+
const available = ctx.modelRegistry?.getAvailable?.() || [];
|
|
1202
|
+
const exploreModelId = resolveModelRef(available, firstPhase.model)?.id || firstPhase.model;
|
|
1203
|
+
ctx.ui.notify(`\n🔭 **Parallel exploration** (read-only, up to 2 concurrent) — building a fuller map before phase 1...`, "info");
|
|
1204
|
+
const fan = await runExploreFanout(defaultExplorerSpecs(description), {
|
|
1205
|
+
model: exploreModelId,
|
|
1206
|
+
tools: READONLY_EXPLORER_TOOLS,
|
|
1207
|
+
cwd: ctx.cwd || process.cwd(),
|
|
1208
|
+
concurrency: 2,
|
|
1209
|
+
timeoutMs: 4 * 60 * 1000,
|
|
1210
|
+
});
|
|
1211
|
+
const okCount = fan.results.filter((r) => r.ok).length;
|
|
1212
|
+
if (fan.merged.trim()) {
|
|
1213
|
+
firstPhase.instruction =
|
|
1214
|
+
`## Parallel exploration findings (${okCount} read-only sub-explorer(s))\nUse these as a starting map; verify and synthesize them into your brief, do not trust them blindly.\n\n${fan.merged}\n\n---\n${firstPhase.instruction}`;
|
|
1215
|
+
ctx.ui.notify(`\n🔭 ${okCount} sub-exploration(s) merged into EXPLORE${fan.rateLimited ? " (rate limit hit — ran the rest sequentially)" : ""}.`, "info");
|
|
1216
|
+
} else {
|
|
1217
|
+
ctx.ui.notify(`\n🔭 Parallel exploration returned nothing usable${fan.rateLimited ? " (rate-limited)" : ""} — running the normal single-agent EXPLORE.`, "warning");
|
|
1218
|
+
}
|
|
1219
|
+
} catch {
|
|
1220
|
+
ctx.ui.notify(`\n🔭 Parallel exploration skipped (error) — running the normal single-agent EXPLORE.`, "warning");
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1186
1224
|
// Switch model and activate agent for first phase
|
|
1187
1225
|
const { modelId, warning } = await switchModelForPhase(firstPhase, ctx);
|
|
1188
1226
|
activateAgent(firstPhase, ctx);
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in parallel EXPLORE fan-out for the /plan orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Spawns a small number of read-only `pi` sub-explorers, each with a narrow focus
|
|
5
|
+
* (architecture / impacted files / risks), and merges their findings into the
|
|
6
|
+
* EXPLORE phase context. This is the perspective-diverse pattern applied to the
|
|
7
|
+
* front of the pipeline: a more complete map of the codebase means better
|
|
8
|
+
* downstream plan/code/test phases.
|
|
9
|
+
*
|
|
10
|
+
* GUARDRAILS (this whole module is best-effort and NEVER throws):
|
|
11
|
+
* - Read-only only: sub-explorers get a read-only tool set, no write/edit/bash.
|
|
12
|
+
* - Hard concurrency cap of 2 (a single rate-limited proxy key is the constraint).
|
|
13
|
+
* - Adaptive fallback: if any sub-explorer reports a 429 / rate limit, the
|
|
14
|
+
* remaining ones run sequentially (concurrency drops to 1).
|
|
15
|
+
* - Per-explorer timeout, with the process killed on expiry.
|
|
16
|
+
* - Any failure (spawn error, timeout, no output, rate limit) degrades to "no
|
|
17
|
+
* findings" so the orchestrator simply runs the normal single-agent EXPLORE.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { spawn } from "node:child_process";
|
|
21
|
+
import { existsSync } from "node:fs";
|
|
22
|
+
import { basename } from "node:path";
|
|
23
|
+
|
|
24
|
+
export interface ExplorerSpec {
|
|
25
|
+
focus: string;
|
|
26
|
+
prompt: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ExplorerResult {
|
|
30
|
+
focus: string;
|
|
31
|
+
text: string;
|
|
32
|
+
ok: boolean;
|
|
33
|
+
rateLimited: boolean;
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface FanoutOptions {
|
|
38
|
+
model?: string;
|
|
39
|
+
tools?: string[];
|
|
40
|
+
cwd?: string;
|
|
41
|
+
concurrency?: number;
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Read-only tools handed to every sub-explorer. */
|
|
46
|
+
export const READONLY_EXPLORER_TOOLS = ["read", "grep", "glob", "ls", "find", "memory_search", "memory_read"];
|
|
47
|
+
|
|
48
|
+
const HARD_CONCURRENCY_CAP = 2;
|
|
49
|
+
const DEFAULT_TIMEOUT_MS = 4 * 60 * 1000;
|
|
50
|
+
|
|
51
|
+
/** Re-invoke the current phi binary (so the sub-explorer is the same build). */
|
|
52
|
+
function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
53
|
+
const currentScript = process.argv[1];
|
|
54
|
+
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
|
55
|
+
if (currentScript && !isBunVirtualScript && existsSync(currentScript)) {
|
|
56
|
+
return { command: process.execPath, args: [currentScript, ...args] };
|
|
57
|
+
}
|
|
58
|
+
const execName = basename(process.execPath).toLowerCase();
|
|
59
|
+
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
60
|
+
if (!isGenericRuntime) return { command: process.execPath, args };
|
|
61
|
+
return { command: "pi", args };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isRateLimited(text: string): boolean {
|
|
65
|
+
return /\b429\b|rate.?limit(ed)?|too many requests|overloaded|quota exceeded|resource exhausted/i.test(text);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Run one sub-explorer. Never throws; always resolves to an ExplorerResult. */
|
|
69
|
+
function runOneExplorer(spec: ExplorerSpec, opts: FanoutOptions): Promise<ExplorerResult> {
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
const args = ["--mode", "json", "-p", "--no-session"];
|
|
72
|
+
if (opts.model) args.push("--model", opts.model);
|
|
73
|
+
const tools = opts.tools && opts.tools.length > 0 ? opts.tools : READONLY_EXPLORER_TOOLS;
|
|
74
|
+
args.push("--tools", tools.join(","));
|
|
75
|
+
args.push(`Task: ${spec.prompt}`);
|
|
76
|
+
|
|
77
|
+
let proc: ReturnType<typeof spawn>;
|
|
78
|
+
try {
|
|
79
|
+
const inv = getPiInvocation(args);
|
|
80
|
+
proc = spawn(inv.command, inv.args, { cwd: opts.cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
81
|
+
} catch (e) {
|
|
82
|
+
resolve({ focus: spec.focus, text: "", ok: false, rateLimited: false, error: String(e) });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let buffer = "";
|
|
87
|
+
let finalText = "";
|
|
88
|
+
let stderr = "";
|
|
89
|
+
let errorMessage = "";
|
|
90
|
+
let settled = false;
|
|
91
|
+
|
|
92
|
+
const timer = setTimeout(() => {
|
|
93
|
+
try {
|
|
94
|
+
proc.kill();
|
|
95
|
+
} catch {
|
|
96
|
+
/* best effort */
|
|
97
|
+
}
|
|
98
|
+
}, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
99
|
+
|
|
100
|
+
const processLine = (line: string) => {
|
|
101
|
+
if (!line.trim()) return;
|
|
102
|
+
let event: any;
|
|
103
|
+
try {
|
|
104
|
+
event = JSON.parse(line);
|
|
105
|
+
} catch {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (event?.type === "message_end" && event.message?.role === "assistant") {
|
|
109
|
+
const parts = event.message.content;
|
|
110
|
+
if (Array.isArray(parts)) {
|
|
111
|
+
for (const p of parts) {
|
|
112
|
+
if (p?.type === "text" && typeof p.text === "string") finalText = p.text;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (event.message.errorMessage) errorMessage = String(event.message.errorMessage);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const finish = (timedOut: boolean) => {
|
|
120
|
+
if (settled) return;
|
|
121
|
+
settled = true;
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
if (buffer.trim()) processLine(buffer);
|
|
124
|
+
const blob = `${finalText}\n${errorMessage}\n${stderr}`;
|
|
125
|
+
const rateLimited = isRateLimited(blob);
|
|
126
|
+
const ok = !timedOut && finalText.trim().length > 0 && !rateLimited;
|
|
127
|
+
resolve({
|
|
128
|
+
focus: spec.focus,
|
|
129
|
+
text: finalText,
|
|
130
|
+
ok,
|
|
131
|
+
rateLimited,
|
|
132
|
+
error: errorMessage || (ok ? undefined : timedOut ? "timeout" : "no output"),
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
proc.stdout?.on("data", (data) => {
|
|
137
|
+
buffer += data.toString();
|
|
138
|
+
const lines = buffer.split("\n");
|
|
139
|
+
buffer = lines.pop() || "";
|
|
140
|
+
for (const line of lines) processLine(line);
|
|
141
|
+
});
|
|
142
|
+
proc.stderr?.on("data", (data) => {
|
|
143
|
+
stderr += data.toString();
|
|
144
|
+
});
|
|
145
|
+
proc.on("error", (e) => {
|
|
146
|
+
if (settled) return;
|
|
147
|
+
settled = true;
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
resolve({ focus: spec.focus, text: "", ok: false, rateLimited: false, error: String(e) });
|
|
150
|
+
});
|
|
151
|
+
proc.on("close", () => finish(false));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Run the sub-explorers with a hard concurrency cap of 2; if a batch hits a rate
|
|
157
|
+
* limit, drop to sequential for the rest. Returns the merged findings (markdown)
|
|
158
|
+
* plus the raw results. Best-effort: callers should fall back to the normal
|
|
159
|
+
* single-agent EXPLORE when `merged` is empty.
|
|
160
|
+
*/
|
|
161
|
+
export async function runExploreFanout(
|
|
162
|
+
specs: ExplorerSpec[],
|
|
163
|
+
opts: FanoutOptions,
|
|
164
|
+
): Promise<{ results: ExplorerResult[]; merged: string; rateLimited: boolean }> {
|
|
165
|
+
let concurrency = Math.max(1, Math.min(opts.concurrency ?? HARD_CONCURRENCY_CAP, HARD_CONCURRENCY_CAP));
|
|
166
|
+
const results: ExplorerResult[] = [];
|
|
167
|
+
let i = 0;
|
|
168
|
+
while (i < specs.length) {
|
|
169
|
+
const batch = specs.slice(i, i + concurrency);
|
|
170
|
+
const batchResults = await Promise.all(batch.map((s) => runOneExplorer(s, opts)));
|
|
171
|
+
results.push(...batchResults);
|
|
172
|
+
i += batch.length;
|
|
173
|
+
// Adaptive backoff: a rate limit means the single key is saturated, so
|
|
174
|
+
// serialize whatever is left rather than push harder.
|
|
175
|
+
if (batchResults.some((r) => r.rateLimited)) concurrency = 1;
|
|
176
|
+
}
|
|
177
|
+
const ok = results.filter((r) => r.ok && r.text.trim());
|
|
178
|
+
const merged = ok.map((r) => `### Exploration — ${r.focus}\n${r.text.trim()}`).join("\n\n");
|
|
179
|
+
return { results, merged, rateLimited: results.some((r) => r.rateLimited) };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The default narrow-mandate explorer specs for a /plan request. */
|
|
183
|
+
export function defaultExplorerSpecs(description: string): ExplorerSpec[] {
|
|
184
|
+
const base = `Read-only exploration. Do NOT modify any file. Be concise and cite file:line.`;
|
|
185
|
+
return [
|
|
186
|
+
{
|
|
187
|
+
focus: "architecture & reusable patterns",
|
|
188
|
+
prompt: `${base}\nProject request: ${description}\nExplore the codebase architecture: tech stack, key modules/directories, the conventions and existing utilities/functions to REUSE, and how the pieces fit together.`,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
focus: "impacted files",
|
|
192
|
+
prompt: `${base}\nProject request: ${description}\nIdentify the files and code that will be IMPACTED: grep the relevant symbols, find callers and callees, and list the exact files (file:line) that will likely need changes and why.`,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
focus: "risks & constraints",
|
|
196
|
+
prompt: `${base}\nProject request: ${description}\nIdentify risks, edge cases, and constraints to NOT break: behaviour that must be preserved, error handling, security, and tests that cover the area.`,
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
}
|