pi-plans 0.3.0 → 0.3.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/README.md +21 -10
- package/agents/ref-analyst.md +18 -0
- package/index.ts +8 -2
- package/package.json +11 -2
- package/references/pi-planning-workflow.md +5 -5
- package/references/state-and-config.md +21 -6
- package/scripts/validate.ts +1 -0
- package/skills/plan-with-refs/SKILL.md +3 -3
- package/src/code-graph/commands.ts +73 -27
- package/src/code-graph/prompts.ts +1 -1
- package/src/config-command.ts +35 -0
- package/src/exec.ts +149 -4
- package/src/guard.ts +14 -1
- package/src/refine-prompts.ts +59 -0
- package/src/refine-ui-state.ts +1 -1
- package/src/refine-ui.ts +1 -1
- package/src/state.ts +17 -1
- package/src/subagent.ts +1 -0
- package/src/termination-prompt.ts +22 -0
- package/tests/analyze-refs.test.ts +265 -0
- package/tests/ask-choice.test.ts +1 -0
- package/tests/code-graph-apply-action.test.ts +173 -0
- package/tests/config-command.test.ts +8 -0
- package/tests/exec.test.ts +145 -2
- package/tests/guard.test.ts +27 -1
- package/tests/plans.test.ts +10 -0
- package/tests/refine-prompts.test.ts +35 -1
- package/tests/refine-ui.test.ts +34 -0
- package/tests/state.test.ts +32 -0
- package/tests/subagent.test.ts +22 -0
- package/tools/analyze-refs.ts +263 -0
- package/tools/ask-choice.ts +2 -1
- package/tools/code-graph.ts +25 -2
- package/tools/graph-aware-file-tools.ts +2 -2
- package/tools/plans.ts +14 -2
package/src/exec.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
import { getRun, readActive, resolveStateRootOrNull, setRunStatus, utcNow } from "./state.ts";
|
|
37
37
|
import { graphBlockForExecutor } from "./code-graph/prompts.ts";
|
|
38
38
|
import { resolveGraphMode } from "./code-graph/mode.ts";
|
|
39
|
+
import { TERMINATION_QUESTION, TERMINATION_OPTIONS, renderTerminationOptions } from "./termination-prompt.ts";
|
|
39
40
|
import {
|
|
40
41
|
extractCoverage,
|
|
41
42
|
latestPlanVersion,
|
|
@@ -58,8 +59,21 @@ export interface ExecState {
|
|
|
58
59
|
implItems?: ImplItem[];
|
|
59
60
|
implStatus?: Record<string, ImplMarkerState>;
|
|
60
61
|
currentI?: string;
|
|
62
|
+
goalWait?: GoalWaitState;
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
export interface GoalWaitState {
|
|
66
|
+
noProgressRounds: number;
|
|
67
|
+
waitRounds: number;
|
|
68
|
+
/** Marker/progress snapshot of the last goal-wait round; null = baseline not set. */
|
|
69
|
+
lastMarkers: string | null;
|
|
70
|
+
paused: boolean;
|
|
71
|
+
pausedReason?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const GOAL_WAIT_MAX_NO_PROGRESS = 3;
|
|
75
|
+
const GOAL_WAIT_MAX_WAITING = 6;
|
|
76
|
+
|
|
63
77
|
let execution: ExecState | null = null;
|
|
64
78
|
|
|
65
79
|
// Execution-loop persistence is deferred until the agent settles so turn_end
|
|
@@ -198,7 +212,14 @@ function formatToks(tokens: number): string {
|
|
|
198
212
|
|
|
199
213
|
export function formatExecutionStatusLine(execution: ExecState): string {
|
|
200
214
|
const progress = computeExecutionProgress(execution);
|
|
201
|
-
|
|
215
|
+
let line = `⌛ plans ${progress.done}/${progress.total}: spent ${formatElapsed(execution.startedAt)} · ${formatToks(execution.usage.inToks)} in-toks · ${formatToks(execution.usage.outToks)} out-toks`;
|
|
216
|
+
const goalWait = execution.goalWait;
|
|
217
|
+
if (goalWait?.paused) {
|
|
218
|
+
line += ` · ⏸ goal-wait paused (${goalWait.pausedReason ?? "paused"})`;
|
|
219
|
+
} else if (goalWait && (goalWait.noProgressRounds > 0 || goalWait.waitRounds > 0)) {
|
|
220
|
+
line += ` · 🔁 goal-wait · 无进展 ${goalWait.noProgressRounds}/3 · 等待 ${goalWait.waitRounds}/6`;
|
|
221
|
+
}
|
|
222
|
+
return line;
|
|
202
223
|
}
|
|
203
224
|
|
|
204
225
|
export function updateStatusWidget(ctx: ExtensionContext): void {
|
|
@@ -250,6 +271,7 @@ function persist(pi: ExtensionAPI): void {
|
|
|
250
271
|
implItems: execution.implItems,
|
|
251
272
|
implStatus: execution.implStatus,
|
|
252
273
|
currentI: execution.currentI,
|
|
274
|
+
goalWait: execution.goalWait,
|
|
253
275
|
});
|
|
254
276
|
}
|
|
255
277
|
|
|
@@ -260,7 +282,10 @@ export async function startExecution(
|
|
|
260
282
|
items: CheckItem[],
|
|
261
283
|
implItems?: ImplItem[],
|
|
262
284
|
): Promise<void> {
|
|
263
|
-
execution = { planPath, items, startedAt: utcNow(), usage: { inToks: 0, outToks: 0 }, implItems: implItems ?? [], implStatus: {} };
|
|
285
|
+
execution = { planPath, items, startedAt: utcNow(), usage: { inToks: 0, outToks: 0 }, implItems: implItems ?? [], implStatus: {}, goalWait: { noProgressRounds: 0, waitRounds: 0, lastMarkers: null, paused: false } };
|
|
286
|
+
// Seed the marker baseline so the first quiet round is counted against a
|
|
287
|
+
// real snapshot instead of counting unconditionally (F-006).
|
|
288
|
+
if (execution.goalWait) execution.goalWait.lastMarkers = goalWaitSnapshot();
|
|
264
289
|
pendingExecutionFlush = false; // fresh run: no inherited flush debt
|
|
265
290
|
resetExecutionCompactionState(ctx);
|
|
266
291
|
persist(pi);
|
|
@@ -336,6 +361,8 @@ export function registerExecutionTurnHandlers(
|
|
|
336
361
|
}
|
|
337
362
|
if (getExecution() && isExecutionComplete()) {
|
|
338
363
|
await completeExecution(pi, ctx);
|
|
364
|
+
} else if (getExecution()) {
|
|
365
|
+
maybeGoalWaitFollowUp(pi, ctx, text);
|
|
339
366
|
}
|
|
340
367
|
await onTurnEnd?.(ctx);
|
|
341
368
|
});
|
|
@@ -465,6 +492,7 @@ export async function handleExecutionCompact(pi: ExtensionAPI, ctx: ExtensionCon
|
|
|
465
492
|
if (!event.willRetry && stats) {
|
|
466
493
|
ctx.ui.notify(formatVccCompactionStats(stats), "info");
|
|
467
494
|
if (followUpPrompt) {
|
|
495
|
+
compactionFollowUpSentThisTurn = true;
|
|
468
496
|
await pi.sendUserMessage?.(followUpPrompt);
|
|
469
497
|
} else if ((event.reason === "threshold" || event.reason === "overflow") && shouldScheduleAutoContinue(continueAfterThresholdCompact, runtimePiVersion(ctx))) {
|
|
470
498
|
state.resumeGuard = true;
|
|
@@ -881,6 +909,109 @@ export function isExecutionComplete(): boolean {
|
|
|
881
909
|
return execution !== null && execution.items.length > 0 && execution.items.every((item) => item.done);
|
|
882
910
|
}
|
|
883
911
|
|
|
912
|
+
let compactionFollowUpSentThisTurn = false;
|
|
913
|
+
|
|
914
|
+
/** Reset per-turn continuation flags at the start of a new agent turn. */
|
|
915
|
+
export function resetGoalWaitTurnFlags(): void {
|
|
916
|
+
compactionFollowUpSentThisTurn = false;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function goalWaitSnapshot(): string {
|
|
920
|
+
if (!execution) return "";
|
|
921
|
+
return JSON.stringify({
|
|
922
|
+
done: execution.items
|
|
923
|
+
.filter((item) => item.done)
|
|
924
|
+
.map((item) => item.id)
|
|
925
|
+
.sort()
|
|
926
|
+
.join("|"),
|
|
927
|
+
implStatus: execution.implStatus ?? {},
|
|
928
|
+
currentI: execution.currentI ?? null,
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function pauseGoalWait(pi: ExtensionAPI, ctx: ExtensionContext, reason: string): void {
|
|
933
|
+
const ex = getExecution();
|
|
934
|
+
if (!ex?.goalWait) return;
|
|
935
|
+
ex.goalWait.paused = true;
|
|
936
|
+
ex.goalWait.pausedReason = reason;
|
|
937
|
+
persist(pi);
|
|
938
|
+
ctx.ui.notify?.(
|
|
939
|
+
`pi-plans: goal-wait paused (${reason}). Send any message or run /plans-execute to resume.`,
|
|
940
|
+
"warning",
|
|
941
|
+
);
|
|
942
|
+
updateStatusWidget(ctx);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Goal-wait continuation: a turn that ends with unpassed VCs gets one light
|
|
947
|
+
* followUp so the worker keeps going (working, or polling an external event
|
|
948
|
+
* per the taught backoff rules). Skipped when the compaction machinery owns
|
|
949
|
+
* continuation for this turn, and paused entirely by the no-progress guard.
|
|
950
|
+
* Note: the tri-flag check here deliberately runs BEFORE index.ts's
|
|
951
|
+
* handleExecutionTurnCompaction consumes resumeGuard — checking after the
|
|
952
|
+
* one-shot consumption would never observe it.
|
|
953
|
+
*/
|
|
954
|
+
function maybeGoalWaitFollowUp(pi: ExtensionAPI, ctx: ExtensionContext, assistantText: string): void {
|
|
955
|
+
const ex = getExecution();
|
|
956
|
+
if (!ex) return;
|
|
957
|
+
ex.goalWait ??= { noProgressRounds: 0, waitRounds: 0, lastMarkers: null, paused: false };
|
|
958
|
+
const goalWait = ex.goalWait;
|
|
959
|
+
if (goalWait.paused) {
|
|
960
|
+
updateStatusWidget(ctx);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
const compaction = executionCompactionState(ctx);
|
|
964
|
+
if (compaction && (compaction.inFlight || compaction.pendingFollowUpPrompt != null || compaction.resumeGuard)) {
|
|
965
|
+
updateStatusWidget(ctx);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (compactionFollowUpSentThisTurn) {
|
|
969
|
+
compactionFollowUpSentThisTurn = false; // the compaction path already queued a continuation
|
|
970
|
+
updateStatusWidget(ctx);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
const snapshot = goalWaitSnapshot();
|
|
974
|
+
const changed = goalWait.lastMarkers !== null && snapshot !== goalWait.lastMarkers;
|
|
975
|
+
goalWait.lastMarkers = snapshot;
|
|
976
|
+
if (!changed) {
|
|
977
|
+
if (/waiting for/i.test(assistantText)) {
|
|
978
|
+
goalWait.waitRounds += 1;
|
|
979
|
+
} else {
|
|
980
|
+
goalWait.noProgressRounds += 1;
|
|
981
|
+
}
|
|
982
|
+
} else {
|
|
983
|
+
goalWait.noProgressRounds = 0;
|
|
984
|
+
goalWait.waitRounds = 0;
|
|
985
|
+
}
|
|
986
|
+
if (goalWait.noProgressRounds >= GOAL_WAIT_MAX_NO_PROGRESS) {
|
|
987
|
+
pauseGoalWait(pi, ctx, `no progress in ${goalWait.noProgressRounds} rounds`);
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (goalWait.waitRounds >= GOAL_WAIT_MAX_WAITING) {
|
|
991
|
+
pauseGoalWait(pi, ctx, `waiting without progress for ${goalWait.waitRounds} rounds`);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const remaining = ex.items.filter((item) => !item.done);
|
|
995
|
+
const remainingIds = remaining.map((item) => `\`${item.id}\``).join(", ");
|
|
996
|
+
pi.sendUserMessage?.(
|
|
997
|
+
`Goal wait: ${remaining.length}/${ex.items.length} verifier items still open (${remainingIds}). Continue the plan — if blocked on an external event, keep waiting per the backoff rules; otherwise resolve the remaining items.`,
|
|
998
|
+
{ deliverAs: "followUp" },
|
|
999
|
+
);
|
|
1000
|
+
updateStatusWidget(ctx);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/** Any external input re-kicks a paused goal-wait (clears pause and counters). */
|
|
1004
|
+
export function resumeGoalWaitIfPaused(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
1005
|
+
const ex = getExecution();
|
|
1006
|
+
if (!ex?.goalWait?.paused) return;
|
|
1007
|
+
ex.goalWait.paused = false;
|
|
1008
|
+
ex.goalWait.pausedReason = undefined;
|
|
1009
|
+
ex.goalWait.noProgressRounds = 0;
|
|
1010
|
+
ex.goalWait.waitRounds = 0;
|
|
1011
|
+
persist(pi);
|
|
1012
|
+
updateStatusWidget(ctx);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
884
1015
|
export async function completeExecution(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
885
1016
|
if (!execution) return;
|
|
886
1017
|
resetExecutionCompactionState(ctx);
|
|
@@ -925,9 +1056,10 @@ export async function completeExecution(pi: ExtensionAPI, ctx: ExtensionContext)
|
|
|
925
1056
|
|
|
926
1057
|
/** Instructions appended to the post-execution completion message in
|
|
927
1058
|
* interactive sessions, telling the agent to enter the goal-running
|
|
928
|
-
* implementation-review loop.
|
|
1059
|
+
* implementation-review loop. Termination options are single-sourced from
|
|
1060
|
+
* src/termination-prompt.ts (shared with the ask_choice trailing branch). */
|
|
929
1061
|
export const AMELIORATION_PROMPT_TEXT = `---
|
|
930
|
-
Goal-running continuation: immediately ask the user now via ask_choice (autoComplete: false, in the session language)
|
|
1062
|
+
Goal-running continuation: immediately ask the user now via ask_choice (autoComplete: false, in the session language) the termination question: "${TERMINATION_QUESTION}" Options (recommended first): ${renderTerminationOptions()}. Then keep running the implementation-review loop without asking whether to continue; the goal-wait option keeps the loop running until no unpassed VCs remain.`;
|
|
931
1063
|
|
|
932
1064
|
/** Injection text for before_agent_start while executing. */
|
|
933
1065
|
export function executionContextMessage(ctx: ExtensionContext): string | null {
|
|
@@ -1015,6 +1147,9 @@ export async function restoreFromSession(pi: ExtensionAPI, ctx: ExtensionContext
|
|
|
1015
1147
|
implItems: snapshot.implItems ?? [],
|
|
1016
1148
|
implStatus: { ...(snapshot.implStatus ?? {}) },
|
|
1017
1149
|
currentI: snapshot.currentI ?? inferCurrentI(snapshot.implItems, snapshot.items, snapshot.implStatus),
|
|
1150
|
+
goalWait: snapshot.goalWait
|
|
1151
|
+
? { ...snapshot.goalWait }
|
|
1152
|
+
: { noProgressRounds: 0, waitRounds: 0, lastMarkers: null, paused: false },
|
|
1018
1153
|
};
|
|
1019
1154
|
for (let i = snapshotIndex + 1; i < entries.length; i++) {
|
|
1020
1155
|
const entry = entries[i];
|
|
@@ -1034,6 +1169,16 @@ export async function restoreFromSession(pi: ExtensionAPI, ctx: ExtensionContext
|
|
|
1034
1169
|
}
|
|
1035
1170
|
}
|
|
1036
1171
|
if (execution) {
|
|
1172
|
+
// D-010: replay may have advanced progress past the persisted baseline.
|
|
1173
|
+
// Recompute the goal-wait markers; new progress resets the guard counters.
|
|
1174
|
+
if (execution.goalWait) {
|
|
1175
|
+
const markerSnapshot = goalWaitSnapshot();
|
|
1176
|
+
if (markerSnapshot !== execution.goalWait.lastMarkers) {
|
|
1177
|
+
execution.goalWait.lastMarkers = markerSnapshot;
|
|
1178
|
+
execution.goalWait.noProgressRounds = 0;
|
|
1179
|
+
execution.goalWait.waitRounds = 0;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1037
1182
|
persist(pi); // refresh snapshot so the next resume has less to rescan
|
|
1038
1183
|
if (isExecutionComplete()) {
|
|
1039
1184
|
// Completed during the rescan: restore the planning model on the way out.
|
package/src/guard.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import * as os from "node:os";
|
|
9
9
|
import * as path from "node:path";
|
|
10
|
-
import { getRun, readActive, resolveStateRootOrNull } from "./state.ts";
|
|
10
|
+
import { getRun, loadConfig, readActive, resolveStateRootOrNull } from "./state.ts";
|
|
11
11
|
|
|
12
12
|
const GUARDED_TOOLS = new Set(["write", "edit"]);
|
|
13
13
|
const GUARDED_STATUSES = new Set(["planning", "accepted"]);
|
|
@@ -31,6 +31,19 @@ export function planningWriteBlockReason(input: GuardInput): string | null {
|
|
|
31
31
|
const allowedRoots = [stateRoot, active.artifact_dir, path.join(os.homedir(), ".cache", "pi-plans")].filter(
|
|
32
32
|
(root): root is string => root !== null,
|
|
33
33
|
);
|
|
34
|
+
// A configured refs root (plan-with-refs downloads) is writable while planning.
|
|
35
|
+
if (stateRoot !== null) {
|
|
36
|
+
try {
|
|
37
|
+
const config = loadConfig(stateRoot);
|
|
38
|
+
if (config.refs_root) {
|
|
39
|
+
allowedRoots.push(
|
|
40
|
+
path.isAbsolute(config.refs_root) ? config.refs_root : path.resolve(input.workdir, config.refs_root),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
/* config read failed: no extra root */
|
|
45
|
+
}
|
|
46
|
+
}
|
|
34
47
|
const allowed = allowedRoots.some((root) => target === root || target.startsWith(`${root}${path.sep}`));
|
|
35
48
|
if (allowed) return null;
|
|
36
49
|
|
package/src/refine-prompts.ts
CHANGED
|
@@ -103,6 +103,65 @@ ${opts.planText}
|
|
|
103
103
|
---8<--- END PLAN CONTENT ---8<---`;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
export interface RefAnalystTaskInput {
|
|
107
|
+
refId: string;
|
|
108
|
+
localPath: string;
|
|
109
|
+
title?: string;
|
|
110
|
+
url?: string;
|
|
111
|
+
kind?: string;
|
|
112
|
+
context?: string;
|
|
113
|
+
languageTag?: string | null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const REF_ANALYST_SECTIONS = [
|
|
117
|
+
"Overview",
|
|
118
|
+
"Key Mechanisms And Design Tradeoffs",
|
|
119
|
+
"Adoptable Ideas For The Target Repo",
|
|
120
|
+
"Pitfalls And Anti-Patterns",
|
|
121
|
+
"Evidence Citations",
|
|
122
|
+
"Coverage",
|
|
123
|
+
"Evidence Gaps",
|
|
124
|
+
] as const;
|
|
125
|
+
|
|
126
|
+
export function refAnalystSections(): readonly string[] {
|
|
127
|
+
return REF_ANALYST_SECTIONS;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Task brief for the plan-with-refs per-reference analysis subagent. */
|
|
131
|
+
export function buildRefAnalystTask(opts: RefAnalystTaskInput): string {
|
|
132
|
+
const titleLine = opts.title ? `\nTitle: ${opts.title}` : "";
|
|
133
|
+
const urlLine = opts.url ? `\nURL: ${opts.url}` : "";
|
|
134
|
+
const kindLine = opts.kind ? `\nKind: ${opts.kind}` : "";
|
|
135
|
+
const contextLine = opts.context ? `\n\nTarget repo context: ${opts.context}` : "";
|
|
136
|
+
const languageLine = opts.languageTag
|
|
137
|
+
? `\n\nWrite all prose in the language with BCP47 tag "${opts.languageTag}". Keep file paths, identifiers, and code snippets verbatim.`
|
|
138
|
+
: "";
|
|
139
|
+
const template = REF_ANALYST_SECTIONS.map((section) => `## ${section}`).join("\n\n(empty)\n\n");
|
|
140
|
+
return `Goal: deep-read this downloaded reference and extract what the target repository should adopt from it.
|
|
141
|
+
|
|
142
|
+
Reference id: ${opts.refId}${titleLine}${urlLine}${kindLine}
|
|
143
|
+
Local path (your working directory): ${opts.localPath}
|
|
144
|
+
|
|
145
|
+
Authority boundary: read-only analysis only. Do not edit, write, delete, commit, push, or spawn subagents. Stay inside the reference directory.
|
|
146
|
+
|
|
147
|
+
Evidence: inspect the reference with read, grep, find, and ls before judging it. Cite files as <relative-path>:<line> for every claim; quote only what you verified.${contextLine}${languageLine}
|
|
148
|
+
|
|
149
|
+
Success criteria: a structured analysis the main agent can paste into REF_ANALYSIS.md and turn into adoption questions.
|
|
150
|
+
|
|
151
|
+
Output: Markdown with exactly these seven top-level sections, in this order:
|
|
152
|
+
|
|
153
|
+
${template}
|
|
154
|
+
|
|
155
|
+
Section contracts:
|
|
156
|
+
- Overview: what the reference is, its scope, maturity, and license (when discoverable).
|
|
157
|
+
- Key Mechanisms And Design Tradeoffs: the mechanisms that make it work and the tradeoffs they embody.
|
|
158
|
+
- Adoptable Ideas For The Target Repo: concrete, portable ideas ranked by expected value; name the target-repo surface each would touch.
|
|
159
|
+
- Pitfalls And Anti-Patterns: what to avoid when borrowing; failure modes the reference itself documents or exhibits.
|
|
160
|
+
- Evidence Citations: the file:line references backing the claims above.
|
|
161
|
+
- Coverage: which parts of the reference you actually read versus skipped.
|
|
162
|
+
- Evidence Gaps: what you could not determine from the reference alone.`;
|
|
163
|
+
}
|
|
164
|
+
|
|
106
165
|
export function buildImplementationCriticizerTask(opts: RefinePromptInput): string {
|
|
107
166
|
return `${buildImplementationSharedHeader("criticizer", opts)}
|
|
108
167
|
|
package/src/refine-ui-state.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SubagentProgressEvent, SubagentResult } from "./subagent.ts";
|
|
2
2
|
|
|
3
|
-
export type RefineOverlayRole = "reviewer" | "criticizer";
|
|
3
|
+
export type RefineOverlayRole = "reviewer" | "criticizer" | "refs";
|
|
4
4
|
export type RefineLaneStatus = "queued" | "running" | "complete" | "failed" | "cancelled";
|
|
5
5
|
export type RefineTranscriptEntryType = "assistant-text" | "thinking" | "tool-call" | "tool-result" | "diagnostic";
|
|
6
6
|
|
package/src/refine-ui.ts
CHANGED
|
@@ -203,7 +203,7 @@ function summaryFor(role: RefineOverlayRole, lanes: RefineLaneState[], modelLabe
|
|
|
203
203
|
const complete = lanes.filter((lane) => lane.status === "complete").length;
|
|
204
204
|
const terminal = lanes.filter((lane) => ["complete", "failed", "cancelled"].includes(lane.status)).length;
|
|
205
205
|
const running = lanes.filter((lane) => lane.status === "running").length;
|
|
206
|
-
const title = role === "reviewer" ? "Reviewer" : "Criticizer";
|
|
206
|
+
const title = role === "reviewer" ? "Reviewer" : role === "refs" ? "Refs" : "Criticizer";
|
|
207
207
|
const visibleTitle = modelLabel ? `${title} (${modelLabel})` : title;
|
|
208
208
|
const state = terminal === lanes.length ? "done" : running > 0 ? `${running} running` : "queued";
|
|
209
209
|
return `${visibleTitle} · ${complete}/${lanes.length} done · ${state}`;
|
package/src/state.ts
CHANGED
|
@@ -43,6 +43,10 @@ export interface PlansConfig {
|
|
|
43
43
|
artifact_root: string;
|
|
44
44
|
artifact_root_source: SettingSource;
|
|
45
45
|
artifact_root_updated_at: string | null;
|
|
46
|
+
/** null = never asked; plan-with-refs must ask once (three options) before downloading. */
|
|
47
|
+
refs_root: string | null;
|
|
48
|
+
refs_root_source: SettingSource;
|
|
49
|
+
refs_root_updated_at: string | null;
|
|
46
50
|
/** null = never asked; the plans tool surfaces a hint so the agent asks once. */
|
|
47
51
|
graph_enabled: boolean | null;
|
|
48
52
|
graph_enabled_updated_at: string | null;
|
|
@@ -83,6 +87,9 @@ export const DEFAULT_CONFIG: PlansConfig = {
|
|
|
83
87
|
artifact_root: DEFAULT_ARTIFACT_ROOT,
|
|
84
88
|
artifact_root_source: "unset",
|
|
85
89
|
artifact_root_updated_at: null,
|
|
90
|
+
refs_root: null,
|
|
91
|
+
refs_root_source: "unset",
|
|
92
|
+
refs_root_updated_at: null,
|
|
86
93
|
graph_enabled: null,
|
|
87
94
|
graph_enabled_updated_at: null,
|
|
88
95
|
};
|
|
@@ -138,7 +145,7 @@ export interface RefEntry {
|
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
export interface SubagentEntry {
|
|
141
|
-
role: "reviewer" | "criticizer";
|
|
148
|
+
role: "reviewer" | "criticizer" | "ref-analyst";
|
|
142
149
|
name: string;
|
|
143
150
|
model?: string | null;
|
|
144
151
|
session_dir?: string;
|
|
@@ -330,6 +337,15 @@ export function setArtifactRoot(workdir: string, artifactRoot: string, source: "
|
|
|
330
337
|
return { config, stateRoot, notices };
|
|
331
338
|
}
|
|
332
339
|
|
|
340
|
+
export function setRefsRoot(workdir: string, refsRoot: string, source: "user" | "auto"): EnsureResult {
|
|
341
|
+
const { config, stateRoot, notices } = ensureState(workdir);
|
|
342
|
+
config.refs_root = refsRoot;
|
|
343
|
+
config.refs_root_source = source;
|
|
344
|
+
config.refs_root_updated_at = utcNow();
|
|
345
|
+
atomicWriteJson(path.join(stateRoot, "config.json"), config);
|
|
346
|
+
return { config, stateRoot, notices };
|
|
347
|
+
}
|
|
348
|
+
|
|
333
349
|
export function setGraphEnabled(workdir: string, enabled: boolean): EnsureResult {
|
|
334
350
|
const { config, stateRoot, notices } = ensureState(workdir);
|
|
335
351
|
config.graph_enabled = enabled;
|
package/src/subagent.ts
CHANGED
|
@@ -306,6 +306,7 @@ export async function runPiSubagent(options: SubagentOptions): Promise<SubagentR
|
|
|
306
306
|
cwd: options.cwd,
|
|
307
307
|
shell: false,
|
|
308
308
|
stdio: ["ignore", "pipe", "pipe"],
|
|
309
|
+
env: { ...process.env, PI_PLANS_REFINER: "1" },
|
|
309
310
|
});
|
|
310
311
|
let buffer = "";
|
|
311
312
|
let closed = false;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the post-execution implementation-review
|
|
3
|
+
* termination question. Consumed by BOTH the goal-running continuation prompt
|
|
4
|
+
* (src/exec.ts AMELIORATION_PROMPT_TEXT) and the ask_choice trailing branch
|
|
5
|
+
* (tools/ask-choice.ts). Pure constants only — no runtime imports, no
|
|
6
|
+
* execution-loop coupling.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const TERMINATION_QUESTION = "How should the implementation-review loop terminate?";
|
|
10
|
+
|
|
11
|
+
export const TERMINATION_OPTIONS = [
|
|
12
|
+
"goal wait: continue until no unpassed VCs remain (auto-continue each round)",
|
|
13
|
+
"until no high-severity finding (hard cap 5 rounds)",
|
|
14
|
+
"1 round",
|
|
15
|
+
"2 rounds",
|
|
16
|
+
"3 rounds",
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
/** "1. <option> 2. <option> …" — recommended (goal wait) first. */
|
|
20
|
+
export function renderTerminationOptions(): string {
|
|
21
|
+
return TERMINATION_OPTIONS.map((option, index) => `${index + 1}. ${option}`).join(" ");
|
|
22
|
+
}
|