avtc-pi-featyard 1.1.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.
Files changed (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +194 -0
  3. package/agents/fy-design-reviewer.md +77 -0
  4. package/agents/fy-feature-verifier.md +24 -0
  5. package/agents/fy-general-reviewer.md +81 -0
  6. package/agents/fy-guidelines-reviewer.md +46 -0
  7. package/agents/fy-implementer.md +20 -0
  8. package/agents/fy-performance-reviewer.md +46 -0
  9. package/agents/fy-plan-reviewer.md +74 -0
  10. package/agents/fy-plan-verifier.md +44 -0
  11. package/agents/fy-quality-reviewer.md +50 -0
  12. package/agents/fy-requirements-reviewer.md +46 -0
  13. package/agents/fy-researcher.md +6 -0
  14. package/agents/fy-security-reviewer.md +46 -0
  15. package/agents/fy-task-verifier.md +42 -0
  16. package/agents/fy-testing-reviewer.md +60 -0
  17. package/assets/images/kanban-board.png +0 -0
  18. package/assets/images/settings-dropdown.png +0 -0
  19. package/assets/images/settings.png +0 -0
  20. package/assets/images/widget.png +0 -0
  21. package/docs/CONFIGURATION.md +266 -0
  22. package/index.ts +8 -0
  23. package/package.json +85 -0
  24. package/skills/fy-design/SKILL.md +108 -0
  25. package/skills/fy-design-review/SKILL.md +45 -0
  26. package/skills/fy-finish/SKILL.md +28 -0
  27. package/skills/fy-implement/SKILL.md +36 -0
  28. package/skills/fy-plan/SKILL.md +77 -0
  29. package/skills/fy-plan-review/SKILL.md +45 -0
  30. package/skills/fy-research/SKILL.md +38 -0
  31. package/skills/fy-review/SKILL.md +88 -0
  32. package/skills/fy-verify/SKILL.md +23 -0
  33. package/src/commands/auto-agent-commands.ts +748 -0
  34. package/src/commands/kanban-commands.ts +278 -0
  35. package/src/commands/workflow-commands.ts +531 -0
  36. package/src/compaction/compact-handler.ts +371 -0
  37. package/src/compaction/compact-message.ts +52 -0
  38. package/src/compaction/compact-trigger.ts +121 -0
  39. package/src/compaction/safe-editor-write.ts +18 -0
  40. package/src/events/agent/agent-end.ts +19 -0
  41. package/src/events/agent/agent-settled.ts +20 -0
  42. package/src/events/agent/agent-start.ts +18 -0
  43. package/src/events/agent/turn-end.ts +20 -0
  44. package/src/events/agent/turn-start.ts +19 -0
  45. package/src/events/event-deps.ts +33 -0
  46. package/src/events/index.ts +91 -0
  47. package/src/events/input/input.ts +143 -0
  48. package/src/events/session/context.ts +68 -0
  49. package/src/events/session/model-select.ts +20 -0
  50. package/src/events/session/session-compact.ts +20 -0
  51. package/src/events/session/session-shutdown.ts +26 -0
  52. package/src/events/session/session-start.ts +45 -0
  53. package/src/events/session/session-tree.ts +17 -0
  54. package/src/events/tool/tool-call.ts +58 -0
  55. package/src/events/tool/tool-result.ts +54 -0
  56. package/src/git/git-queries.ts +154 -0
  57. package/src/git/resolve-base-branch.ts +173 -0
  58. package/src/git/worktrees/worktree-helpers.ts +142 -0
  59. package/src/git/worktrees/worktree-interception.ts +66 -0
  60. package/src/git/worktrees/worktree-lifecycle.ts +380 -0
  61. package/src/guardrails/completion-gating.ts +63 -0
  62. package/src/guardrails/file-classifier.ts +461 -0
  63. package/src/guardrails/force-add-guard.ts +312 -0
  64. package/src/guardrails/guardrail-tracker.ts +73 -0
  65. package/src/guardrails/guardrail-violations.ts +47 -0
  66. package/src/guardrails/guardrails-engine.ts +663 -0
  67. package/src/guardrails/session-guardrails.ts +75 -0
  68. package/src/guardrails/shell-decompose.ts +285 -0
  69. package/src/guardrails/tdd-enforcement.ts +122 -0
  70. package/src/guardrails/test-output.ts +272 -0
  71. package/src/index.ts +598 -0
  72. package/src/integrations/parallel-work-guardrail-integration.ts +29 -0
  73. package/src/integrations/subagent-integration.ts +166 -0
  74. package/src/integrations/todo-integration.ts +58 -0
  75. package/src/kanban/auto-agent/auto-agent-cleanup.ts +37 -0
  76. package/src/kanban/auto-agent/auto-agent-events.ts +142 -0
  77. package/src/kanban/auto-agent/auto-agent-grace-period.ts +83 -0
  78. package/src/kanban/auto-agent/auto-agent-lifecycle.ts +497 -0
  79. package/src/kanban/auto-agent/auto-agent-notify.ts +43 -0
  80. package/src/kanban/auto-agent/auto-agent-pause.ts +44 -0
  81. package/src/kanban/auto-agent/auto-agent-state-machine.ts +629 -0
  82. package/src/kanban/data/kanban-database.ts +851 -0
  83. package/src/kanban/data/kanban-db-schema.ts +137 -0
  84. package/src/kanban/data/kanban-detect-project.ts +43 -0
  85. package/src/kanban/data/kanban-move-feature.ts +43 -0
  86. package/src/kanban/data/kanban-types.ts +64 -0
  87. package/src/kanban/ensure-feature.ts +226 -0
  88. package/src/kanban/kanban-board-ui/app.js +1125 -0
  89. package/src/kanban/kanban-board-ui/csv-parser.d.ts +5 -0
  90. package/src/kanban/kanban-board-ui/csv-parser.js +80 -0
  91. package/src/kanban/kanban-board-ui/index.html +127 -0
  92. package/src/kanban/kanban-board-ui/style.css +820 -0
  93. package/src/kanban/kanban-bridge.ts +216 -0
  94. package/src/kanban/kanban-context.ts +43 -0
  95. package/src/kanban/kanban-events.ts +34 -0
  96. package/src/kanban/kanban-generate-title.ts +259 -0
  97. package/src/kanban/kanban-generate-topic.ts +170 -0
  98. package/src/kanban/kanban-operations.ts +170 -0
  99. package/src/kanban/kanban-server.ts +931 -0
  100. package/src/kanban/kanban-turn-handlers.ts +55 -0
  101. package/src/kanban/model-capture.ts +34 -0
  102. package/src/log.ts +47 -0
  103. package/src/phases/env-sync.ts +64 -0
  104. package/src/phases/execution-mode.ts +79 -0
  105. package/src/phases/implement-to-verify.ts +31 -0
  106. package/src/phases/phase-progression.ts +249 -0
  107. package/src/phases/phase-transitions.ts +226 -0
  108. package/src/phases/workflow-router.ts +98 -0
  109. package/src/prompts/finish-instructions.ts +143 -0
  110. package/src/prompts/skill-block-builder.ts +73 -0
  111. package/src/prompts/skill-expansion.ts +262 -0
  112. package/src/prompts/task-gate-skill.ts +83 -0
  113. package/src/prompts/template-engine.ts +433 -0
  114. package/src/prompts/text-blocks.ts +279 -0
  115. package/src/review/review-context.ts +56 -0
  116. package/src/review/review-counter.ts +66 -0
  117. package/src/review/review-empty-loop-tracking.ts +52 -0
  118. package/src/review/review-loops.ts +217 -0
  119. package/src/review/review-report.ts +101 -0
  120. package/src/settings/model-overrides.ts +269 -0
  121. package/src/settings/settings-schema.ts +458 -0
  122. package/src/settings/settings-types.ts +81 -0
  123. package/src/settings/settings-ui.ts +222 -0
  124. package/src/shared/errors.ts +24 -0
  125. package/src/shared/global.d.ts +22 -0
  126. package/src/shared/session-entries.ts +24 -0
  127. package/src/shared/types.ts +195 -0
  128. package/src/shared/workflow-refs.ts +253 -0
  129. package/src/shared/workflow-types.ts +180 -0
  130. package/src/snippets/vendored/subscribe-to-dialog-coordinator.ts +98 -0
  131. package/src/snippets/vendored/subscribe-to-notifications.ts +150 -0
  132. package/src/snippets/vendored/subscribe-to-parallel-work-guardrail.ts +35 -0
  133. package/src/snippets/vendored/subscribe-to-subagent.ts +67 -0
  134. package/src/snippets/vendored/subscribe-to-todo.ts +63 -0
  135. package/src/state/archive-artifacts.ts +602 -0
  136. package/src/state/artifact-junction.ts +462 -0
  137. package/src/state/artifact-paths.ts +230 -0
  138. package/src/state/feature-activation.ts +239 -0
  139. package/src/state/feature-management.ts +325 -0
  140. package/src/state/feature-record-store.ts +39 -0
  141. package/src/state/feature-session.ts +263 -0
  142. package/src/state/feature-state.ts +439 -0
  143. package/src/state/post-turn-dispatch.ts +106 -0
  144. package/src/state/session-lifecycle.ts +346 -0
  145. package/src/state/state-persistence.ts +122 -0
  146. package/src/state/worth-notes.ts +89 -0
  147. package/src/tools/add-to-backlog.ts +117 -0
  148. package/src/tools/phase-ready.ts +742 -0
  149. package/src/tools/task-ready-advance.ts +235 -0
  150. package/src/tools/text-result.ts +15 -0
  151. package/src/ui/featyard-widget.ts +250 -0
  152. package/src/ui/manage-features-dialog.ts +246 -0
  153. package/src/ui/select-dialog.ts +27 -0
@@ -0,0 +1,742 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3
+
4
+ /**
5
+ * Phase-ready module — phase_ready tool handler.
6
+ *
7
+ * Handles design → plan, plan → implement, verify → review, and finish → done transitions.
8
+ * Also handles the code-review loop (fy-review calls phase_ready with issuesFound).
9
+ * During the implement phase, phase_ready is blocked by the guardrails interceptor
10
+ * (the implement→verify transition is owned by task_ready_advance).
11
+ * Includes design/plan review loop logic, compact-triggered review iteration compacts,
12
+ * auto-agent integration, and kanban lane moves.
13
+ *
14
+ * All factory-coupled dependencies are injected via PhaseReadyDeps.
15
+ */
16
+
17
+ import { Type } from "@earendil-works/pi-ai";
18
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
19
+ import { Text } from "@earendil-works/pi-tui";
20
+ import { NO_COMPACT_CALLBACK, triggerContextCompact } from "../compaction/compact-trigger.js";
21
+ import { syncWorktreeStatus } from "../git/worktrees/worktree-helpers.js";
22
+ import { areAllTodosDone } from "../integrations/todo-integration.js";
23
+ import { log, NO_ERROR } from "../log.js";
24
+ import { syncEnvVarsFromState } from "../phases/env-sync.js";
25
+ import { isPhaseDone } from "../phases/phase-progression.js";
26
+ import { getLoopCountForPhase, transitionToUatPhase } from "../phases/phase-transitions.js";
27
+ import { toRouteConfig } from "../phases/workflow-router.js";
28
+ import { reviewSkillName, toDisplayLoopNumber } from "../review/review-context.js";
29
+ import { reviewLoopStateField, startReviewIteration } from "../review/review-counter.js";
30
+ import { NO_REVIEW_CONTEXT, resolveReviewLoopDecision } from "../review/review-loops.js";
31
+ import { generateReviewReport, getReportableReviewHistory } from "../review/review-report.js";
32
+ import { getSettings, resolveReviewSkill } from "../settings/settings-ui.js";
33
+ import { NO_AGENT_NAME, NO_FEATURE_STATE_OVERRIDE } from "../shared/workflow-refs.js";
34
+ import type { IGuardrails, IPhaseReady, WorkflowTransitionDeps } from "../shared/workflow-types.js";
35
+ import { withCoordinator } from "../snippets/vendored/subscribe-to-dialog-coordinator.js";
36
+ import { recoverArtifactsFromDisk } from "../state/feature-management.js";
37
+ import type { FeatureSession } from "../state/feature-session.js";
38
+ import {
39
+ DEFAULT_DIR,
40
+ type ExpandSkillCommandFn,
41
+ type FeatureState,
42
+ markFeatureDone,
43
+ recordReviewHistory,
44
+ saveFeatureState,
45
+ } from "../state/feature-state.js";
46
+ import { schedulePostTurnFollowUp } from "../state/post-turn-dispatch.js";
47
+ import { isSubagentSession, persistState } from "../state/state-persistence.js";
48
+ import { notifyFeatureCompleted, worthNotesPointerFor } from "../state/worth-notes.js";
49
+ import { NO_FEATURE_STATE, updateWidget } from "../ui/featyard-widget.js";
50
+ import { textResult } from "./text-result.js";
51
+
52
+ const MSG_NO_SLUG = "phase_ready failed — no active feature slug.";
53
+ const MSG_NO_STATE = "phase_ready failed — no feature state found.";
54
+ const MSG_NO_CALLBACK = "phase_ready failed — auto-agent callback lost.";
55
+
56
+ // Unified guard against multiple phase_ready calls within a single AGENT run
57
+ // (one user prompt → agent_start … agent_settled). A "turn" in pi is one LLM
58
+ // response; the agent frequently re-calls phase_ready across several turns within
59
+ // one agent run (Thinking between calls). turn_end fires between those calls and
60
+ // would reset a turn-scoped guard — so this guard is reset on agent_end instead
61
+ // (the per-cycle boundary: every low-level run, including retry/compact/followUp
62
+ // continuations, ends with its own agent_end).
63
+ //
64
+ // IMPORTANT: phase-transition followUps are NOT dispatched inline. An inline
65
+ // followUp would drain inside the same agent loop with NO agent_end between the
66
+ // dispatching turn and the followUp-driven turn (pi: "the agent loop drains both
67
+ // queues before emitting agent_end"), so this run's phase_ready would still hold
68
+ // the guard and the followUp skill's own phase_ready would be deduped. Such
69
+ // followUps are staged via schedulePostTurnFollowUp and drained (deferred) by
70
+ // the agent_settled handler. agent_settled fires only when pi has no pending
71
+ // continuation, and at that point session.isStreaming is false, so the drained
72
+ // followUp starts a FRESH agent run. agent_end therefore fires between every
73
+ // iteration and the guard resets correctly.
74
+ //
75
+ // Set ONLY when phase_ready is honored — gates passed AND a real action occurred
76
+ // (phase transition OR followUp staged). NOT on gate failures / no-ops / UI
77
+ // "Discuss". This lets the agent legitimately retry (e.g. verify after running
78
+ // tests) while collapsing any confused repeated call into a single transition.
79
+ let phaseReadyPassed = false;
80
+
81
+ /** @internal Reset the phase_ready dedup flag (test isolation under isolate:false). */
82
+ export function _resetPhaseReadyPassed(): void {
83
+ phaseReadyPassed = false;
84
+ }
85
+
86
+ export interface PhaseReadyDeps extends WorkflowTransitionDeps {
87
+ guardrails: IGuardrails;
88
+ /** Recover a failed `ctx.compact` — resumes the agent turn with the follow-up (mirrors the
89
+ * narrow `recoverCompactFailure` param the sibling tools inject, rather than the whole
90
+ * compaction module). */
91
+ recoverCompactFailure: () => void;
92
+ expandSkillCommand: ExpandSkillCommandFn;
93
+ applyExecutionMode: (ctx: ExtensionContext) => Promise<void>;
94
+ cleanupWorktreeOnFinish: (featureState: FeatureState, ctx: ExtensionContext) => Promise<void>;
95
+ getAutoAgentCallback: () => ReturnType<
96
+ typeof import("../kanban/auto-agent/auto-agent-state-machine.js").getAutoAgentCallback
97
+ >;
98
+ }
99
+
100
+ /**
101
+ * Build and return the full review follow-up message for a review iteration.
102
+ *
103
+ * Expands the review skill via `expandSkillCommand`. All PI_FY_* placeholders
104
+ * (REVIEW_LOOP_CONTEXT, REVIEW_METHOD, report file, etc) are
105
+ * resolved by the main substitution pipeline inside `expandSkillCommand` — this
106
+ * function does not perform any substitution or read feature state itself.
107
+ *
108
+ * @param opts.loopNumber - Review iteration number for the followUp message
109
+ * text. **0** for the first pass, **rawLoopCount + 1** for subsequent
110
+ * iterations (the post-increment counter value, computed by the caller).
111
+ */
112
+ function buildReviewFollowUp(
113
+ deps: PhaseReadyDeps,
114
+ opts: {
115
+ /** e.g. "fy-plan-review" or "fy-design-review" */
116
+ skillName: string;
117
+ /** e.g. "plan review" or "design review" — human-readable label for the followUp message */
118
+ label: string;
119
+ /** Loop number for the followUp message text — 0 for first iteration */
120
+ loopNumber: number;
121
+ },
122
+ ): string {
123
+ // Placeholders (REVIEW_LOOP_CONTEXT, REVIEW_METHOD, report file)
124
+ // are resolved by the main substitution pipeline inside expandSkillCommand.
125
+ return deps.expandSkillCommand(
126
+ `/skill:${opts.skillName} Run ${opts.label} iteration #${opts.loopNumber || 1}`,
127
+ NO_FEATURE_STATE_OVERRIDE,
128
+ NO_AGENT_NAME,
129
+ );
130
+ }
131
+
132
+ /**
133
+ * Apply review-iteration-reset compact logic.
134
+ * @returns true if compact was triggered (caller should early-return), false if caller should proceed normally.
135
+ */
136
+
137
+ /** Empty compact note — no extra preface before the skill expansion. */
138
+ const NO_COMPACT_MESSAGE = "";
139
+
140
+ /** Options that differ between plan and design review loops. */
141
+ interface ReviewLoopOpts {
142
+ /** Phase name for getLoopCountForPhase */
143
+ phaseName: "plan" | "design";
144
+ /** Skill name, e.g. "fy-plan-review" or "fy-design-review" */
145
+ skillName: string;
146
+ /** Human-readable label for messages, e.g. "plan review" or "design review" */
147
+ label: string;
148
+ /** Log label for triggerContextCompact */
149
+ logLabel: string;
150
+ }
151
+
152
+ type ReviewLoopResult = { kind: "first-iteration" } | { kind: "should-loop" } | { kind: "loops-done" };
153
+
154
+ /**
155
+ * Shared review loop logic for plan and design phases.
156
+ * Handles: first-iteration detection, loop counting, review history recording,
157
+ * and loop decision. Does NOT handle post-loop continuation (different per phase).
158
+ *
159
+ * @returns the loop result indicating what the caller should do next.
160
+ */
161
+ async function handleReviewLoop(
162
+ deps: PhaseReadyDeps,
163
+ ctx: ExtensionContext,
164
+ handler: FeatureSession,
165
+ slug: string,
166
+ issuesFound: number,
167
+ cannotFix: number,
168
+ falsePositives: number,
169
+ opts: ReviewLoopOpts,
170
+ ): Promise<ReviewLoopResult> {
171
+ const settings = getSettings();
172
+ const state = handler.getActiveFeatureState();
173
+ if (!state) return { kind: "loops-done" };
174
+
175
+ const loopCount = state[reviewLoopStateField(opts.phaseName)].reviewLoopCount ?? 0;
176
+
177
+ // First iteration — send initial review follow-up.
178
+ // Always sync env + widget: startReviewIteration just changed reviewLoopCount
179
+ // (0→1) and reviewActive. syncEnvVarsFromState re-syncs PI_FY_STAGE (fork-mode
180
+ // derivation). The shouldLoop branch below always syncs too; this keeps both
181
+ // branches consistent.
182
+ if (loopCount === 0) {
183
+ startReviewIteration(handler, slug, opts.phaseName, state);
184
+ syncEnvVarsFromState(handler);
185
+ updateWidget(handler, NO_FEATURE_STATE);
186
+ // Stage the review-skill followUp for delivery after agent_settled (not
187
+ // inline): an inline followUp would drain inside the same agent loop with no
188
+ // agent_end between runs, so this run's phase_ready would still hold the
189
+ // guard and the next run's phase_ready would be deduped. Staging defers
190
+ // delivery to the agent_settled handler (deferred), which starts a fresh run.
191
+ schedulePostTurnFollowUp(
192
+ buildReviewFollowUp(deps, {
193
+ skillName: opts.skillName,
194
+ label: opts.label,
195
+ loopNumber: 0,
196
+ }),
197
+ );
198
+ return { kind: "first-iteration" };
199
+ }
200
+
201
+ // Subsequent iterations — resolve loop decision
202
+ const rawLoopCount = getLoopCountForPhase(state, opts.phaseName);
203
+ const adjustedLoopCount = toDisplayLoopNumber(rawLoopCount);
204
+ const resolvedIssues = Math.max(0, issuesFound);
205
+ const resolvedFalsePositives = Math.max(0, falsePositives);
206
+ const resolvedCannotFix = Math.max(0, cannotFix);
207
+
208
+ const { shouldLoop } = resolveReviewLoopDecision(
209
+ { reviewLoops: settings.maxPlanReviewRounds, minReviewLoops: settings.minReviewLoops },
210
+ adjustedLoopCount,
211
+ resolvedIssues,
212
+ );
213
+
214
+ recordReviewHistory(state, {
215
+ phase: opts.phaseName,
216
+ loopNumber: adjustedLoopCount,
217
+ issuesFound: resolvedIssues,
218
+ falsePositives: resolvedFalsePositives,
219
+ cannotFixIssues: resolvedCannotFix,
220
+ });
221
+
222
+ startReviewIteration(handler, slug, opts.phaseName, state);
223
+
224
+ if (shouldLoop) {
225
+ syncEnvVarsFromState(handler);
226
+ updateWidget(handler, NO_FEATURE_STATE);
227
+ // Next iteration number = the post-increment counter (rawLoopCount + 1).
228
+ // adjustedLoopCount is 0-indexed "iterations completed" and is off by one
229
+ // for the message — using it would label iteration #2 as "#1".
230
+ const loopNumber = rawLoopCount + 1;
231
+ const substituted = buildReviewFollowUp(deps, {
232
+ skillName: opts.skillName,
233
+ label: opts.label,
234
+ loopNumber,
235
+ });
236
+ // Compact-handler owns skill + framing; pass skillName + the specific note only.
237
+ const reviewNote = `Run ${opts.label} iteration #${loopNumber || 1}`;
238
+
239
+ if (
240
+ await triggerContextCompact(
241
+ ctx,
242
+ {
243
+ settingValue: settings.reviewIterationCompact,
244
+ skillName: opts.skillName,
245
+ message: reviewNote,
246
+ logLabel: `${opts.logLabel} shouldLoop=true`,
247
+ },
248
+ NO_COMPACT_CALLBACK,
249
+ deps.recoverCompactFailure,
250
+ )
251
+ ) {
252
+ return { kind: "should-loop" };
253
+ }
254
+
255
+ schedulePostTurnFollowUp(substituted);
256
+ return { kind: "should-loop" };
257
+ }
258
+
259
+ return { kind: "loops-done" };
260
+ }
261
+
262
+ function completeDesignPhase(handler: FeatureSession, slug: string): void {
263
+ const featureState = handler.getActiveFeatureState();
264
+ if (featureState) {
265
+ handler.setActiveFeatureState(featureState);
266
+ recoverArtifactsFromDisk(handler);
267
+ } else {
268
+ log.warn(`phase_ready: no feature state found for '${slug}' — skipping artifact recovery`);
269
+ }
270
+ handler.completeCurrentWorkflowPhase(toRouteConfig(getSettings()));
271
+ }
272
+
273
+ export function registerPhaseReady(deps: PhaseReadyDeps): IPhaseReady {
274
+ const {
275
+ pi,
276
+ handler,
277
+ guardrails,
278
+ recoverCompactFailure,
279
+ expandSkillCommand,
280
+ applyModelOverrideForPhase,
281
+ handleReviewToUatTransition,
282
+ cleanupWorktreeOnFinish,
283
+ applyExecutionMode,
284
+ } = deps;
285
+
286
+ pi.registerTool({
287
+ name: "phase_ready",
288
+ label: "Phase Ready",
289
+ description:
290
+ "Workflow-stage tool for signaling phase completion. Only use when a skill prompt explicitly instructs it.",
291
+ parameters: Type.Object({
292
+ issuesFound: Type.Optional(
293
+ Type.Number({
294
+ description:
295
+ "Number of real issues found in this review iteration (fixed + cannot-fix, excluding false positives)",
296
+ }),
297
+ ),
298
+ cannotFix: Type.Optional(
299
+ Type.Number({
300
+ description:
301
+ "Number of issues marked cannot-fix in this review iteration (subset of issuesFound; sets the report warning level)",
302
+ }),
303
+ ),
304
+ falsePositives: Type.Optional(
305
+ Type.Number({
306
+ description:
307
+ "Number of findings dismissed as false-positive in this review iteration (excluded from issuesFound; surfaces in the review report)",
308
+ }),
309
+ ),
310
+ }),
311
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
312
+ const ws = handler.getWorkflowState();
313
+ if (!ws?.currentPhase) {
314
+ return textResult("phase_ready is not supported — no active workflow.");
315
+ }
316
+
317
+ const isDesign = ws.currentPhase === "design";
318
+ const isPlan = ws.currentPhase === "plan";
319
+ const isImplement = ws.currentPhase === "implement";
320
+ const isVerify = ws.currentPhase === "verify";
321
+ const isReview = ws.currentPhase === "review";
322
+ if (!isDesign && !isPlan && !isImplement && !isVerify && !isReview && ws.currentPhase !== "finish") {
323
+ return textResult(`phase_ready is not supported for ${ws.currentPhase}.`);
324
+ }
325
+
326
+ // phase_ready is blocked by the guardrails tool_call interceptor during the
327
+ // implement phase (it redirects to task_ready_advance). Defensive no-op if
328
+ // execute() is reached directly — never runs the implement→verify machinery
329
+ // (that transition is owned by task_ready_advance's last-task branch).
330
+ if (isImplement) {
331
+ return textResult("");
332
+ }
333
+
334
+ // Unified once-per-agent-turn guard: if phase_ready was already honored this
335
+ // agent turn (transition or follow-up dispatched), every subsequent call is a
336
+ // silent no-op. Prevents the agent from double-transitioning / dispatching N
337
+ // follow-ups when it re-calls phase_ready after seeing the `✓ done` result
338
+ // (which spans multiple LLM turns within one agent turn). Reset on agent_end.
339
+ if (phaseReadyPassed) {
340
+ return textResult("");
341
+ }
342
+
343
+ // --- Code review phase: drive the review loop from issuesFound/cannotFix ---
344
+ // The fy-review skill calls phase_ready({issuesFound, cannotFix}) at the
345
+ // end of each iteration; guardrails records history, tracks empty loops, and
346
+ // decides whether to loop again or transition to UAT/finish.
347
+ if (isReview) {
348
+ // Reaching here means first phase_ready this agent turn. The review loop
349
+ // always acts (records history + dispatches/transitions) — mark honored.
350
+ phaseReadyPassed = true;
351
+ await guardrails.completeCodeReviewLoop(
352
+ ctx,
353
+ params.issuesFound ?? 0,
354
+ params.cannotFix ?? 0,
355
+ params.falsePositives ?? 0,
356
+ );
357
+ return textResult("");
358
+ }
359
+
360
+ // --- Plan phase: review loop or execution handoff ---
361
+ if (isPlan) {
362
+ const settings = getSettings();
363
+ if (settings.maxPlanReviewRounds !== 0) {
364
+ const planSlug = handler.getActiveFeatureSlug();
365
+ if (!planSlug) {
366
+ return textResult(MSG_NO_SLUG);
367
+ }
368
+ const planState = handler.getActiveFeatureState();
369
+ if (!planState) {
370
+ return textResult(MSG_NO_STATE);
371
+ }
372
+ const loopResult = await handleReviewLoop(
373
+ deps,
374
+ ctx,
375
+ handler,
376
+ planSlug,
377
+ params.issuesFound ?? 0,
378
+ params.cannotFix ?? 0,
379
+ params.falsePositives ?? 0,
380
+ {
381
+ phaseName: "plan",
382
+ skillName: reviewSkillName("plan"),
383
+ label: "plan review",
384
+ logLabel: "plan",
385
+ },
386
+ );
387
+ if (loopResult.kind === "first-iteration" || loopResult.kind === "should-loop") {
388
+ // A review-loop iteration follow-up was dispatched — phase_ready honored.
389
+ phaseReadyPassed = true;
390
+ return textResult("");
391
+ }
392
+ // loops-done: apply execution mode (advance plan → implement + dispatch fy-implement).
393
+ // fy-implement seeds its todo list from the plan doc on start, so no separate
394
+ // handoff/coverage-verification message is needed here.
395
+ if (
396
+ await triggerContextCompact(
397
+ ctx,
398
+ {
399
+ settingValue: settings.reviewIterationCompact,
400
+ skillName: "fy-implement",
401
+ message: "Plan review complete. Continuing to implementation.",
402
+ logLabel: "plan shouldLoop=false",
403
+ },
404
+ NO_COMPACT_CALLBACK,
405
+ recoverCompactFailure,
406
+ )
407
+ ) {
408
+ // Compact initiated (re-injects fy-implement) — phase_ready honored.
409
+ phaseReadyPassed = true;
410
+ return textResult("");
411
+ }
412
+ await applyExecutionMode(ctx);
413
+ phaseReadyPassed = true;
414
+ return textResult("");
415
+ }
416
+ // plan review off: apply execution mode directly (advance plan → implement + dispatch fy-implement).
417
+ await applyExecutionMode(ctx);
418
+ phaseReadyPassed = true;
419
+ return textResult("");
420
+ }
421
+
422
+ // --- Verify phase ---
423
+ if (isVerify) {
424
+ const verifySlug = handler.getActiveFeatureSlug();
425
+ if (!verifySlug) {
426
+ return textResult(MSG_NO_SLUG);
427
+ }
428
+
429
+ if (!guardrails.isVerifyTestsPassed()) {
430
+ log.info("[workflow] phase_ready (verify): tests have not passed, staying in verify");
431
+ return textResult(
432
+ "Tests have not passed yet. Run the test suite and ensure it passes before calling phase_ready.",
433
+ );
434
+ }
435
+
436
+ if (!areAllTodosDone()) {
437
+ log.info("[workflow] phase_ready (verify): todos not all done, staying in verify");
438
+ return textResult("Not all todos are complete. Finish or cancel remaining items before calling phase_ready.");
439
+ }
440
+
441
+ const settings = getSettings();
442
+ const reviewSkill = resolveReviewSkill(settings);
443
+ // Gates passed (tests + todos) — phase_ready will be honored this call.
444
+ phaseReadyPassed = true;
445
+ if (reviewSkill) {
446
+ handler.completeCurrentWorkflowPhase(toRouteConfig(settings)); // verify → review
447
+ await applyModelOverrideForPhase(pi, ctx, "review");
448
+ persistState(pi, handler);
449
+ updateWidget(handler, NO_FEATURE_STATE);
450
+
451
+ schedulePostTurnFollowUp(
452
+ expandSkillCommand(`/skill:${reviewSkill}`, NO_FEATURE_STATE_OVERRIDE, NO_AGENT_NAME),
453
+ );
454
+ } else {
455
+ // No review skill — skip to review, then transition through it to UAT/finish
456
+ handler.setCurrentPhase("review");
457
+ await handleReviewToUatTransition(ctx, verifySlug, settings, NO_REVIEW_CONTEXT);
458
+ return textResult("");
459
+ }
460
+ return textResult("");
461
+ }
462
+
463
+ // --- Finish phase ---
464
+ if (ws.currentPhase === "finish") {
465
+ const finishSlug = handler.getActiveFeatureSlug();
466
+ if (!finishSlug) {
467
+ return textResult(MSG_NO_SLUG);
468
+ }
469
+
470
+ const featureState = handler.getActiveFeatureState();
471
+ const settings = getSettings();
472
+
473
+ if (
474
+ featureState &&
475
+ settings.uatMode === "after-finish" &&
476
+ !isPhaseDone(
477
+ { currentPhase: featureState.workflow.currentPhase, completedAt: featureState.completedAt },
478
+ "uat",
479
+ )
480
+ ) {
481
+ handler.completeCurrentWorkflowPhase(toRouteConfig(settings));
482
+ // finish → UAT transition — phase_ready honored.
483
+ phaseReadyPassed = true;
484
+ // Notify on the Finish→UAT transition: the review summary (regenerated from
485
+ // persisted reviewHistory) + worth-notes pointer, mirroring after-review so worth-notes
486
+ // are visible at UAT in every uatMode. Level mirrors the report: warning if any
487
+ // cannot-fix in history, else info.
488
+ const report = generateReviewReport(featureState);
489
+ const cannotFix = getReportableReviewHistory(featureState).some((e) => e.cannotFixIssues > 0);
490
+ const pointer = worthNotesPointerFor(finishSlug);
491
+ const notifyMessage = pointer ? `${report}\n\n${pointer}` : report;
492
+ await transitionToUatPhase(
493
+ ctx,
494
+ finishSlug,
495
+ featureState,
496
+ {
497
+ pi,
498
+ handler,
499
+ applyModelOverrideForPhase: deps.applyModelOverrideForPhase,
500
+ getAutoAgentCallback: deps.getAutoAgentCallback,
501
+ },
502
+ {
503
+ kanbanNote: "finish complete — after-finish UAT",
504
+ notifyMessage,
505
+ notifyLevel: cannotFix ? "warning" : "info",
506
+ },
507
+ );
508
+ return textResult("");
509
+ }
510
+
511
+ if (
512
+ featureState &&
513
+ !isPhaseDone(
514
+ { currentPhase: featureState.workflow.currentPhase, completedAt: featureState.completedAt },
515
+ "uat",
516
+ )
517
+ ) {
518
+ log.info("[workflow] phase_ready (finish): UAT not yet resolved, skipping markFeatureDone");
519
+ return textResult("UAT not yet resolved. Complete UAT first.");
520
+ }
521
+
522
+ handler.completeCurrentWorkflowPhase(toRouteConfig(settings));
523
+ // finish → done completion — phase_ready honored.
524
+ phaseReadyPassed = true;
525
+ if (featureState) {
526
+ await cleanupWorktreeOnFinish(featureState, ctx);
527
+ // Mark done (completedAt) and keep the slot active with the done state, so the
528
+ // widget renders a terminal DONE line until the next feature displaces it.
529
+ // (done = visible: do NOT clear the active feature / env on completion)
530
+ const doneState = markFeatureDone(featureState);
531
+ saveFeatureState(doneState, DEFAULT_DIR);
532
+ handler.setActiveFeatureState(doneState);
533
+ // Sync the worktree status icon for the done state (completion is the
534
+ // off-signal — clears the icon regardless of whether removal succeeded).
535
+ syncWorktreeStatus(doneState);
536
+ }
537
+ // Emit the completion notify with the worth-notes pointer merged in.
538
+ notifyFeatureCompleted(finishSlug);
539
+ updateWidget(handler, NO_FEATURE_STATE);
540
+
541
+ if (featureState?.featureId != null) {
542
+ try {
543
+ const { moveFeatureToLane } = await import("../kanban/data/kanban-move-feature.js");
544
+ await moveFeatureToLane({
545
+ featureId: featureState.featureId,
546
+ toLane: "done",
547
+ fromLane: undefined,
548
+ note: "feature complete",
549
+ });
550
+ } catch (err) {
551
+ log.warn(`phase_ready (finish): kanban lane move to done failed: ${err}`);
552
+ }
553
+ }
554
+
555
+ try {
556
+ const autoAgentCb = deps.getAutoAgentCallback();
557
+ if (autoAgentCb && finishSlug) {
558
+ log.info(`[workflow] auto-agent callback found, notifying feature completion for ${finishSlug}`);
559
+ autoAgentCb.onFeatureComplete(finishSlug);
560
+ }
561
+ } catch (err) {
562
+ log.error(`[workflow] auto-agent callback error: ${err}`, NO_ERROR);
563
+ }
564
+
565
+ return textResult("");
566
+ }
567
+
568
+ // --- Brainstorm phase ---
569
+ const autoAgentCb = deps.getAutoAgentCallback();
570
+ const isAutoMode = autoAgentCb?.isActive?.() === true;
571
+
572
+ const slug = handler.getActiveFeatureSlug();
573
+ if (!slug) {
574
+ return textResult(MSG_NO_SLUG);
575
+ }
576
+
577
+ const settings = getSettings();
578
+ if (settings.maxPlanReviewRounds !== 0) {
579
+ const designState = handler.getActiveFeatureState();
580
+ if (!designState) {
581
+ return textResult(MSG_NO_STATE);
582
+ }
583
+ const loopResult = await handleReviewLoop(
584
+ deps,
585
+ ctx,
586
+ handler,
587
+ slug,
588
+ params.issuesFound ?? 0,
589
+ params.cannotFix ?? 0,
590
+ params.falsePositives ?? 0,
591
+ {
592
+ phaseName: "design",
593
+ skillName: reviewSkillName("design"),
594
+ label: "design review",
595
+ logLabel: "design",
596
+ },
597
+ );
598
+ if (loopResult.kind === "first-iteration" || loopResult.kind === "should-loop") {
599
+ // A design review-loop iteration follow-up was dispatched — phase_ready honored.
600
+ phaseReadyPassed = true;
601
+ return textResult("");
602
+ }
603
+ // loops-done: fall through to design completion
604
+ }
605
+ // maxPlanReviewRounds is off OR review loops finished
606
+ if (!isAutoMode) {
607
+ const guard = globalThis.__piCtx;
608
+ const ui = guard?.ui;
609
+ if (isSubagentSession() || !ui?.select) {
610
+ return textResult("");
611
+ }
612
+ const { withAttention, getLastMessage } = await import("../snippets/vendored/subscribe-to-notifications.js");
613
+ const detail = ["design review", slug, getLastMessage()].filter(Boolean).join(" • ");
614
+ const choice = await withAttention("workflow", detail, () =>
615
+ withCoordinator(() =>
616
+ ui.select("Design phase complete. What would you like to do?", ["Proceed with implementation", "Discuss"]),
617
+ ),
618
+ );
619
+ if (choice === undefined || choice === "Discuss") {
620
+ return textResult("");
621
+ }
622
+ }
623
+
624
+ try {
625
+ // Design will advance (design → plan, or auto-agent handoff) — phase_ready honored.
626
+ // Set after the UI "Discuss" gate so a Discuss no-op does not consume the guard.
627
+ phaseReadyPassed = true;
628
+ completeDesignPhase(handler, slug);
629
+
630
+ const featureState = handler.getActiveFeatureState();
631
+ if (featureState?.featureId !== null && featureState) {
632
+ try {
633
+ const { getDatabaseInstance } = await import("../kanban/kanban-bridge.js");
634
+ const kanbanDb = getDatabaseInstance();
635
+ if (kanbanDb && featureState.featureId !== null) {
636
+ const kanbanSettings = getSettings();
637
+ const targetLane = kanbanSettings.designApprovalEnabled ? "design-approval" : "ready";
638
+ kanbanDb.moveFeature({
639
+ featureId: featureState.featureId,
640
+ toLane: targetLane,
641
+ changedBy: "system",
642
+ note: "design complete",
643
+ });
644
+ kanbanDb.unlockFeature(featureState.featureId);
645
+ log.info(`phase_ready: moved feature ${featureState.featureId} to ${targetLane}`);
646
+ }
647
+ } catch (err) {
648
+ log.warn(`phase_ready: kanban lane move failed: ${err}`);
649
+ }
650
+ }
651
+
652
+ if (isAutoMode) {
653
+ persistState(pi, handler);
654
+ updateWidget(handler, NO_FEATURE_STATE);
655
+ if (!autoAgentCb) {
656
+ return textResult(MSG_NO_CALLBACK);
657
+ }
658
+ if (settings.maxPlanReviewRounds !== 0) {
659
+ if (
660
+ await triggerContextCompact(
661
+ ctx,
662
+ {
663
+ settingValue: settings.reviewIterationCompact,
664
+ message: NO_COMPACT_MESSAGE,
665
+ logLabel: "design shouldLoop=false auto",
666
+ },
667
+ () => autoAgentCb.onFeatureComplete(slug),
668
+ recoverCompactFailure,
669
+ )
670
+ ) {
671
+ return textResult("");
672
+ }
673
+ }
674
+ autoAgentCb.onFeatureComplete(slug);
675
+ } else {
676
+ handler.setCurrentPhase("plan");
677
+ await applyModelOverrideForPhase(pi, ctx, "plan");
678
+ persistState(pi, handler);
679
+ updateWidget(handler, NO_FEATURE_STATE);
680
+ if (settings.maxPlanReviewRounds !== 0) {
681
+ if (
682
+ await triggerContextCompact(
683
+ ctx,
684
+ {
685
+ settingValue: settings.reviewIterationCompact,
686
+ skillName: "fy-plan",
687
+ message: NO_COMPACT_MESSAGE,
688
+ logLabel: "design shouldLoop=false non-auto",
689
+ },
690
+ NO_COMPACT_CALLBACK,
691
+ recoverCompactFailure,
692
+ )
693
+ ) {
694
+ return textResult("");
695
+ }
696
+ }
697
+ schedulePostTurnFollowUp(expandSkillCommand("/skill:fy-plan", NO_FEATURE_STATE_OVERRIDE, NO_AGENT_NAME));
698
+ }
699
+
700
+ return textResult("");
701
+ } catch (err) {
702
+ log.error(`phase_ready failed: ${err instanceof Error ? err.message : err}`, NO_ERROR);
703
+ return textResult(`phase_ready failed: ${err instanceof Error ? err.message : String(err)}`);
704
+ }
705
+ },
706
+ renderCall(args, theme) {
707
+ let text = theme.fg("toolTitle", theme.bold("phase_ready"));
708
+ if (args.issuesFound !== undefined) text += ` ${theme.fg("accent", `${args.issuesFound} issues`)}`;
709
+ return new Text(text, 0, 0);
710
+ },
711
+ renderResult(result, _options, theme) {
712
+ let text = "";
713
+ for (let i = result.content.length - 1; i >= 0; i--) {
714
+ const c = result.content[i];
715
+ if (c?.type === "text") {
716
+ text = c.text;
717
+ break;
718
+ }
719
+ }
720
+ const isError = text !== "";
721
+ const indicator = isError ? theme.fg("error", "✗ ") : theme.fg("success", "✓ ");
722
+ const truncated = text.length > 80 ? `${text.slice(0, 80)}…` : text;
723
+ return new Text(indicator + theme.fg("muted", truncated || "done"), 0, 0);
724
+ },
725
+ });
726
+
727
+ // The once-per-agent-run guard (phaseReadyPassed) is reset from the
728
+ // agent_end handler (auto-agent-events.ts) — the per-cycle boundary. The staged
729
+ // phase-transition followUp is drained (deferred) by the agent_settled handler
730
+ // (post-turn-dispatch.ts), which starts a fresh agent run. So agent_end fires
731
+ // between every run, the guard is cleared, and the next run's phase_ready (e.g.
732
+ // the fy-review skill ending its iteration) processes instead of being deduped.
733
+ // NOTE: intentionally NOT reset on turn_end — a pi "turn" is one LLM response,
734
+ // and a confused model's repeated phase_ready calls span multiple turns within
735
+ // one agent turn (those repeats must stay collapsed until the turn truly ends).
736
+
737
+ return {
738
+ resetTracking() {
739
+ phaseReadyPassed = false;
740
+ },
741
+ };
742
+ }