pi-harness-runtime 1.1.407 → 1.1.408

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.
Files changed (166) hide show
  1. package/index.ts +78 -0
  2. package/package.json +1 -1
  3. package/packages/a2a-adapter/package.json +1 -1
  4. package/packages/agent-policy/package.json +1 -1
  5. package/packages/architecture-generator/package.json +1 -1
  6. package/packages/auth/package.json +1 -1
  7. package/packages/autonomous-refactor/package.json +1 -1
  8. package/packages/autonomous-runtime/package.json +1 -1
  9. package/packages/cache-strategy/package.json +1 -1
  10. package/packages/capability-registry/package.json +1 -1
  11. package/packages/checkpoint/package.json +1 -1
  12. package/packages/cli-plugin-sdk/package.json +1 -1
  13. package/packages/clipboard/package.json +1 -1
  14. package/packages/clipboard-plugin/package.json +1 -1
  15. package/packages/code-generation/package.json +1 -1
  16. package/packages/code-review/package.json +1 -1
  17. package/packages/codex-adapter/package.json +1 -1
  18. package/packages/codex-watcher/package.json +1 -1
  19. package/packages/config-capture/package.json +1 -1
  20. package/packages/connection-runtime/package.json +1 -1
  21. package/packages/context-compiler/package.json +1 -1
  22. package/packages/context-discovery/package.json +1 -1
  23. package/packages/context-manager/package.json +1 -1
  24. package/packages/contracts/package.json +1 -1
  25. package/packages/cookie-sanitizer/package.json +1 -1
  26. package/packages/cost-optimizer/package.json +1 -1
  27. package/packages/dependency-analyzer/package.json +1 -1
  28. package/packages/django-plugin/package.json +1 -1
  29. package/packages/doc-generator/package.json +1 -1
  30. package/packages/durable-review/package.json +1 -1
  31. package/packages/e2e-tester/package.json +1 -1
  32. package/packages/evaluation-engine/package.json +1 -1
  33. package/packages/evaluation-runner/package.json +1 -1
  34. package/packages/event-bus/package.json +1 -1
  35. package/packages/event-sink/package.json +1 -1
  36. package/packages/event-store/package.json +1 -1
  37. package/packages/experience-replay/package.json +1 -1
  38. package/packages/feedback-collector/package.json +1 -1
  39. package/packages/file-copy-helper/package.json +1 -1
  40. package/packages/framework-detector/package.json +1 -1
  41. package/packages/framework-plugin-sdk/package.json +1 -1
  42. package/packages/frappe-plugin/package.json +1 -1
  43. package/packages/generic-web-plugin/package.json +1 -1
  44. package/packages/health-monitor/package.json +1 -1
  45. package/packages/intent-analyzer/package.json +1 -1
  46. package/packages/jev-judge/package.json +1 -1
  47. package/packages/knowledge-graph/package.json +1 -1
  48. package/packages/knowledge-retrieval/package.json +1 -1
  49. package/packages/laravel-plugin/package.json +1 -1
  50. package/packages/learning-engine/package.json +1 -1
  51. package/packages/logging/package.json +1 -1
  52. package/packages/mcp-adapter/package.json +1 -1
  53. package/packages/memory-engine/package.json +1 -1
  54. package/packages/milestone-manager/package.json +1 -1
  55. package/packages/model-registry/package.json +1 -1
  56. package/packages/nextjs-plugin/package.json +1 -1
  57. package/packages/notification/package.json +1 -1
  58. package/packages/observability/package.json +1 -1
  59. package/packages/okf-indexer/package.json +1 -1
  60. package/packages/paperclip-integration/package.json +1 -1
  61. package/packages/performance-optimizer/package.json +1 -1
  62. package/packages/permission-mode/package.json +1 -1
  63. package/packages/privilege-broker/package.json +1 -1
  64. package/packages/project-analyzer/package.json +1 -1
  65. package/packages/project-bootstrap/package.json +1 -1
  66. package/packages/projection-engine/package.json +1 -1
  67. package/packages/prompt-compiler/package.json +1 -1
  68. package/packages/prompt-versioning/package.json +1 -1
  69. package/packages/provider-adapter-sdk/package.json +1 -1
  70. package/packages/provider-router/package.json +1 -1
  71. package/packages/provider-selector/package.json +1 -1
  72. package/packages/providers/package.json +1 -1
  73. package/packages/qdrant-skills/package.json +1 -1
  74. package/packages/quota-manager/package.json +1 -1
  75. package/packages/rate-limiter/package.json +1 -1
  76. package/packages/react-vite-plugin/package.json +1 -1
  77. package/packages/relay/package.json +1 -1
  78. package/packages/release-manager/package.json +1 -1
  79. package/packages/requirement-compiler/package.json +1 -1
  80. package/packages/runtime/package.json +1 -1
  81. package/packages/scheduler/package.json +1 -1
  82. package/packages/scheduler-adapter/package.json +1 -1
  83. package/packages/session/package.json +1 -1
  84. package/packages/session-api/package.json +1 -1
  85. package/packages/session-export/package.json +1 -1
  86. package/packages/shared-context/package.json +1 -1
  87. package/packages/skill-mcp-client/package.json +1 -1
  88. package/packages/skill-registry/package.json +1 -1
  89. package/packages/skills/package.json +1 -1
  90. package/packages/sprint-planner/package.json +1 -1
  91. package/packages/subscription-engine/package.json +1 -1
  92. package/packages/task-compiler/package.json +1 -1
  93. package/packages/telegram/package.json +1 -1
  94. package/packages/tencentdb-memory/package.json +1 -1
  95. package/packages/tencentdb-sync/package.json +1 -1
  96. package/packages/test-data-generator/package.json +1 -1
  97. package/packages/test-generator/package.json +1 -1
  98. package/packages/todo-bd-sync/package.json +1 -1
  99. package/packages/token-estimation/package.json +1 -1
  100. package/packages/token-optimizer/package.json +1 -1
  101. package/packages/tooling-preference/package.json +1 -1
  102. package/packages/trajectory/package.json +1 -1
  103. package/packages/tui/package.json +1 -1
  104. package/packages/two-phase-review/dist/classify.d.ts +56 -0
  105. package/packages/two-phase-review/dist/classify.d.ts.map +1 -0
  106. package/packages/two-phase-review/dist/classify.js +108 -0
  107. package/packages/two-phase-review/dist/classify.js.map +1 -0
  108. package/packages/two-phase-review/dist/index.d.ts +20 -0
  109. package/packages/two-phase-review/dist/index.d.ts.map +1 -0
  110. package/packages/two-phase-review/dist/index.js +14 -0
  111. package/packages/two-phase-review/dist/index.js.map +1 -0
  112. package/packages/two-phase-review/dist/rules.d.ts +81 -0
  113. package/packages/two-phase-review/dist/rules.d.ts.map +1 -0
  114. package/packages/two-phase-review/dist/rules.js +195 -0
  115. package/packages/two-phase-review/dist/rules.js.map +1 -0
  116. package/packages/two-phase-review/dist/runtime.d.ts +70 -0
  117. package/packages/two-phase-review/dist/runtime.d.ts.map +1 -0
  118. package/packages/two-phase-review/dist/runtime.js +298 -0
  119. package/packages/two-phase-review/dist/runtime.js.map +1 -0
  120. package/packages/two-phase-review/dist/scope.d.ts +68 -0
  121. package/packages/two-phase-review/dist/scope.d.ts.map +1 -0
  122. package/packages/two-phase-review/dist/scope.js +205 -0
  123. package/packages/two-phase-review/dist/scope.js.map +1 -0
  124. package/packages/two-phase-review/dist/signals.d.ts +74 -0
  125. package/packages/two-phase-review/dist/signals.d.ts.map +1 -0
  126. package/packages/two-phase-review/dist/signals.js +297 -0
  127. package/packages/two-phase-review/dist/signals.js.map +1 -0
  128. package/packages/two-phase-review/dist/state.d.ts +72 -0
  129. package/packages/two-phase-review/dist/state.d.ts.map +1 -0
  130. package/packages/two-phase-review/dist/state.js +238 -0
  131. package/packages/two-phase-review/dist/state.js.map +1 -0
  132. package/packages/two-phase-review/dist/types.d.ts +62 -0
  133. package/packages/two-phase-review/dist/types.d.ts.map +1 -0
  134. package/packages/two-phase-review/dist/types.js +9 -0
  135. package/packages/two-phase-review/dist/types.js.map +1 -0
  136. package/packages/two-phase-review/package.json +21 -0
  137. package/packages/two-phase-review/src/classify.ts +129 -0
  138. package/packages/two-phase-review/src/index.ts +110 -0
  139. package/packages/two-phase-review/src/rules.ts +236 -0
  140. package/packages/two-phase-review/src/runtime.ts +392 -0
  141. package/packages/two-phase-review/src/scope.ts +247 -0
  142. package/packages/two-phase-review/src/signals.ts +340 -0
  143. package/packages/two-phase-review/src/state.ts +280 -0
  144. package/packages/two-phase-review/src/types.ts +72 -0
  145. package/packages/two-phase-review/test/classify.test.ts +102 -0
  146. package/packages/two-phase-review/test/helpers.ts +72 -0
  147. package/packages/two-phase-review/test/rules.test.ts +150 -0
  148. package/packages/two-phase-review/test/runtime.test.ts +362 -0
  149. package/packages/two-phase-review/test/scope.test.ts +186 -0
  150. package/packages/two-phase-review/test/signals.test.ts +183 -0
  151. package/packages/two-phase-review/test/state.test.ts +201 -0
  152. package/packages/types/package.json +1 -1
  153. package/packages/voice-api/package.json +1 -1
  154. package/packages/webmcp-tester/package.json +1 -1
  155. package/packages/wiki/package.json +1 -1
  156. package/packages/workflow-events/package.json +1 -1
  157. package/packages/workspace-scanner/package.json +1 -1
  158. package/packages/worktree/dist/worktree.d.ts +31 -0
  159. package/packages/worktree/dist/worktree.d.ts.map +1 -1
  160. package/packages/worktree/dist/worktree.js +21 -8
  161. package/packages/worktree/dist/worktree.js.map +1 -1
  162. package/packages/worktree/package.json +1 -1
  163. package/packages/worktree/src/worktree.ts +49 -8
  164. package/packages/worktree/test/worktree.test.ts +27 -27
  165. package/packages/write-review/package.json +1 -1
  166. package/packages/worktree/worktree.ts +0 -293
