taskplane 0.28.6 → 0.28.8
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,6 +942,14 @@ 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 (revised): Show task title on a second grid row spanning task-id
|
|
946
|
+
// through progress (cols 3–6), giving ~5× more horizontal space than the
|
|
947
|
+
// original task-id-only width. Stops before col 7 (task-step + telemetry)
|
|
948
|
+
// so the step info and worker stats stay visible alongside the title.
|
|
949
|
+
// Falls back to no subtitle row when taskTitle is missing.
|
|
950
|
+
const titleHtml = task.taskTitle
|
|
951
|
+
? `<div class="task-title-subtitle">${escapeHtml(task.taskTitle)}</div>`
|
|
952
|
+
: "";
|
|
861
953
|
html += `
|
|
862
954
|
<div class="task-row">
|
|
863
955
|
<span class="task-icon"><span class="status-dot ${task.status}"></span></span>
|
|
@@ -867,6 +959,7 @@ function renderLanesTasks(batch, sessions) {
|
|
|
867
959
|
<span class="task-duration">${dur}</span>
|
|
868
960
|
<span>${progressHtml}</span>
|
|
869
961
|
<span class="task-step">${stepHtml}${workerHtml}</span>
|
|
962
|
+
${titleHtml}
|
|
870
963
|
</div>`;
|
|
871
964
|
html += reviewerRowHtml;
|
|
872
965
|
}
|
|
@@ -607,8 +607,11 @@ body {
|
|
|
607
607
|
.task-row {
|
|
608
608
|
display: grid;
|
|
609
609
|
grid-template-columns: 36px 24px 100px 90px 80px 200px 1fr;
|
|
610
|
+
/* #485 (revised): row 1 holds the primary cells; row 2 (auto, collapses to
|
|
611
|
+
* 0 when empty) holds the optional task-title-subtitle spanning cols 3–6. */
|
|
612
|
+
grid-template-rows: auto auto;
|
|
610
613
|
align-items: center;
|
|
611
|
-
gap: 8px;
|
|
614
|
+
gap: 8px 8px;
|
|
612
615
|
padding: 8px 14px;
|
|
613
616
|
border-bottom: 1px solid var(--border-subtle);
|
|
614
617
|
transition: background 0.15s;
|
|
@@ -625,6 +628,28 @@ body {
|
|
|
625
628
|
font-family: var(--font-mono);
|
|
626
629
|
font-weight: 600;
|
|
627
630
|
font-size: 0.85rem;
|
|
631
|
+
/* #485 (revised): task-id is back to a single inline cell. The optional
|
|
632
|
+
* title-subtitle is now a separate grid item placed in row 2 — it spans
|
|
633
|
+
* cols 3–6 (task-id through progress) for ~5× more horizontal space than
|
|
634
|
+
* the previous nested-subtitle approach. */
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.task-title-subtitle {
|
|
638
|
+
/* #485 (revised): human-readable task title placed on row 2 of the task-row
|
|
639
|
+
* grid, spanning cols 3–6 (task-id, status, duration, progress) and
|
|
640
|
+
* stopping before col 7 (task-step + telemetry). Auto row 2 collapses to
|
|
641
|
+
* 0 height when this element is absent (no title for the task). */
|
|
642
|
+
grid-column: 3 / 7;
|
|
643
|
+
grid-row: 2;
|
|
644
|
+
font-family: var(--font-sans, inherit);
|
|
645
|
+
font-weight: 400;
|
|
646
|
+
font-size: 0.72rem;
|
|
647
|
+
color: var(--text-muted);
|
|
648
|
+
white-space: nowrap;
|
|
649
|
+
overflow: hidden;
|
|
650
|
+
text-overflow: ellipsis;
|
|
651
|
+
/* Pull subtitle visually closer to the task-id row above. */
|
|
652
|
+
margin-top: -2px;
|
|
628
653
|
}
|
|
629
654
|
|
|
630
655
|
.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
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @module orch/worktree
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync, mkdirSync, readdirSync, realpathSync, rmdirSync, rmSync } from "fs";
|
|
6
|
-
import { execSync } from "child_process";
|
|
6
|
+
import { execSync, execFileSync } from "child_process";
|
|
7
7
|
import { join, basename, resolve } from "path";
|
|
8
8
|
|
|
9
9
|
import { execLog } from "./execution.ts";
|
|
@@ -639,6 +639,57 @@ export function isRetriableRemoveError(stderr: string): boolean {
|
|
|
639
639
|
return false;
|
|
640
640
|
}
|
|
641
641
|
|
|
642
|
+
/**
|
|
643
|
+
* Detect Windows MAX_PATH ("Filename too long") errors from `git worktree remove`.
|
|
644
|
+
*
|
|
645
|
+
* On Windows with default `core.longpaths = false`, git refuses to delete
|
|
646
|
+
* paths that exceed MAX_PATH (260 characters). Deep `node_modules` trees
|
|
647
|
+
* commonly trip this. Native `cmd` `rd /s /q` uses a different deletion
|
|
648
|
+
* code path (NT object namespace, longer path tolerance) and usually
|
|
649
|
+
* succeeds where git fails.
|
|
650
|
+
*
|
|
651
|
+
* @param stderr - Error output from `git worktree remove`
|
|
652
|
+
* @returns true if the failure looks like the Windows MAX_PATH case
|
|
653
|
+
* @since TP-188 (#543)
|
|
654
|
+
*/
|
|
655
|
+
export function isWindowsMaxPathError(stderr: string): boolean {
|
|
656
|
+
if (process.platform !== "win32") return false;
|
|
657
|
+
return /filename too long/i.test(stderr);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Run `cmd /c rd /s /q <path>` to recursively delete a directory on Windows.
|
|
662
|
+
*
|
|
663
|
+
* Used as a fallback after `git worktree remove` fails with the Windows
|
|
664
|
+
* MAX_PATH ("Filename too long") error. Caller must ensure platform is win32
|
|
665
|
+
* and the path is absolute. Path separators are normalized to backslashes
|
|
666
|
+
* because cmd's `rd` is more reliable with native Windows paths.
|
|
667
|
+
*
|
|
668
|
+
* @param absolutePath - Absolute path to remove (forward or back slashes accepted)
|
|
669
|
+
* @returns { ok, stdout, stderr }
|
|
670
|
+
* @since TP-188 (#543)
|
|
671
|
+
*/
|
|
672
|
+
export function runWindowsCmdRd(
|
|
673
|
+
absolutePath: string,
|
|
674
|
+
): { ok: boolean; stdout: string; stderr: string } {
|
|
675
|
+
const winPath = absolutePath.replace(/\//g, "\\");
|
|
676
|
+
try {
|
|
677
|
+
const stdout = execFileSync("cmd", ["/c", "rd", "/s", "/q", winPath], {
|
|
678
|
+
encoding: "utf-8",
|
|
679
|
+
timeout: 60_000,
|
|
680
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
681
|
+
}).toString().trim();
|
|
682
|
+
return { ok: true, stdout, stderr: "" };
|
|
683
|
+
} catch (err: unknown) {
|
|
684
|
+
const e = err as { stdout?: string; stderr?: string; message?: string };
|
|
685
|
+
return {
|
|
686
|
+
ok: false,
|
|
687
|
+
stdout: (e.stdout ?? "").toString().trim(),
|
|
688
|
+
stderr: (e.stderr ?? e.message ?? "unknown error").toString().trim(),
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
642
693
|
/**
|
|
643
694
|
* Remove a git worktree and clean up its associated branch.
|
|
644
695
|
*
|
|
@@ -733,6 +784,50 @@ export function removeWorktree(
|
|
|
733
784
|
|
|
734
785
|
lastError = removeResult.stderr;
|
|
735
786
|
|
|
787
|
+
// ── Windows MAX_PATH fallback (#543) ────────────────────────
|
|
788
|
+
// On Windows, `git worktree remove` fails with "Filename too long"
|
|
789
|
+
// when the worktree contains deep `node_modules` trees (most
|
|
790
|
+
// non-trivial Node projects) and `core.longpaths = false` (default).
|
|
791
|
+
// `cmd /c rd /s /q <path>` uses a different deletion code path
|
|
792
|
+
// that tolerates long paths better. Try it ONCE before classifying
|
|
793
|
+
// the error as terminal/retriable so other error classes still
|
|
794
|
+
// surface unchanged.
|
|
795
|
+
if (isWindowsMaxPathError(lastError)) {
|
|
796
|
+
execLog(
|
|
797
|
+
"cleanup",
|
|
798
|
+
"worktree",
|
|
799
|
+
`Windows MAX_PATH detected — falling back to cmd "rd /s /q"`,
|
|
800
|
+
{ path: worktreePath, attempt },
|
|
801
|
+
);
|
|
802
|
+
const fallback = runWindowsCmdRd(worktreePath);
|
|
803
|
+
if (fallback.ok) {
|
|
804
|
+
execLog(
|
|
805
|
+
"cleanup",
|
|
806
|
+
"worktree",
|
|
807
|
+
`cmd "rd /s /q" fallback succeeded; pruning git worktree state`,
|
|
808
|
+
{ path: worktreePath },
|
|
809
|
+
);
|
|
810
|
+
// The on-disk tree is gone; git's bookkeeping still has a
|
|
811
|
+
// stale entry. Prune so isRegisteredWorktree() returns false
|
|
812
|
+
// during post-removal verification below.
|
|
813
|
+
runGit(["worktree", "prune"], repoRoot);
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
// Fallback also failed — enrich error so the operator sees both
|
|
817
|
+
// attempts, then fall through to the existing terminal/retry
|
|
818
|
+
// classification (which will throw because "Filename too long"
|
|
819
|
+
// is non-retriable per isRetriableRemoveError).
|
|
820
|
+
execLog(
|
|
821
|
+
"cleanup",
|
|
822
|
+
"worktree",
|
|
823
|
+
`cmd "rd /s /q" fallback failed`,
|
|
824
|
+
{ path: worktreePath, error: fallback.stderr.slice(0, 200) },
|
|
825
|
+
);
|
|
826
|
+
lastError =
|
|
827
|
+
`git worktree remove failed: ${lastError}; ` +
|
|
828
|
+
`cmd rd /s /q fallback failed: ${fallback.stderr}`;
|
|
829
|
+
}
|
|
830
|
+
|
|
736
831
|
// Check if error is terminal (non-retriable)
|
|
737
832
|
if (!isRetriableRemoveError(lastError)) {
|
|
738
833
|
throw new WorktreeError(
|
package/package.json
CHANGED
|
@@ -50,6 +50,74 @@ You handle a single review request and then exit.
|
|
|
50
50
|
Do NOT just respond with text — the orchestrator reads the OUTPUT FILE to get
|
|
51
51
|
your verdict. If you don't write the file, your review is lost.
|
|
52
52
|
|
|
53
|
+
## Quality-check verification (code reviews only)
|
|
54
|
+
|
|
55
|
+
**This section applies to code reviews only.** For plan reviews, skip this
|
|
56
|
+
section entirely — there is no code to type-check or lint yet.
|
|
57
|
+
|
|
58
|
+
Before returning a code-review verdict, run the project's declared
|
|
59
|
+
typecheck / lint / format-check commands against the post-change tree. A
|
|
60
|
+
behavioural-correctness APPROVE is **invalidated** by failing quality checks.
|
|
61
|
+
|
|
62
|
+
The reviewer's tool allowlist already includes `bash`, so you can invoke these
|
|
63
|
+
commands directly — no special tooling is required.
|
|
64
|
+
|
|
65
|
+
### How to discover the commands
|
|
66
|
+
|
|
67
|
+
1. **Project config first.** Read `.pi/taskplane-config.json` (or the legacy
|
|
68
|
+
`.pi/task-runner.yaml` / `.pi/task-runner.json` fallbacks) and look at
|
|
69
|
+
`taskRunner.testing.commands` — a `Record<string, string>` mapping a
|
|
70
|
+
command name (e.g. `typecheck`, `lint`, `format:check`) to a
|
|
71
|
+
shell command. Run any command whose key matches one of
|
|
72
|
+
`typecheck` / `tsc` / `types` / `lint` / `format:check`.
|
|
73
|
+
**Prefer `format:check` over `format`** — the latter typically rewrites
|
|
74
|
+
files in place, which would mutate the working tree the reviewer is
|
|
75
|
+
evaluating. If only a mutating `format` script is available in either
|
|
76
|
+
source, skip it and note this in the Summary; do not run mutating
|
|
77
|
+
commands from the reviewer.
|
|
78
|
+
2. **Fallback to `package.json` scripts.** If step 1 did not yield any
|
|
79
|
+
relevant commands — either because `taskRunner.testing.commands` is
|
|
80
|
+
absent OR because it exists but contains no keys matching the
|
|
81
|
+
typecheck/lint/format-check set — read `package.json` and run any of
|
|
82
|
+
these scripts that exist, in this order:
|
|
83
|
+
`npm run typecheck`, `npm run lint`, `npm run format:check`.
|
|
84
|
+
Skip a script if `package.json#scripts` does not declare it — do not
|
|
85
|
+
invent commands.
|
|
86
|
+
3. **Skip silently** if neither source yields a relevant command. Do not fail
|
|
87
|
+
the review just because the project has no quality-check pipeline
|
|
88
|
+
configured. Note this in the Summary so the operator knows quality checks
|
|
89
|
+
were not exercised.
|
|
90
|
+
|
|
91
|
+
Do NOT run the project's full test suite from this section — that is the
|
|
92
|
+
worker's Testing & Verification step. The quality checks here are
|
|
93
|
+
**fast static checks** (typecheck, lint, format) that are cheap to run and
|
|
94
|
+
high-signal for catching regressions the behavioural diff review would miss.
|
|
95
|
+
|
|
96
|
+
### What to do with the results
|
|
97
|
+
|
|
98
|
+
- **All quality checks pass** → proceed to behavioural code review as normal.
|
|
99
|
+
- **A quality check fails** → surface each failing command as an entry in
|
|
100
|
+
**Issues Found** with severity `important`. Include:
|
|
101
|
+
- The command that failed (e.g. `npm run typecheck`)
|
|
102
|
+
- The first few lines of the failing output (file/line locations are
|
|
103
|
+
most useful)
|
|
104
|
+
- A concrete suggested fix where the failure makes one obvious
|
|
105
|
+
- **Verdict downgrade rule:** If quality checks fail, the verdict is
|
|
106
|
+
**REVISE** — even if the behavioural code review would otherwise have
|
|
107
|
+
been APPROVE. Quality-check failures are blocking by definition: they
|
|
108
|
+
would surface at the worker's Testing & Verification step and force a
|
|
109
|
+
redo of the entire review cycle, so it is strictly cheaper to surface
|
|
110
|
+
them here.
|
|
111
|
+
|
|
112
|
+
### Worked example (Issues Found entry)
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
1. **[npm run typecheck:1] [important]** — 5 strict-mode errors in
|
|
116
|
+
tests/foo.test.ts. Sample: "Argument of type 'undefined' is not
|
|
117
|
+
assignable to parameter of type 'string'" at line 42. Fix: narrow
|
|
118
|
+
`getThing()` return type or assert non-null at call site.
|
|
119
|
+
```
|
|
120
|
+
|
|
53
121
|
## Verdict Criteria
|
|
54
122
|
|
|
55
123
|
- **APPROVE** — Step will achieve its stated outcomes. Minor suggestions belong
|