taskplane 0.28.5 → 0.28.7
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/dashboard/public/app.js
CHANGED
|
@@ -616,7 +616,13 @@ function renderSummary(batch) {
|
|
|
616
616
|
const isCurrent = i === waveIdx && (batch.phase === "executing" || batch.phase === "merging");
|
|
617
617
|
const isMergingChip = i === waveIdx && batch.phase === "merging";
|
|
618
618
|
const cls = isDone ? "done" : isMergingChip ? "current merging" : isCurrent ? "current" : "";
|
|
619
|
-
|
|
619
|
+
// #484: Show lane parallelization within each wave. Group taskIds by
|
|
620
|
+
// their assigned lane: tasks on the same lane render with `→` (serial),
|
|
621
|
+
// tasks on different lanes render with ` | ` (parallel). Tooltip shows
|
|
622
|
+
// the expanded lane breakdown.
|
|
623
|
+
const { compact, tooltip } = formatWaveLaneBreakdown(taskIds, batch.lanes || [], i + 1);
|
|
624
|
+
const titleAttr = tooltip ? ` title="${escapeHtml(tooltip)}"` : "";
|
|
625
|
+
wavesHtml += `<span class="wave-chip ${cls}"${titleAttr}>W${i + 1} [${compact}]</span>`;
|
|
620
626
|
});
|
|
621
627
|
$summaryWaves.innerHTML = wavesHtml;
|
|
622
628
|
} else {
|
|
@@ -624,6 +630,84 @@ function renderSummary(batch) {
|
|
|
624
630
|
}
|
|
625
631
|
}
|
|
626
632
|
|
|
633
|
+
/**
|
|
634
|
+
* Compute lane parallelization for a wave's tasks (#484).
|
|
635
|
+
*
|
|
636
|
+
* Returns:
|
|
637
|
+
* - `compact`: a string like "TP-165 → TP-166 | TP-168 | TP-167" suitable
|
|
638
|
+
* for the wave-chip body. Tasks on the same lane are joined by ` → `
|
|
639
|
+
* (in lane execution order, not just appearance order); separate lanes
|
|
640
|
+
* are joined by ` | `.
|
|
641
|
+
* - `tooltip`: a multi-line string like
|
|
642
|
+
* "W1\n L1: TP-165 → TP-166\n L2: TP-168\n L3: TP-167" suitable for
|
|
643
|
+
* the chip's `title` attribute. Empty string when no lane data is
|
|
644
|
+
* available (future waves not yet provisioned).
|
|
645
|
+
*
|
|
646
|
+
* When lane data is missing for one or more tasks (e.g., the wave has not
|
|
647
|
+
* yet been provisioned, or task hasn't been assigned), unassigned tasks
|
|
648
|
+
* are shown with the previous flat formatting and no tooltip is generated
|
|
649
|
+
* — this preserves backward compatibility with future-wave display.
|
|
650
|
+
*/
|
|
651
|
+
function formatWaveLaneBreakdown(taskIds, lanes, waveNumber) {
|
|
652
|
+
if (!Array.isArray(taskIds) || taskIds.length === 0) {
|
|
653
|
+
return { compact: "", tooltip: "" };
|
|
654
|
+
}
|
|
655
|
+
// Build taskId → laneNumber map for the lanes that have any of these tasks.
|
|
656
|
+
const taskToLane = new Map();
|
|
657
|
+
for (const lane of lanes) {
|
|
658
|
+
if (!lane || !Array.isArray(lane.taskIds)) continue;
|
|
659
|
+
for (const tid of lane.taskIds) {
|
|
660
|
+
// First lane to claim a task wins (lanes shouldn't overlap, but be defensive).
|
|
661
|
+
if (!taskToLane.has(tid)) taskToLane.set(tid, lane.laneNumber);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// If no task in this wave has lane data, fall back to flat display.
|
|
665
|
+
const hasAnyLaneData = taskIds.some((t) => taskToLane.has(t));
|
|
666
|
+
if (!hasAnyLaneData) {
|
|
667
|
+
return { compact: taskIds.join(", "), tooltip: "" };
|
|
668
|
+
}
|
|
669
|
+
// Group taskIds by lane. Preserve appearance order across lanes.
|
|
670
|
+
// Within a lane, preserve the lane's own taskIds order so `→` reflects
|
|
671
|
+
// execution order, not the order taskIds happens to appear here.
|
|
672
|
+
const laneOrder = []; // lane numbers in order they first appear in taskIds
|
|
673
|
+
const laneToTasks = new Map(); // laneNumber → ordered taskIds for this wave
|
|
674
|
+
const unassigned = []; // taskIds not in any lane (shouldn't happen but be defensive)
|
|
675
|
+
for (const tid of taskIds) {
|
|
676
|
+
const ln = taskToLane.get(tid);
|
|
677
|
+
if (ln === undefined) {
|
|
678
|
+
unassigned.push(tid);
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (!laneToTasks.has(ln)) {
|
|
682
|
+
laneToTasks.set(ln, []);
|
|
683
|
+
laneOrder.push(ln);
|
|
684
|
+
}
|
|
685
|
+
laneToTasks.get(ln).push(tid);
|
|
686
|
+
}
|
|
687
|
+
// Sort each lane's tasks by their position in the lane.taskIds array
|
|
688
|
+
// so `→` always reflects execution order on that lane.
|
|
689
|
+
for (const ln of laneOrder) {
|
|
690
|
+
const lane = lanes.find((l) => l && l.laneNumber === ln);
|
|
691
|
+
if (lane && Array.isArray(lane.taskIds)) {
|
|
692
|
+
const orderIndex = new Map(lane.taskIds.map((t, idx) => [t, idx]));
|
|
693
|
+
laneToTasks.get(ln).sort((a, b) => (orderIndex.get(a) ?? 0) - (orderIndex.get(b) ?? 0));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// Build compact representation.
|
|
697
|
+
const laneSegments = laneOrder.map((ln) => laneToTasks.get(ln).join(" → "));
|
|
698
|
+
if (unassigned.length > 0) laneSegments.push(unassigned.join(", "));
|
|
699
|
+
const compact = laneSegments.join(" | ");
|
|
700
|
+
// Build tooltip representation.
|
|
701
|
+
const tooltipLines = [`W${waveNumber}`];
|
|
702
|
+
for (const ln of laneOrder) {
|
|
703
|
+
tooltipLines.push(` L${ln}: ${laneToTasks.get(ln).join(" → ")}`);
|
|
704
|
+
}
|
|
705
|
+
if (unassigned.length > 0) {
|
|
706
|
+
tooltipLines.push(` (unassigned): ${unassigned.join(", ")}`);
|
|
707
|
+
}
|
|
708
|
+
return { compact, tooltip: tooltipLines.join("\n") };
|
|
709
|
+
}
|
|
710
|
+
|
|
627
711
|
// ─── Render: Lanes + Tasks (integrated) ─────────────────────────────────────
|
|
628
712
|
|
|
629
713
|
function renderLanesTasks(batch, sessions) {
|
|
@@ -858,11 +942,16 @@ function renderLanesTasks(batch, sessions) {
|
|
|
858
942
|
? `<button class="viewer-eye-btn${isViewingStatus ? ' active' : ''}" onclick="viewStatusMd('${escapeHtml(task.taskId)}')" title="View STATUS.md">👁</button>`
|
|
859
943
|
: '';
|
|
860
944
|
|
|
945
|
+
// #485: Show task title (from PROMPT.md `# Task: <ID> - <title>`) under
|
|
946
|
+
// the task-id when available. Falls back to just the ID when missing.
|
|
947
|
+
const titleHtml = task.taskTitle
|
|
948
|
+
? `<div class="task-title-subtitle">${escapeHtml(task.taskTitle)}</div>`
|
|
949
|
+
: "";
|
|
861
950
|
html += `
|
|
862
951
|
<div class="task-row">
|
|
863
952
|
<span class="task-icon"><span class="status-dot ${task.status}"></span></span>
|
|
864
953
|
<span class="task-actions">${eyeHtml}</span>
|
|
865
|
-
<span class="task-id status-${task.status}">${escapeHtml(task.taskId)}${showRepos ? repoBadgeHtml(tRepo, "repo-badge-task") : ""}</span>
|
|
954
|
+
<span class="task-id status-${task.status}"><div class="task-id-line">${escapeHtml(task.taskId)}${showRepos ? repoBadgeHtml(tRepo, "repo-badge-task") : ""}</div>${titleHtml}</span>
|
|
866
955
|
<span><span class="status-badge status-${task.status}"><span class="status-dot ${task.status}"></span> ${task.status}</span></span>
|
|
867
956
|
<span class="task-duration">${dur}</span>
|
|
868
957
|
<span>${progressHtml}</span>
|
|
@@ -625,6 +625,28 @@ body {
|
|
|
625
625
|
font-family: var(--font-mono);
|
|
626
626
|
font-weight: 600;
|
|
627
627
|
font-size: 0.85rem;
|
|
628
|
+
/* #485: task-id may now contain a stacked task-id-line + task-title-subtitle */
|
|
629
|
+
display: flex;
|
|
630
|
+
flex-direction: column;
|
|
631
|
+
gap: 2px;
|
|
632
|
+
min-width: 0; /* allow inner ellipsis to work */
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.task-id-line {
|
|
636
|
+
/* The original TP-XXX line; preserves the previous single-line look */
|
|
637
|
+
white-space: nowrap;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.task-title-subtitle {
|
|
641
|
+
/* #485: human-readable task title under the ID */
|
|
642
|
+
font-family: var(--font-sans, inherit);
|
|
643
|
+
font-weight: 400;
|
|
644
|
+
font-size: 0.72rem;
|
|
645
|
+
color: var(--text-muted);
|
|
646
|
+
white-space: nowrap;
|
|
647
|
+
overflow: hidden;
|
|
648
|
+
text-overflow: ellipsis;
|
|
649
|
+
max-width: 100%;
|
|
628
650
|
}
|
|
629
651
|
|
|
630
652
|
.task-duration {
|
package/dashboard/server.cjs
CHANGED
|
@@ -160,6 +160,52 @@ function resolveTaskFolder(task, state) {
|
|
|
160
160
|
return task.taskFolder;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Read the task title from a task folder's PROMPT.md.
|
|
165
|
+
*
|
|
166
|
+
* Extracts the human-readable title from the first `# Task:` heading. The
|
|
167
|
+
* heading format is `# Task: <ID> - <title>` per the create-taskplane-task
|
|
168
|
+
* skill's prompt template. Returns null when PROMPT.md is missing, the
|
|
169
|
+
* pattern doesn't match, or any read error occurs.
|
|
170
|
+
*
|
|
171
|
+
* Cached per-folder for the lifetime of this server process: PROMPT.md is
|
|
172
|
+
* immutable above the `---` divider so the title never changes mid-batch.
|
|
173
|
+
* Cache is keyed by absolute task folder path. (#485)
|
|
174
|
+
*/
|
|
175
|
+
const _taskTitleCache = new Map();
|
|
176
|
+
function parseTaskTitle(taskFolder) {
|
|
177
|
+
if (!taskFolder) return null;
|
|
178
|
+
const cacheKey = path.resolve(taskFolder);
|
|
179
|
+
if (_taskTitleCache.has(cacheKey)) return _taskTitleCache.get(cacheKey);
|
|
180
|
+
|
|
181
|
+
// Look at both the canonical folder and the archive fallback (matches
|
|
182
|
+
// parseStatusMd's two-candidate strategy).
|
|
183
|
+
const candidates = [taskFolder];
|
|
184
|
+
const taskId = path.basename(taskFolder);
|
|
185
|
+
const archiveBase = taskFolder.replace(/[/\\]tasks[/\\][^/\\]+$/, "/tasks/archive/" + taskId);
|
|
186
|
+
if (archiveBase !== taskFolder) candidates.push(archiveBase);
|
|
187
|
+
|
|
188
|
+
for (const folder of candidates) {
|
|
189
|
+
const promptPath = path.join(folder, "PROMPT.md");
|
|
190
|
+
try {
|
|
191
|
+
const content = fs.readFileSync(promptPath, "utf-8");
|
|
192
|
+
// Match `# Task: <ID> - <title>` (the canonical first-line heading).
|
|
193
|
+
const match = content.match(/^# Task:\s*\S+\s*[-\u2014\u2013]\s*(.+?)\s*$/m);
|
|
194
|
+
if (match) {
|
|
195
|
+
const title = match[1].trim();
|
|
196
|
+
_taskTitleCache.set(cacheKey, title);
|
|
197
|
+
return title;
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// PROMPT.md missing or unreadable in this candidate — try the next.
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Cache the negative result too, so we don't re-attempt on every poll.
|
|
205
|
+
_taskTitleCache.set(cacheKey, null);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
163
209
|
function parseStatusMd(taskFolder) {
|
|
164
210
|
const candidates = [taskFolder];
|
|
165
211
|
const taskId = path.basename(taskFolder);
|
|
@@ -1102,13 +1148,17 @@ function buildDashboardState() {
|
|
|
1102
1148
|
const tasks = (state.tasks || []).map((task) => {
|
|
1103
1149
|
const effectiveFolder = resolveTaskFolder(task, state);
|
|
1104
1150
|
let statusData = null;
|
|
1151
|
+
let taskTitle = null;
|
|
1105
1152
|
if (effectiveFolder) {
|
|
1106
1153
|
statusData = parseStatusMd(effectiveFolder);
|
|
1154
|
+
// #485: read the human-readable title from PROMPT.md once, surface
|
|
1155
|
+
// alongside taskId so the dashboard can show it as a subtitle.
|
|
1156
|
+
taskTitle = parseTaskTitle(effectiveFolder);
|
|
1107
1157
|
}
|
|
1108
1158
|
if (!task.doneFileFound && effectiveFolder) {
|
|
1109
1159
|
task.doneFileFound = checkDoneFile(effectiveFolder);
|
|
1110
1160
|
}
|
|
1111
|
-
return { ...task, statusData };
|
|
1161
|
+
return { ...task, statusData, taskTitle };
|
|
1112
1162
|
});
|
|
1113
1163
|
|
|
1114
1164
|
// TP-107: Load Runtime V2 data when available
|
|
@@ -132,6 +132,59 @@ function writeSegmentExpansionRequest(request: SegmentExpansionRequest): string
|
|
|
132
132
|
return finalPath;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* TP-186 — Death-spiral guard helper for `review_step`.
|
|
137
|
+
*
|
|
138
|
+
* Inspects STATUS.md to determine whether the worker has prematurely set the
|
|
139
|
+
* given step's section heading to `**Status:** ✅ Complete`. The guard fires
|
|
140
|
+
* for `code` and `test` review types only — plan reviews fire BEFORE
|
|
141
|
+
* implementation, when an empty STATUS is correct.
|
|
142
|
+
*
|
|
143
|
+
* Returns `true` ONLY if the step's section explicitly carries the
|
|
144
|
+
* `**Status:** ✅ Complete` line. The top-of-file (task-level) `**Status:**`
|
|
145
|
+
* field does not trip this guard because it is not inside any `### Step N:`
|
|
146
|
+
* section. All-checkboxes-checked is also NOT a trigger — it is the normal
|
|
147
|
+
* pre-code-review state.
|
|
148
|
+
*
|
|
149
|
+
* Designed to fail-open: any I/O error or a missing step heading returns
|
|
150
|
+
* `false` (the review proceeds). The prompt-side Recovery Recipe is the
|
|
151
|
+
* primary defense; this guard is a hard backstop, not a gatekeeper.
|
|
152
|
+
*
|
|
153
|
+
* @param statusPath absolute path to the worker's STATUS.md
|
|
154
|
+
* @param stepNum the step number being reviewed
|
|
155
|
+
* @returns true iff the step is marked Complete in STATUS.md
|
|
156
|
+
*/
|
|
157
|
+
export function isStepMarkedComplete(statusPath: string, stepNum: number): boolean {
|
|
158
|
+
let content: string;
|
|
159
|
+
try {
|
|
160
|
+
content = readFileSync(statusPath, "utf-8");
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const lines = content.split(/\r?\n/);
|
|
166
|
+
const stepHeadingRe = new RegExp(`^###\\s+Step\\s+${stepNum}\\b`);
|
|
167
|
+
const nextStepHeadingRe = /^###\s+Step\s+\d+\b/;
|
|
168
|
+
|
|
169
|
+
let inSection = false;
|
|
170
|
+
for (const line of lines) {
|
|
171
|
+
if (!inSection) {
|
|
172
|
+
if (stepHeadingRe.test(line)) inSection = true;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
// Stop scanning at the next step heading.
|
|
176
|
+
if (nextStepHeadingRe.test(line)) break;
|
|
177
|
+
// Match a literal status line within this step's section.
|
|
178
|
+
// Examples that should match:
|
|
179
|
+
// **Status:** ✅ Complete
|
|
180
|
+
// **Status:** ✅ Complete (note ...)
|
|
181
|
+
if (/^\s*\*\*Status:\*\*\s*✅\s*Complete\b/.test(line)) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
135
188
|
export default function (pi: ExtensionAPI) {
|
|
136
189
|
pi.registerTool({
|
|
137
190
|
name: "notify_supervisor",
|
|
@@ -632,6 +685,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
632
685
|
const reviewsDir = process.env.TASKPLANE_REVIEWS_DIR || join(taskFolder, ".reviews");
|
|
633
686
|
if (!existsSync(reviewsDir)) mkdirSync(reviewsDir, { recursive: true });
|
|
634
687
|
|
|
688
|
+
// ── TP-186 death-spiral guard ─────────────────────────────────
|
|
689
|
+
// Refuse to spawn a code/test reviewer on a step that is already
|
|
690
|
+
// marked `**Status:** ✅ Complete` in STATUS.md. The worker has
|
|
691
|
+
// violated the Order of Operations contract; the only safe path
|
|
692
|
+
// is to revert STATUS first, then re-call review_step. Plan
|
|
693
|
+
// reviews are exempt because they fire BEFORE implementation.
|
|
694
|
+
if (reviewType !== "plan" && isStepMarkedComplete(statusPath, stepNum)) {
|
|
695
|
+
const taskIdMatch = statusPath.match(/[\\/]([A-Z]{2,}-\d+)[^\\/]*[\\/]STATUS\.md$/);
|
|
696
|
+
const taskId = taskIdMatch ? taskIdMatch[1] : "<TASK-ID>";
|
|
697
|
+
const refusal = [
|
|
698
|
+
`REFUSED: Step ${stepNum} is already marked \`**Status:** ✅ Complete\` in STATUS.md.`,
|
|
699
|
+
`Per the Order of Operations rule, code review must run BEFORE you mark a step Complete.`,
|
|
700
|
+
`Follow the Recovery Recipe in the worker prompt:`,
|
|
701
|
+
` 1. Revert the step's Status to \`🟨 In Progress\` in STATUS.md`,
|
|
702
|
+
` 2. Commit: chore(${taskId}): revert premature step-${stepNum} completion`,
|
|
703
|
+
` 3. Re-call review_step(step=${stepNum}, type="${reviewType}", baseline=<sha>)`,
|
|
704
|
+
].join("\n");
|
|
705
|
+
return { content: [{ type: "text" as const, text: refusal }], details: undefined };
|
|
706
|
+
}
|
|
707
|
+
|
|
635
708
|
// Read review counter from STATUS.md
|
|
636
709
|
let reviewCounter = 0;
|
|
637
710
|
try {
|
package/package.json
CHANGED
|
@@ -278,9 +278,79 @@ code is already written.
|
|
|
278
278
|
6. Commit implementation
|
|
279
279
|
7. Call `review_step(step=N, type="code")` — AFTER implementation
|
|
280
280
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
281
|
+
### ⚠️ MANDATORY: Order of Operations for steps with code review
|
|
282
|
+
|
|
283
|
+
**For any step that requires a code review (Review Level ≥ 2), the following
|
|
284
|
+
order is MANDATORY. Workers MUST NOT mark a step `Status: ✅ Complete` in
|
|
285
|
+
STATUS.md before the code review for that step has returned APPROVE.**
|
|
286
|
+
|
|
287
|
+
1. **Implement** the step's checkbox items (write code, edit docs, etc.) —
|
|
288
|
+
check each box `[x]` in STATUS.md as you finish that item, but leave the
|
|
289
|
+
step's `**Status:**` heading set to `🟨 In Progress`.
|
|
290
|
+
2. **Commit** the implementation:
|
|
291
|
+
`git add -A && git commit -m "feat(TASK-ID): step N implementation"`
|
|
292
|
+
3. **Call** `review_step(step=N, type="code", baseline=<sha>)`.
|
|
293
|
+
4. If the verdict is **REVISE**: read the review file in `.reviews/`, apply
|
|
294
|
+
the fixes, commit them, and call `review_step` again. Repeat until APPROVE
|
|
295
|
+
(max 2 code review cycles per step).
|
|
296
|
+
5. If the verdict is **APPROVE**: NOW update the step's `**Status:**` heading
|
|
297
|
+
to `✅ Complete` in STATUS.md and commit the status update.
|
|
298
|
+
6. **Move to step N+1.**
|
|
299
|
+
|
|
300
|
+
The key invariant: **`Status: ✅ Complete` is the worker's commitment that the
|
|
301
|
+
reviewer has signed off on the step.** It is not an in-progress marker. Setting
|
|
302
|
+
it before APPROVE creates a contradiction the worker cannot recover from on
|
|
303
|
+
its own — STATUS says done while the reviewer says revise.
|
|
304
|
+
|
|
305
|
+
Individual checkboxes (`- [x] item text`) inside the step MAY be checked while
|
|
306
|
+
implementation is in flight — they record per-item progress. The **step-level
|
|
307
|
+
`Status:` heading** (the line that reads `**Status:** ✅ Complete` in STATUS.md)
|
|
308
|
+
is the only field governed by this rule.
|
|
309
|
+
|
|
310
|
+
### Recovery: "I marked the step Complete, then the reviewer returned REVISE"
|
|
311
|
+
|
|
312
|
+
If you violated the Order of Operations and set `**Status:** ✅ Complete` for
|
|
313
|
+
a step before the code review returned APPROVE, **you can recover without
|
|
314
|
+
operator intervention**. Follow this recipe exactly:
|
|
315
|
+
|
|
316
|
+
1. **Revert STATUS.md** for the affected step:
|
|
317
|
+
- Change the step's `**Status:** ✅ Complete` heading back to
|
|
318
|
+
`**Status:** 🟨 In Progress`.
|
|
319
|
+
- Leave the individual `- [x]` checkboxes alone — they record real work
|
|
320
|
+
that was done.
|
|
321
|
+
- If the top-of-file `**Current Step:**` field was advanced past this
|
|
322
|
+
step, set it back to this step's name.
|
|
323
|
+
2. **Commit** the revert with a dedicated message:
|
|
324
|
+
`git commit -am "chore(TASK-ID): revert premature step-N completion"`
|
|
325
|
+
3. **Handle the REVISE through the normal recipe:** read the review file in
|
|
326
|
+
`.reviews/`, add Issues-Found items as new checkboxes inside the step
|
|
327
|
+
(using the standard "After a REVISE Review" flow above), commit those
|
|
328
|
+
hydration changes, fix the issues, commit the fixes, then call
|
|
329
|
+
`review_step(step=N, type="code")` again.
|
|
330
|
+
4. Once the reviewer returns APPROVE, follow Order of Operations step 5 and
|
|
331
|
+
set `**Status:** ✅ Complete` for real.
|
|
332
|
+
|
|
333
|
+
Do NOT skip step 1. Leaving STATUS in the contradictory state (`Complete` +
|
|
334
|
+
an open REVISE) is the failure mode this recipe exists to undo. The engine's
|
|
335
|
+
`review_step` tool now refuses to run on a step already marked Complete and
|
|
336
|
+
will return a `REFUSED` verdict pointing back at this recipe.
|
|
337
|
+
|
|
338
|
+
### ❌ FORBIDDEN sequences (these break the review contract)
|
|
339
|
+
|
|
340
|
+
Workers MUST NOT do any of the following:
|
|
341
|
+
|
|
342
|
+
1. ~~Mark a step `**Status:** ✅ Complete` before its code review (Level ≥ 2)
|
|
343
|
+
has returned APPROVE.~~ This is the **death-spiral anti-pattern**: if
|
|
344
|
+
the reviewer subsequently returns REVISE, the worker enters a state
|
|
345
|
+
contradiction it cannot resolve and the lane is lost. If you did this
|
|
346
|
+
accidentally, follow the Recovery Recipe above.
|
|
347
|
+
2. ~~Hydrate, implement, check off, commit, THEN call plan review~~ — this
|
|
348
|
+
makes plan review pointless; the work is already written.
|
|
349
|
+
3. ~~Skip the code review and proceed to the next step on a Review Level ≥ 2
|
|
350
|
+
task~~ — the merge agent will reject the lane.
|
|
351
|
+
|
|
352
|
+
These rules sit alongside the existing "NEVER add, remove, or renumber steps"
|
|
353
|
+
rule from STATUS.md Hydration → Rules.
|
|
284
354
|
|
|
285
355
|
**Handling verdicts:**
|
|
286
356
|
- **APPROVE** → proceed (to implementation after plan review; to next step after code review)
|
|
@@ -288,6 +358,12 @@ code is already written.
|
|
|
288
358
|
- **REVISE** → read the review file in `.reviews/` for detailed feedback,
|
|
289
359
|
address the issues, commit fixes, then **call `review_step` again** for re-review.
|
|
290
360
|
The same reviewer evaluates whether your fixes address its concerns.
|
|
361
|
+
- **REFUSED** → the engine's `review_step` guard rejected your call because the
|
|
362
|
+
step is already marked `**Status:** ✅ Complete` in STATUS.md while you're
|
|
363
|
+
trying to run a `code` or `test` review on it. This is the death-spiral
|
|
364
|
+
precondition. Follow the Recovery Recipe above (revert the premature status
|
|
365
|
+
update, commit the revert, then call `review_step` again — it will run
|
|
366
|
+
this time because the step is no longer marked Complete).
|
|
291
367
|
- **UNAVAILABLE** → reviewer failed, proceed with caution
|
|
292
368
|
|
|
293
369
|
**Example flow for a Review Level 2 task, Step 3:**
|