@@ -0,0 +1,236 @@
1
+ /**
2
+ * The workflow rules.
3
+ *
4
+ * The rules are injected into the system prompt: they tell the agent how the
5
+ * two phases are separated and what it has to do in the current phase.
6
+ */
7
+
8
+ import type { ReviewChunk, ReviewSignal, TwoPhasePhase, TwoPhaseState } from "./types.js";
9
+ import {
10
+ REVIEW_FIX_PREFIX,
11
+ REVIEW_PREFIX,
12
+ classifyTodo,
13
+ summarizeTodos,
14
+ type TodoLike,
15
+ } from "./classify.js";
16
+ import { summarizeSignals } from "./signals.js";
17
+
18
+ /** The contract of the workflow. */
19
+ export const TWO_PHASE_CONTRACT = `## Two-phase implementation and review
20
+
21
+ A task with several steps runs in two phases.
22
+
23
+ ### Phase 1 - implementation
24
+ - Work through every open implementation todo of the current request.
25
+ - Do not start a comprehensive review between implementation todos. A review that is requested while the implementation runs is queued, not executed.
26
+ - Keep the task-level checks active: syntax errors, broken imports, type errors of the code you write right now and failing required tests are part of the implementation and are fixed now.
27
+ - Broader feedback (style, maintainability, a full diff review) is queued as one [REVIEW] todo. Never discard it.
28
+ - Queued review work is not an implementation blocker. The request is not complete while implementation todos remain.
29
+
30
+ ### Phase 2 - review
31
+ - The review covers the diff of the task, not the whole repository.
32
+ - Review the scope in chunks, per module. Use independent reviewers for independent chunks.
33
+ - Record every confirmed finding as a [REVIEW-FIX] todo, grouped by root cause and severity. A minor suggestion does not interrupt a running chunk.
34
+ - Fix the confirmed findings after the review pass, check the changed area, and run one final integration review over the task diff.
35
+ - The request is complete only when the implementation todos, the review chunks and the required fix todos are done or explicitly accepted.
36
+
37
+ ### Todo prefixes
38
+ - [IMPLEMENT] implementation work of the current request
39
+ - ${REVIEW_PREFIX} the queued review of the task diff
40
+ - ${REVIEW_FIX_PREFIX} a confirmed finding that has to be fixed
41
+
42
+ A user pause, an approval requirement, a security issue or any other safety rule stops the work immediately. The phase order never overrides the intent of the user.`;
43
+
44
+ /** The current state of the workflow as the agent sees it. */
45
+ export interface PhaseView {
46
+ phase: TwoPhasePhase;
47
+ /** How many implementation todos are still open. */
48
+ implementationOpen: number;
49
+ /** How many review todos are open. */
50
+ reviewOpen: number;
51
+ /** How many review-fix todos are open. */
52
+ reviewFixOpen: number;
53
+ /** How many files the review covers. */
54
+ scopeFiles: number;
55
+ /** The review chunks and their state. */
56
+ chunks: { id: string; label: string; status: string }[];
57
+ /** Is a review queued? */
58
+ queued: boolean;
59
+ /** The signals that queued the review. */
60
+ signals: ReviewSignal[];
61
+ }
62
+
63
+ /** Build the phase view from the state and the todo list. */
64
+ export function buildPhaseView(state: TwoPhaseState, todos: TodoLike[]): PhaseView {
65
+ const summary = summarizeTodos(todos);
66
+ return {
67
+ phase: state.phase,
68
+ implementationOpen: summary.implementation,
69
+ reviewOpen: summary.review,
70
+ reviewFixOpen: summary.reviewFix,
71
+ scopeFiles: state.scopeFiles.length,
72
+ chunks: state.chunks.map((chunk) => ({
73
+ id: chunk.id,
74
+ label: chunk.label,
75
+ status: chunk.status,
76
+ })),
77
+ queued: state.signals.length > 0,
78
+ signals: state.signals,
79
+ };
80
+ }
81
+
82
+ /** The subject of the one queued review todo. */
83
+ export function buildReviewTodoSubject(state: TwoPhaseState): string {
84
+ const summary = summarizeSignals(state.signals);
85
+ const first = summary.sources[0] ?? "review signal";
86
+ const more = summary.sources.length > 1 ? ` +${summary.sources.length - 1} more` : "";
87
+ return `${REVIEW_PREFIX} Review the task diff (queued by ${first}${more})`;
88
+ }
89
+
90
+ /** The description of the one queued review todo. */
91
+ export function buildReviewTodoDescription(state: TwoPhaseState): string {
92
+ const summary = summarizeSignals(state.signals);
93
+ const lines: string[] = [];
94
+ if (summary.sources.length > 0) lines.push(`Source: ${summary.sources.join(", ")}`);
95
+ if (summary.reason) lines.push(`Reason: ${summary.reason}`);
96
+ if (state.scopeFiles.length > 0) {
97
+ const shown = state.scopeFiles.slice(0, 20);
98
+ const rest = state.scopeFiles.length - shown.length;
99
+ lines.push(`Scope: ${shown.join(", ")}${rest > 0 ? ` (+${rest} more)` : ""}`);
100
+ }
101
+ lines.push("The review starts after the implementation todos are done.");
102
+ return lines.join("\n");
103
+ }
104
+
105
+ /**
106
+ * The status line of the workflow.
107
+ *
108
+ * The line is plain ASCII: it is shown in the footer of the session.
109
+ */
110
+ export function formatPhaseStatus(state: TwoPhaseState): string {
111
+ switch (state.phase) {
112
+ case "implementation":
113
+ return "[PHASE] implementation";
114
+ case "review_pending":
115
+ return "[PHASE] review_pending";
116
+ case "review": {
117
+ const done = state.chunks.filter((chunk) => chunk.status === "done").length;
118
+ return `[PHASE] review (${done}/${state.chunks.length} chunk(s), ${state.scopeFiles.length} file(s))`;
119
+ }
120
+ case "review_fixes":
121
+ return `[PHASE] review_fixes (${state.findings.length} finding(s))`;
122
+ case "complete":
123
+ return "[PHASE] complete";
124
+ default:
125
+ return "[PHASE] implementation";
126
+ }
127
+ }
128
+
129
+ /**
130
+ * The subject of the todo that covers one review chunk.
131
+ *
132
+ * The chunk id is part of the subject, so the runtime can tell which chunk a
133
+ * todo belongs to.
134
+ */
135
+ export function buildChunkTodoSubject(chunk: ReviewChunk): string {
136
+ return `${REVIEW_PREFIX} ${chunk.id}: ${chunk.label}`;
137
+ }
138
+
139
+ /** The description of the todo that covers one review chunk. */
140
+ export function buildChunkTodoDescription(chunk: ReviewChunk): string {
141
+ const files = chunk.files.map((file) => `- ${file}`).join("\n");
142
+ return [
143
+ `Review the files of ${chunk.id} (${chunk.label}):`,
144
+ files,
145
+ "",
146
+ `Review only this chunk. Record confirmed findings as ${REVIEW_FIX_PREFIX} todos grouped by root cause and severity.`,
147
+ ].join("\n");
148
+ }
149
+
150
+ /** Does the todo item belong to the review chunk? */
151
+ export function isChunkTodo(todo: TodoLike, chunk: ReviewChunk): boolean {
152
+ if (classifyTodo(todo) !== "review") return false;
153
+ const subject = todo.subject ?? "";
154
+ const at = subject.indexOf(chunk.id);
155
+ if (at < 0) return false;
156
+ // "chunk-1" must not match "chunk-10".
157
+ const after = subject.slice(at + chunk.id.length, at + chunk.id.length + 1);
158
+ return after === "" || !/[0-9a-z]/i.test(after);
159
+ }
160
+
161
+ /**
162
+ * Derive the status of the review chunks from the todo list.
163
+ *
164
+ * The status only moves forward: a chunk that is done stays done, so a todo
165
+ * list that is briefly empty (a restart, a compaction) can never erase the
166
+ * progress of the review.
167
+ */
168
+ export function syncChunkStatus(state: TwoPhaseState, todos: TodoLike[]): TwoPhaseState {
169
+ if (state.chunks.length === 0) return state;
170
+ let changed = false;
171
+ const chunks = state.chunks.map((chunk) => {
172
+ if (chunk.status === "done") return chunk;
173
+ const done = todos.some(
174
+ (todo) => isChunkTodo(todo, chunk) && todo.status === "completed",
175
+ );
176
+ if (!done) return chunk;
177
+ changed = true;
178
+ return { ...chunk, status: "done" as const };
179
+ });
180
+ return changed ? { ...state, chunks } : state;
181
+ }
182
+
183
+ /**
184
+ * The reason a comprehensive review was deferred.
185
+ *
186
+ * The reason has to be actionable: the agent continues with the
187
+ * implementation and keeps the task-level checks alive.
188
+ */
189
+ export function buildDeferralReason(
190
+ state: TwoPhaseState,
191
+ context: { source: string; implementationOpen: number },
192
+ ): string {
193
+ const sources = [...new Set(state.signals.map((signal) => signal.source))];
194
+ return [
195
+ `Phase 1 (implementation) is still running: ${context.implementationOpen} implementation todo(s) are open.`,
196
+ `The comprehensive review (${sources.join(", ") || context.source}) is queued and starts after the implementation todos are done.`,
197
+ "Continue with the implementation. Keep task-level checks (syntax errors, broken imports, type errors of the code you write now, failing required tests) active and fix them now.",
198
+ ].join(" ");
199
+ }
200
+
201
+ /**
202
+ * The instruction for the current phase.
203
+ *
204
+ * The instruction is short: it names the phase, the open work and what the
205
+ * agent has to do next.
206
+ */
207
+ export function buildPhaseInstruction(state: TwoPhaseState, view: PhaseView): string {
208
+ if (state.phase === "implementation") {
209
+ if (!view.queued) {
210
+ return `[two-phase] Phase 1 (implementation): ${view.implementationOpen} implementation todo(s) open. Finish the implementation first; keep task-level checks (type errors, failing tests, build) active. Do not start a comprehensive review.`;
211
+ }
212
+ const summary = summarizeSignals(state.signals);
213
+ return `[two-phase] Phase 1 (implementation): ${view.implementationOpen} implementation todo(s) open. A comprehensive review is queued (${summary.sources.join(", ") || "review signal"}) and must not start yet. Fix correctness problems of the current code now; queue broader feedback as one ${REVIEW_PREFIX} todo. The request is not complete while implementation todos remain.`;
214
+ }
215
+
216
+ if (state.phase === "review_pending") {
217
+ return `[two-phase] Phase 2 (review): the implementation is done, the review scope is being derived from the task diff (${view.scopeFiles} file(s)).`;
218
+ }
219
+
220
+ if (state.phase === "review") {
221
+ const chunkLines = view.chunks.map(
222
+ (chunk) => `- ${chunk.id}: ${chunk.label} [${chunk.status}]`,
223
+ );
224
+ return [
225
+ `[two-phase] Phase 2 (review): the implementation is done. Review scope: ${view.scopeFiles} file(s) in ${view.chunks.length} chunk(s).`,
226
+ ...chunkLines,
227
+ `Review each chunk in scope (never the whole repository). Record confirmed findings as ${REVIEW_FIX_PREFIX} todos grouped by root cause and severity, fix them after the pass, then run one final integration review over the task diff. The request is not complete while review or fix items remain.`,
228
+ ].join("\n");
229
+ }
230
+
231
+ if (state.phase === "review_fixes") {
232
+ return `[two-phase] Phase 2 (review fixes): ${view.reviewFixOpen} review-fix todo(s) open. Fix the confirmed findings, check the changed area, then run one final integration review over the task diff.`;
233
+ }
234
+
235
+ return "[two-phase] The task is complete: implementation, review and the required fixes are done.";
236
+ }
@@ -0,0 +1,392 @@
1
+ /**
2
+ * The runtime wiring of the two-phase workflow.
3
+ *
4
+ * The wiring lives in the package so it can be tested without a Pi session:
5
+ * the extension API, the todo reader and the workspace are injected. Every
6
+ * decision that the runtime takes is conservative - when the state of the
7
+ * todo list is unknown, the workflow waits instead of guessing.
8
+ */
9
+
10
+ import {
11
+ implementationTodos,
12
+ reviewFixTodos,
13
+ reviewTodos,
14
+ type TodoLike,
15
+ } from "./classify.js";
16
+ import {
17
+ detectReviewInvocation,
18
+ explicitReviewRequest,
19
+ parseExtraPatterns,
20
+ } from "./signals.js";
21
+ import { chunkScope, collectScope, currentRevision } from "./scope.js";
22
+ import {
23
+ TwoPhaseStateStore,
24
+ allChunksDone,
25
+ createState,
26
+ hasQueuedReview,
27
+ markComplete,
28
+ recordSignal,
29
+ startReview,
30
+ transitionToReviewFixes,
31
+ transitionToReviewPending,
32
+ } from "./state.js";
33
+ import {
34
+ TWO_PHASE_CONTRACT,
35
+ buildChunkTodoSubject,
36
+ buildDeferralReason,
37
+ buildPhaseInstruction,
38
+ buildPhaseView,
39
+ buildReviewTodoDescription,
40
+ buildReviewTodoSubject,
41
+ formatPhaseStatus,
42
+ syncChunkStatus,
43
+ } from "./rules.js";
44
+ import type { TwoPhasePhase, TwoPhaseState } from "./types.js";
45
+
46
+ /** The marker every message of the harness itself carries. */
47
+ export const HARNESS_MESSAGE_MARKER = "[harness:";
48
+
49
+ /** The part of the extension API the workflow needs. */
50
+ export interface TwoPhaseExtensionApi {
51
+ on(event: string, handler: (...args: any[]) => unknown): void;
52
+ appendEntry?(type: string, data: unknown): void;
53
+ sendUserMessage?(content: string, options?: { deliverAs?: "steer" | "followUp" }): Promise<void> | void;
54
+ }
55
+
56
+ /** The part of the extension context the workflow needs. */
57
+ export interface TwoPhaseExtensionContext {
58
+ sessionManager?: { getSessionId?: () => string | undefined };
59
+ ui?: { setStatus?: (key: string, value: string) => void };
60
+ }
61
+
62
+ /** The result of reading the todo list of the session. */
63
+ export interface TodoReadResult {
64
+ /** Is the todo list readable at all? */
65
+ available: boolean;
66
+ /** The todo items (empty when the list is not readable). */
67
+ todos: TodoLike[];
68
+ /** Why the list is not readable. */
69
+ reason?: string;
70
+ }
71
+
72
+ /** Reads the todo list of the session. */
73
+ export type TodoReader = (ctx: unknown) => Promise<TodoReadResult>;
74
+
75
+ /** The options of the runtime. */
76
+ export interface TwoPhaseRuntimeOptions {
77
+ pi: TwoPhaseExtensionApi;
78
+ /** Reads the todo list of the session. */
79
+ readTodos: TodoReader;
80
+ /** The root of the workspace (the Git repository of the task). */
81
+ workspaceRoot?: () => string;
82
+ /** Where the state of the workflow is stored. */
83
+ store?: TwoPhaseStateStore;
84
+ /** Writes a line into the log of the harness. */
85
+ log?: (message: string) => void;
86
+ /** Extra review tool patterns of the deployment. */
87
+ extraPatterns?: string[];
88
+ }
89
+
90
+ /** The runtime of the workflow. */
91
+ export interface TwoPhaseRuntime {
92
+ /** The persisted state of the workflow. */
93
+ getState(): TwoPhaseState;
94
+ /** The phase the workflow is in (derived from the state and the todo list). */
95
+ getPhase(todos?: TodoLike[]): TwoPhasePhase;
96
+ }
97
+
98
+ function errorMessage(error: unknown): string {
99
+ if (error instanceof Error) return error.message;
100
+ return String(error);
101
+ }
102
+
103
+ /** The text of a message content (a string or a list of parts). */
104
+ function messageText(content: unknown): string {
105
+ if (typeof content === "string") return content;
106
+ if (Array.isArray(content)) {
107
+ return content
108
+ .map((part) => {
109
+ if (!part || typeof part !== "object") return "";
110
+ const entry = part as { type?: string; text?: string };
111
+ return entry.type === "text" ? (entry.text ?? "") : "";
112
+ })
113
+ .join("\n");
114
+ }
115
+ return "";
116
+ }
117
+
118
+ /**
119
+ * Register the two-phase workflow in the session.
120
+ *
121
+ * The handlers are registered before the function returns, so a session that
122
+ * starts right after the registration is already covered.
123
+ */
124
+ export async function initTwoPhaseReview(
125
+ options: TwoPhaseRuntimeOptions,
126
+ ): Promise<TwoPhaseRuntime> {
127
+ const { pi } = options;
128
+ const store = options.store ?? new TwoPhaseStateStore();
129
+ const log = options.log ?? (() => {});
130
+ const workspaceRoot = options.workspaceRoot ?? (() => process.cwd());
131
+ const extraPatterns = options.extraPatterns ?? parseExtraPatterns();
132
+
133
+ let state: TwoPhaseState = createState("default");
134
+ /** The last message of the human of this session (never a harness message). */
135
+ let lastUserMessage = "";
136
+ /** The messages the runtime sent itself. */
137
+ const sentMessages = new Set<string>();
138
+
139
+ const sessionId = (ctx: unknown): string => {
140
+ const context = ctx as TwoPhaseExtensionContext | undefined;
141
+ try {
142
+ return context?.sessionManager?.getSessionId?.() ?? "default";
143
+ } catch {
144
+ return "default";
145
+ }
146
+ };
147
+
148
+ /**
149
+ * Persist a state.
150
+ *
151
+ * The state is only replaced when it was written: the file is the source
152
+ * of truth, so a failed write must not change what the runtime believes.
153
+ */
154
+ const persist = (next: TwoPhaseState): boolean => {
155
+ try {
156
+ state = store.write(next);
157
+ return true;
158
+ } catch (error) {
159
+ log(`state not written: ${errorMessage(error)}`);
160
+ return false;
161
+ }
162
+ };
163
+
164
+ const readTodos = async (ctx: unknown): Promise<TodoReadResult> => {
165
+ try {
166
+ const result = await options.readTodos(ctx);
167
+ if (!result || typeof result !== "object") {
168
+ return { available: false, todos: [], reason: "the todo reader returned nothing" };
169
+ }
170
+ return {
171
+ available: result.available !== false,
172
+ todos: Array.isArray(result.todos) ? result.todos : [],
173
+ reason: result.reason,
174
+ };
175
+ } catch (error) {
176
+ return { available: false, todos: [], reason: errorMessage(error) };
177
+ }
178
+ };
179
+
180
+ const updateStatus = (ctx: unknown): void => {
181
+ const context = ctx as TwoPhaseExtensionContext | undefined;
182
+ try {
183
+ context?.ui?.setStatus?.("harness-review", formatPhaseStatus(state));
184
+ } catch {
185
+ // The status line is a convenience, never a requirement.
186
+ }
187
+ };
188
+
189
+ /**
190
+ * Keep the chunk status and the phase in sync with the todo list.
191
+ *
192
+ * The chunk status only moves forward, so a todo list that is briefly
193
+ * empty can never erase the progress of the review.
194
+ */
195
+ const syncProgress = (todos: TodoLike[]): void => {
196
+ const synced = syncChunkStatus(state, todos);
197
+ const openFixes = reviewFixTodos(todos).length;
198
+ let next = synced;
199
+ if (next.phase === "review" && allChunksDone(next)) {
200
+ next = openFixes > 0 ? transitionToReviewFixes(next) : markComplete(next);
201
+ } else if (next.phase === "review_fixes" && openFixes === 0) {
202
+ next = markComplete(next);
203
+ }
204
+ if (next !== state) persist(next);
205
+ };
206
+
207
+ pi.on("session_start", (_event: unknown, ctx: unknown) => {
208
+ const id = sessionId(ctx);
209
+ state = store.read(id) ?? createState(id);
210
+ if (!state.startRevision) {
211
+ const revision = currentRevision(workspaceRoot());
212
+ if (revision) state = { ...state, startRevision: revision };
213
+ }
214
+ // The intent of a previous session never carries over.
215
+ lastUserMessage = "";
216
+ persist(state);
217
+ updateStatus(ctx);
218
+ });
219
+
220
+ // The intent of the human wins over the phase order.
221
+ pi.on("message_end", (event: { message?: { role?: string; content?: unknown } }) => {
222
+ const message = event?.message;
223
+ if (!message || message.role !== "user") return;
224
+ const text = messageText(message.content).trim();
225
+ if (!text) return;
226
+ // A message of the harness is not the intent of the human.
227
+ if (sentMessages.has(text)) return;
228
+ if (text.startsWith(HARNESS_MESSAGE_MARKER)) return;
229
+ lastUserMessage = text;
230
+ });
231
+
232
+ // A comprehensive review is deferred while the implementation runs.
233
+ pi.on(
234
+ "tool_call",
235
+ async (event: { toolName?: string; input?: unknown }, ctx: unknown) => {
236
+ if (state.phase !== "implementation") return {};
237
+
238
+ const invocation = detectReviewInvocation(
239
+ event?.toolName ?? "",
240
+ event?.input,
241
+ extraPatterns,
242
+ );
243
+ if (!invocation) return {};
244
+ // The human asked for the review explicitly: the phase order does not win.
245
+ if (explicitReviewRequest(lastUserMessage)) return {};
246
+
247
+ const read = await readTodos(ctx);
248
+ if (!read.available) {
249
+ // The implementation state is unknown, so the review waits.
250
+ return {
251
+ block: true,
252
+ reason: `The todo list of the session is not readable (${read.reason ?? "unknown reason"}), so the workflow cannot prove that the implementation is done. The comprehensive review is deferred.`,
253
+ };
254
+ }
255
+
256
+ const openImplementation = implementationTodos(read.todos).length;
257
+ if (openImplementation === 0) return {};
258
+
259
+ const next = recordSignal(state, {
260
+ source: invocation.source,
261
+ files: invocation.files,
262
+ reason: invocation.reason,
263
+ at: new Date().toISOString(),
264
+ });
265
+ if (next !== state) {
266
+ if (!persist(next)) {
267
+ // The signal is not persisted, so it is not queued either.
268
+ return {
269
+ block: true,
270
+ reason: buildDeferralReason(state, {
271
+ source: invocation.source,
272
+ implementationOpen: openImplementation,
273
+ }),
274
+ };
275
+ }
276
+ // The queued item is part of the session, so a compaction cannot lose it.
277
+ try {
278
+ pi.appendEntry?.("harness-two-phase-review", {
279
+ queued: {
280
+ source: invocation.source,
281
+ files: invocation.files,
282
+ reason: invocation.reason,
283
+ subject: buildReviewTodoSubject(state),
284
+ description: buildReviewTodoDescription(state),
285
+ },
286
+ });
287
+ } catch {
288
+ // The session entry is a copy; the state file is the truth.
289
+ }
290
+ }
291
+ updateStatus(ctx);
292
+
293
+ return {
294
+ block: true,
295
+ reason: buildDeferralReason(state, {
296
+ source: invocation.source,
297
+ implementationOpen: openImplementation,
298
+ }),
299
+ };
300
+ },
301
+ );
302
+
303
+ // The implementation is done: the review scope is derived from the task diff.
304
+ pi.on("turn_end", async (_event: unknown, ctx: unknown) => {
305
+ const read = await readTodos(ctx);
306
+ if (!read.available) {
307
+ // The implementation state is unknown, so the phase does not move.
308
+ updateStatus(ctx);
309
+ return undefined;
310
+ }
311
+
312
+ if (state.phase === "implementation") {
313
+ if (implementationTodos(read.todos).length > 0) {
314
+ updateStatus(ctx);
315
+ return undefined;
316
+ }
317
+ const queued = hasQueuedReview(state) || reviewTodos(read.todos).length > 0;
318
+ if (!queued) {
319
+ updateStatus(ctx);
320
+ return undefined;
321
+ }
322
+
323
+ const scope = collectScope(workspaceRoot(), state.startRevision);
324
+ let next = transitionToReviewPending(state, scope.files);
325
+ next = startReview(next, chunkScope(scope.files));
326
+ if (!persist(next)) {
327
+ // Without a persisted state the review must not start.
328
+ updateStatus(ctx);
329
+ return undefined;
330
+ }
331
+ updateStatus(ctx);
332
+
333
+ const lines: string[] = [
334
+ "[harness:two-phase] The implementation todos of this request are done. Phase 2 (review) starts now.",
335
+ `Review scope: ${scope.files.length} file(s) (${scope.reason}).`,
336
+ ...state.chunks.map((chunk) => `- ${chunk.id}: ${chunk.label}`),
337
+ "Review every chunk in scope (never the whole repository). Record confirmed findings as [REVIEW-FIX] todos grouped by root cause and severity, fix them after the pass, then run one final integration review over the task diff.",
338
+ "Create one [REVIEW] todo per chunk with the chunk id in the subject and complete it when the chunk is reviewed:",
339
+ ...state.chunks.map((chunk) => `- ${buildChunkTodoSubject(chunk)}`),
340
+ `Keep exactly one queued review item: ${buildReviewTodoSubject(state)}`,
341
+ ];
342
+ const message = lines.join("\n");
343
+ try {
344
+ sentMessages.add(message);
345
+ await pi.sendUserMessage?.(message, { deliverAs: "steer" });
346
+ log(`review started (${scope.files.length} file(s))`);
347
+ } catch (error) {
348
+ log(`review steer failed: ${errorMessage(error)}`);
349
+ }
350
+ return undefined;
351
+ }
352
+
353
+ // Phase 2: the chunk status and the phase follow the review todos.
354
+ syncProgress(read.todos);
355
+ updateStatus(ctx);
356
+ return undefined;
357
+ });
358
+
359
+ // The workflow rules and the current phase reach the agent.
360
+ pi.on("before_agent_start", async (event: { systemPrompt?: string }, ctx: unknown) => {
361
+ const read = await readTodos(ctx);
362
+ if (!read.available) return undefined;
363
+ syncProgress(read.todos);
364
+
365
+ const view = buildPhaseView(state, read.todos);
366
+ // The workflow is only active when it has something to say.
367
+ const active =
368
+ view.implementationOpen > 0 ||
369
+ view.reviewOpen > 0 ||
370
+ view.reviewFixOpen > 0 ||
371
+ state.signals.length > 0;
372
+ if (!active) return undefined;
373
+
374
+ // The system prompt is built per run, so the contract belongs to every
375
+ // run of the phase, not only to the first one.
376
+ const parts: string[] = [TWO_PHASE_CONTRACT];
377
+ const instruction = buildPhaseInstruction(state, view);
378
+ if (instruction) parts.push(instruction);
379
+
380
+ return { systemPrompt: `${event?.systemPrompt ?? ""}\n\n${parts.join("\n\n")}` };
381
+ });
382
+
383
+ return {
384
+ getState: () => state,
385
+ getPhase: (todos: TodoLike[] = []) => {
386
+ if (state.phase === "complete" && reviewFixTodos(todos).length > 0) {
387
+ return "review_fixes";
388
+ }
389
+ return state.phase;
390
+ },
391
+ };
392
+ }