canon-ai 3.3.1 → 3.5.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.
- package/CHANGELOG.md +39 -0
- package/README.md +1 -1
- package/dist/cli/index.js +68 -90
- package/dist/orchestrator/run-task.js +582 -236
- package/package.json +1 -1
- package/templates/.canon/templates/handoff.md +1 -1
- package/templates/.canon/templates/review.md +4 -0
- package/templates/.claude/agents/code-review-anchored.md +14 -0
- package/templates/.claude/agents/code-review-cold.md +5 -2
- package/templates/.claude/skills/canon-pipeline/recovery.md +21 -0
- package/templates/.claude/skills/canon-spec/SKILL.md +10 -1
- package/templates/.claude/skills/canon-spec-review/SKILL.md +7 -3
- package/templates/docs/pipeline-orchestrator.md +37 -32
- package/templates/scripts/docs-refs-check.mjs +6 -3
|
@@ -79,12 +79,12 @@ import { pathToFileURL } from "url";
|
|
|
79
79
|
|
|
80
80
|
// src/orchestrator/main.ts
|
|
81
81
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
82
|
-
import
|
|
82
|
+
import fs21 from "fs";
|
|
83
83
|
import os from "os";
|
|
84
84
|
import path20 from "path";
|
|
85
85
|
|
|
86
86
|
// src/orchestrator/phases/code-review.ts
|
|
87
|
-
import
|
|
87
|
+
import fs16 from "fs";
|
|
88
88
|
import path15 from "path";
|
|
89
89
|
|
|
90
90
|
// src/orchestrator/cli.ts
|
|
@@ -313,6 +313,7 @@ function isSynchronousMode(args) {
|
|
|
313
313
|
|
|
314
314
|
// src/orchestrator/git.ts
|
|
315
315
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
316
|
+
import fs6 from "fs";
|
|
316
317
|
import path5 from "path";
|
|
317
318
|
|
|
318
319
|
// src/orchestrator/env.ts
|
|
@@ -356,7 +357,7 @@ var WORKTREES_ROOT = process.env.CANON_WORKTREES_ROOT ? path.resolve(REPO_ROOT,
|
|
|
356
357
|
var STALL_TIMEOUT_MS = Number(process.env.PIPELINE_STALL_TIMEOUT_MS) || 10 * 60 * 1e3;
|
|
357
358
|
var STALL_KILL_GRACE_MS = 3e3;
|
|
358
359
|
var LEGACY_FALLBACK_ENV_VARS = [
|
|
359
|
-
{ old: "CLAUDE_MODEL", replacement: "
|
|
360
|
+
{ old: "CLAUDE_MODEL", replacement: "CLAUDE_MODEL_LIGHT / CLAUDE_MODEL_STRONG (still honored as a fallback for every Claude phase, including qa)" },
|
|
360
361
|
{ old: "CODEX_MODEL_DEFAULT", replacement: "CODEX_MODEL_MINI (still honored as fallback)" },
|
|
361
362
|
{ old: "CODEX_MODEL_DELICATE", replacement: "CODEX_MODEL_FULL (still honored as fallback)" }
|
|
362
363
|
];
|
|
@@ -415,15 +416,6 @@ function resolveProjectName() {
|
|
|
415
416
|
}
|
|
416
417
|
var config = {
|
|
417
418
|
projectName: resolveProjectName(),
|
|
418
|
-
claudeBudget: process.env.CLAUDE_BUDGET ?? null,
|
|
419
|
-
claudeModelSpec: process.env.CLAUDE_MODEL_SPEC ?? process.env.CLAUDE_MODEL ?? "opus",
|
|
420
|
-
claudeModelPlan: process.env.CLAUDE_MODEL_PLAN ?? process.env.CLAUDE_MODEL ?? "sonnet",
|
|
421
|
-
claudeModelReview: process.env.CLAUDE_MODEL_REVIEW ?? process.env.CLAUDE_MODEL ?? "sonnet",
|
|
422
|
-
claudeModelReviewLarge: process.env.CLAUDE_MODEL_REVIEW_LARGE ?? process.env.CLAUDE_MODEL ?? "opus",
|
|
423
|
-
claudeModelQa: process.env.CLAUDE_MODEL_QA ?? process.env.CLAUDE_MODEL ?? "sonnet",
|
|
424
|
-
codexModelMini: process.env.CODEX_MODEL_MINI ?? process.env.CODEX_MODEL_DEFAULT ?? "gpt-6-luna",
|
|
425
|
-
codexModelFull: process.env.CODEX_MODEL_FULL ?? process.env.CODEX_MODEL_DELICATE ?? "gpt-6-sol",
|
|
426
|
-
maxReviewLoops: parseMaxReviewLoops(process.env.MAX_REVIEW_LOOPS),
|
|
427
419
|
maxContextBytes: Number.parseInt(process.env.MAX_CONTEXT_BYTES ?? String(64 * 1024), 10)
|
|
428
420
|
};
|
|
429
421
|
|
|
@@ -1329,6 +1321,23 @@ function filterGitIgnoredPaths(paths, cwd) {
|
|
|
1329
1321
|
const stdout = result.stdout ?? "";
|
|
1330
1322
|
return new Set(stdout.split("\0").filter((p) => p.length > 0));
|
|
1331
1323
|
}
|
|
1324
|
+
function filterStageablePaths(paths, cwd) {
|
|
1325
|
+
if (paths.length === 0) return [];
|
|
1326
|
+
return paths.filter((candidate) => {
|
|
1327
|
+
const probe = spawnSync3("git", ["ls-files", "--error-unmatch", "--", candidate], {
|
|
1328
|
+
cwd,
|
|
1329
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
1330
|
+
});
|
|
1331
|
+
if (probe.error || probe.status !== 1) return true;
|
|
1332
|
+
try {
|
|
1333
|
+
fs6.lstatSync(path5.join(cwd, candidate));
|
|
1334
|
+
return true;
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
const code = error.code;
|
|
1337
|
+
return code !== "ENOENT" && code !== "ENOTDIR";
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1332
1341
|
function commitTaskArtifactsToBase(taskIds, _artifactFiles) {
|
|
1333
1342
|
void _artifactFiles;
|
|
1334
1343
|
for (const taskId of taskIds) {
|
|
@@ -1410,7 +1419,10 @@ function truncateUtf8(input, capBytes) {
|
|
|
1410
1419
|
return bytes.subarray(0, end).toString("utf8");
|
|
1411
1420
|
}
|
|
1412
1421
|
function getScopedDiff(baseBranch, cwd, capBytes = 5e4) {
|
|
1413
|
-
|
|
1422
|
+
return getScopedDiffInRange(`${baseBranch}...HEAD`, cwd, capBytes);
|
|
1423
|
+
}
|
|
1424
|
+
function getScopedDiffInRange(rangeExpr, cwd, capBytes = 5e4) {
|
|
1425
|
+
const result = gitSafeAtRaw(cwd, "diff", rangeExpr);
|
|
1414
1426
|
if (!result.ok) return null;
|
|
1415
1427
|
const raw = result.stdout;
|
|
1416
1428
|
if (Buffer.byteLength(raw, "utf8") <= capBytes) {
|
|
@@ -1574,10 +1586,34 @@ function parseNameStatusOutput(raw) {
|
|
|
1574
1586
|
return [...paths].sort();
|
|
1575
1587
|
}
|
|
1576
1588
|
function getAffectedFiles(baseRef, cwd) {
|
|
1577
|
-
|
|
1578
|
-
|
|
1589
|
+
return getPathsInRange(`${baseRef}...HEAD`, cwd) ?? [];
|
|
1590
|
+
}
|
|
1591
|
+
function getPathsInRange(rangeExpr, cwd) {
|
|
1592
|
+
const result = gitSafeAtRaw(cwd, "diff", rangeExpr, "--name-status", "-M", "-z");
|
|
1593
|
+
if (!result.ok) return null;
|
|
1594
|
+
if (!result.stdout) return [];
|
|
1579
1595
|
return parseNameStatusOutput(result.stdout);
|
|
1580
1596
|
}
|
|
1597
|
+
function resolveCommit(ref, cwd) {
|
|
1598
|
+
const result = gitSafeAt(cwd, "rev-parse", "--verify", `${ref}^{commit}`);
|
|
1599
|
+
return result.ok ? result.stdout.trim() : null;
|
|
1600
|
+
}
|
|
1601
|
+
function isAncestorCommit(ancestorRef, descendantRef, cwd) {
|
|
1602
|
+
return gitSafeAt(cwd, "merge-base", "--is-ancestor", ancestorRef, descendantRef).ok;
|
|
1603
|
+
}
|
|
1604
|
+
function getDeltaLineStats(prevSha, cwd, headRef = "HEAD") {
|
|
1605
|
+
const result = gitSafeAtRaw(cwd, "diff", `${prevSha}..${headRef}`, "--numstat", "--no-renames");
|
|
1606
|
+
if (!result.ok) return null;
|
|
1607
|
+
const stats = result.stdout.split("\n").filter(Boolean).map((line) => {
|
|
1608
|
+
const [addedRaw, deletedRaw, ...pathParts] = line.split(" ");
|
|
1609
|
+
return {
|
|
1610
|
+
path: pathParts.join(" "),
|
|
1611
|
+
added: addedRaw === "-" ? 0 : Number(addedRaw),
|
|
1612
|
+
deleted: deletedRaw === "-" ? 0 : Number(deletedRaw)
|
|
1613
|
+
};
|
|
1614
|
+
});
|
|
1615
|
+
return stats.every((stat) => stat.path.length > 0 && Number.isFinite(stat.added) && Number.isFinite(stat.deleted)) ? stats : null;
|
|
1616
|
+
}
|
|
1581
1617
|
function getTreeDriftFiles(baseRef, cwd) {
|
|
1582
1618
|
const result = gitSafeAtRaw(cwd, "diff", baseRef, "HEAD", "--name-status", "-M", "-z");
|
|
1583
1619
|
if (!result.ok) {
|
|
@@ -1587,6 +1623,31 @@ function getTreeDriftFiles(baseRef, cwd) {
|
|
|
1587
1623
|
}
|
|
1588
1624
|
|
|
1589
1625
|
// src/lib/pipeline-policy.ts
|
|
1626
|
+
var CODE_REVIEW_DELTA_LINE_THRESHOLD = 400;
|
|
1627
|
+
function isReviewOwnedPath(filePath, facts) {
|
|
1628
|
+
return facts.taskIds.some((id) => filePath === `tasks/${id}` || filePath.startsWith(`tasks/${id}/`)) || facts.telemetryFiles.includes(filePath);
|
|
1629
|
+
}
|
|
1630
|
+
function resolveCodeReviewScope(facts) {
|
|
1631
|
+
const full = (reason) => ({ scope: "full", base: facts.baseBranch, reason });
|
|
1632
|
+
if (facts.isRound1) return full("Round 1 (initial review)");
|
|
1633
|
+
if (facts.effectiveSize === "XL" || facts.delicate) return full(facts.delicate ? "delicate" : "XL task size");
|
|
1634
|
+
const prev = facts.prevRecord;
|
|
1635
|
+
if (prev === null) return full(facts.previousRecordsDisagree ? "bundle members disagree on the previous reviewed commit" : facts.previousArchiveMalformed ? "unparseable cold-Codex archive record for the previous round" : "no cold-Codex archive record for the previous round");
|
|
1636
|
+
if (!prev.exists) return full(`previous reviewed commit ${prev.reviewedSha} does not resolve`);
|
|
1637
|
+
if (!prev.isAncestor) return full(`previous reviewed commit ${prev.reviewedSha} is not an ancestor of HEAD`);
|
|
1638
|
+
if (prev.equalsHead) return full("previous reviewed commit equals HEAD");
|
|
1639
|
+
if (facts.deltaPaths === null) return full("delta path probe failed");
|
|
1640
|
+
if (facts.priorChangeSetPaths === null) return full("previous change-set path probe failed");
|
|
1641
|
+
if (facts.deltaFileStats === null) return full("delta line-stat probe failed");
|
|
1642
|
+
const priorPaths = new Set(facts.priorChangeSetPaths.filter((p) => !isReviewOwnedPath(p, facts)));
|
|
1643
|
+
const outside = facts.deltaPaths.find((p) => !isReviewOwnedPath(p, facts) && !priorPaths.has(p));
|
|
1644
|
+
if (outside) return full(`${outside} is outside the previous round's change set`);
|
|
1645
|
+
const lines = facts.deltaFileStats.filter((stat) => !isReviewOwnedPath(stat.path, facts)).reduce((sum, stat) => sum + stat.added + stat.deleted, 0);
|
|
1646
|
+
if (lines > CODE_REVIEW_DELTA_LINE_THRESHOLD) {
|
|
1647
|
+
return full(`delta line count ${lines} exceeds ${CODE_REVIEW_DELTA_LINE_THRESHOLD}`);
|
|
1648
|
+
}
|
|
1649
|
+
return { scope: "delta", base: prev.reviewedSha, reason: "delta" };
|
|
1650
|
+
}
|
|
1590
1651
|
var SIZE_ORDER = ["XS", "S", "M", "L", "XL"];
|
|
1591
1652
|
var SINGLE_PASS_BUDGET_BY_SIZE = {
|
|
1592
1653
|
XS: "5.00",
|
|
@@ -1663,7 +1724,37 @@ function codexMatrix(config3) {
|
|
|
1663
1724
|
}
|
|
1664
1725
|
};
|
|
1665
1726
|
}
|
|
1666
|
-
|
|
1727
|
+
var CLAUDE_CELLS = {
|
|
1728
|
+
spec: {
|
|
1729
|
+
XS: { tier: "light", effort: "medium" },
|
|
1730
|
+
S: { tier: "light", effort: "medium" },
|
|
1731
|
+
M: { tier: "strong", effort: "medium" },
|
|
1732
|
+
L: { tier: "strong", effort: "medium" },
|
|
1733
|
+
XL: { tier: "strong", effort: "high" }
|
|
1734
|
+
},
|
|
1735
|
+
plan: {
|
|
1736
|
+
XS: { tier: "light", effort: "medium" },
|
|
1737
|
+
S: { tier: "light", effort: "medium" },
|
|
1738
|
+
M: { tier: "strong", effort: "medium" },
|
|
1739
|
+
L: { tier: "strong", effort: "medium" },
|
|
1740
|
+
XL: { tier: "strong", effort: "high" }
|
|
1741
|
+
},
|
|
1742
|
+
code_review: {
|
|
1743
|
+
XS: { tier: "light", effort: "medium" },
|
|
1744
|
+
S: { tier: "light", effort: "medium" },
|
|
1745
|
+
M: { tier: "strong", effort: "medium" },
|
|
1746
|
+
L: { tier: "strong", effort: "medium" },
|
|
1747
|
+
XL: { tier: "strong", effort: "high" }
|
|
1748
|
+
},
|
|
1749
|
+
qa: {
|
|
1750
|
+
XS: { tier: "light", effort: "medium" },
|
|
1751
|
+
S: { tier: "light", effort: "medium" },
|
|
1752
|
+
M: { tier: "light", effort: "medium" },
|
|
1753
|
+
L: { tier: "light", effort: "medium" },
|
|
1754
|
+
XL: { tier: "light", effort: "medium" }
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
function claudePinFor(phase, size, config3) {
|
|
1667
1758
|
switch (phase) {
|
|
1668
1759
|
case "spec":
|
|
1669
1760
|
return config3.claudeModelSpec;
|
|
@@ -1671,48 +1762,36 @@ function claudeModelFor(config3, phase) {
|
|
|
1671
1762
|
return config3.claudeModelPlan;
|
|
1672
1763
|
case "qa":
|
|
1673
1764
|
return config3.claudeModelQa;
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
// aren't Claude phases; fall back to the spec model so resumed Claude
|
|
1677
|
-
// sessions survive accidental use.
|
|
1678
|
-
default:
|
|
1679
|
-
return config3.claudeModelSpec;
|
|
1765
|
+
case "code_review":
|
|
1766
|
+
return size === "XL" ? config3.claudeModelReviewLarge : config3.claudeModelReview;
|
|
1680
1767
|
}
|
|
1681
1768
|
}
|
|
1769
|
+
function resolveClaudeModel(phase, size, config3) {
|
|
1770
|
+
const pin = claudePinFor(phase, size, config3);
|
|
1771
|
+
if (pin !== null) return pin;
|
|
1772
|
+
const tier = CLAUDE_CELLS[phase][size].tier;
|
|
1773
|
+
const tierModel = tier === "light" ? config3.claudeModelLight : config3.claudeModelStrong;
|
|
1774
|
+
if (tierModel !== null) return tierModel;
|
|
1775
|
+
if (config3.claudeModelLegacy !== null) return config3.claudeModelLegacy;
|
|
1776
|
+
return tier === "light" ? "sonnet" : "opus";
|
|
1777
|
+
}
|
|
1682
1778
|
function claudeMatrix(config3) {
|
|
1683
|
-
const
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
const buildMedium = (phase) => {
|
|
1694
|
-
const model = claudeModelFor(config3, phase);
|
|
1695
|
-
return {
|
|
1696
|
-
XS: { model, effort: "medium" },
|
|
1697
|
-
S: { model, effort: "medium" },
|
|
1698
|
-
M: { model, effort: "medium" },
|
|
1699
|
-
L: { model, effort: "high" },
|
|
1700
|
-
XL: { model, effort: "high" }
|
|
1701
|
-
};
|
|
1702
|
-
};
|
|
1703
|
-
const codeReviewMatrix = () => ({
|
|
1704
|
-
XS: { model: config3.claudeModelReview, effort: "medium" },
|
|
1705
|
-
S: { model: config3.claudeModelReview, effort: "medium" },
|
|
1706
|
-
M: { model: config3.claudeModelReview, effort: "high" },
|
|
1707
|
-
L: { model: config3.claudeModelReview, effort: "high" },
|
|
1708
|
-
XL: { model: config3.claudeModelReviewLarge, effort: "xhigh" }
|
|
1779
|
+
const cell = (phase, size) => ({
|
|
1780
|
+
model: resolveClaudeModel(phase, size, config3),
|
|
1781
|
+
effort: CLAUDE_CELLS[phase][size].effort
|
|
1782
|
+
});
|
|
1783
|
+
const row = (phase) => ({
|
|
1784
|
+
XS: cell(phase, "XS"),
|
|
1785
|
+
S: cell(phase, "S"),
|
|
1786
|
+
M: cell(phase, "M"),
|
|
1787
|
+
L: cell(phase, "L"),
|
|
1788
|
+
XL: cell(phase, "XL")
|
|
1709
1789
|
});
|
|
1710
1790
|
return {
|
|
1711
|
-
spec:
|
|
1712
|
-
plan:
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
qa: buildMedium("qa")
|
|
1791
|
+
spec: row("spec"),
|
|
1792
|
+
plan: row("plan"),
|
|
1793
|
+
code_review: row("code_review"),
|
|
1794
|
+
qa: row("qa")
|
|
1716
1795
|
};
|
|
1717
1796
|
}
|
|
1718
1797
|
function getPipelinePolicy(tasks, config3) {
|
|
@@ -1738,11 +1817,14 @@ function getPipelinePolicy(tasks, config3) {
|
|
|
1738
1817
|
|
|
1739
1818
|
// src/orchestrator/policy.ts
|
|
1740
1819
|
var config2 = {
|
|
1741
|
-
claudeModelSpec: process.env.CLAUDE_MODEL_SPEC ??
|
|
1742
|
-
claudeModelPlan: process.env.CLAUDE_MODEL_PLAN ??
|
|
1743
|
-
claudeModelReview: process.env.CLAUDE_MODEL_REVIEW ??
|
|
1744
|
-
claudeModelReviewLarge: process.env.CLAUDE_MODEL_REVIEW_LARGE ??
|
|
1745
|
-
claudeModelQa: process.env.CLAUDE_MODEL_QA ??
|
|
1820
|
+
claudeModelSpec: process.env.CLAUDE_MODEL_SPEC ?? null,
|
|
1821
|
+
claudeModelPlan: process.env.CLAUDE_MODEL_PLAN ?? null,
|
|
1822
|
+
claudeModelReview: process.env.CLAUDE_MODEL_REVIEW ?? null,
|
|
1823
|
+
claudeModelReviewLarge: process.env.CLAUDE_MODEL_REVIEW_LARGE ?? null,
|
|
1824
|
+
claudeModelQa: process.env.CLAUDE_MODEL_QA ?? null,
|
|
1825
|
+
claudeModelLight: process.env.CLAUDE_MODEL_LIGHT || null,
|
|
1826
|
+
claudeModelStrong: process.env.CLAUDE_MODEL_STRONG || null,
|
|
1827
|
+
claudeModelLegacy: process.env.CLAUDE_MODEL ?? null,
|
|
1746
1828
|
codexModelMini: process.env.CODEX_MODEL_MINI ?? process.env.CODEX_MODEL_DEFAULT ?? "gpt-6-luna",
|
|
1747
1829
|
codexModelFull: process.env.CODEX_MODEL_FULL ?? process.env.CODEX_MODEL_DELICATE ?? "gpt-6-sol",
|
|
1748
1830
|
maxReviewLoops: parseMaxReviewLoops(process.env.MAX_REVIEW_LOOPS),
|
|
@@ -1755,6 +1837,9 @@ function policyConfig() {
|
|
|
1755
1837
|
claudeModelReview: config2.claudeModelReview,
|
|
1756
1838
|
claudeModelReviewLarge: config2.claudeModelReviewLarge,
|
|
1757
1839
|
claudeModelQa: config2.claudeModelQa,
|
|
1840
|
+
claudeModelLight: config2.claudeModelLight,
|
|
1841
|
+
claudeModelStrong: config2.claudeModelStrong,
|
|
1842
|
+
claudeModelLegacy: config2.claudeModelLegacy,
|
|
1758
1843
|
codexModelMini: config2.codexModelMini,
|
|
1759
1844
|
codexModelFull: config2.codexModelFull,
|
|
1760
1845
|
maxReviewLoops: config2.maxReviewLoops,
|
|
@@ -1790,15 +1875,15 @@ function isPlanCombined2(status) {
|
|
|
1790
1875
|
import { spawn as spawn2 } from "child_process";
|
|
1791
1876
|
|
|
1792
1877
|
// src/orchestrator/metrics.ts
|
|
1793
|
-
import
|
|
1878
|
+
import fs7 from "fs";
|
|
1794
1879
|
import path6 from "path";
|
|
1795
1880
|
function getMetricsFile(activeCwd) {
|
|
1796
1881
|
return process.env.CANON_METRICS_FILE_OVERRIDE ? path6.resolve(process.env.CANON_METRICS_FILE_OVERRIDE) : path6.join(activeCwd ?? REPO_ROOT, "docs/pipeline-invocations.md");
|
|
1797
1882
|
}
|
|
1798
1883
|
function recordMetric(entry) {
|
|
1799
1884
|
const metricsFile = getMetricsFile(entry.activeCwd);
|
|
1800
|
-
if (!
|
|
1801
|
-
|
|
1885
|
+
if (!fs7.existsSync(metricsFile)) {
|
|
1886
|
+
fs7.writeFileSync(metricsFile, [
|
|
1802
1887
|
"# Workflow Metrics",
|
|
1803
1888
|
"",
|
|
1804
1889
|
"> Auto-logged by canon's orchestrator. One row per agent invocation.",
|
|
@@ -1812,7 +1897,7 @@ function recordMetric(entry) {
|
|
|
1812
1897
|
const safeCell = (v) => v.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
1813
1898
|
const dur = (entry.durationMs / 1e3).toFixed(1) + "s";
|
|
1814
1899
|
const tok = entry.tokens != null ? String(entry.tokens) : "-";
|
|
1815
|
-
|
|
1900
|
+
fs7.appendFileSync(
|
|
1816
1901
|
metricsFile,
|
|
1817
1902
|
`| ${(/* @__PURE__ */ new Date()).toISOString()} | ${entry.taskId} | ${entry.phase} | ${entry.agent} | ${safeCell(entry.model)} | ${entry.iteration ?? "-"} | ${dur} | ${tok} | ${entry.status} |
|
|
1818
1903
|
`
|
|
@@ -1824,6 +1909,7 @@ var CLAUDE_STARTUP = "Read docs/patterns.md before starting.\nSkim docs/lessons-
|
|
|
1824
1909
|
var CODEX_STARTUP = 'Read docs/patterns.md and docs/codebase-map.md before starting.\nSkim docs/lessons-learned.md for entries relevant to your task area.\nRead docs/decisions.md if a settled decision governs the area you are changing, or if the task would revisit one.\nRead docs/product-context.md if the task touches user-visible behavior, product terminology, or a business rule.\nGround every claim in the current file, diff, or artifact before you state it. Do not rely on prior-session memory for code existence, validation results, or completion status.\nOn resumed sessions, re-read the task-specific files named in the prompt and inspect the current working tree before saying anything is already done.\n\nGit ownership: the pipeline orchestrator handles staging, committing, and pushing \u2014 do NOT run `git add`, `git commit`, or `git push`. Edit files in the working tree only; the orchestrator reads `git status` after your session and stages every file listed in handoff.md\'s Changes table. Read-only git is fine (`git status`, `git diff`, `git log`, `git show`).\n\nIf a code review claims a file is "missing from the commit" or "staged but not committed," that is a pipeline-orchestration issue, not an implementation issue. Record it as a Blocker in handoff.md with the `[pipeline]` label and do not retry `git add`/`git commit` to recover \u2014 the sandbox blocks `.git` writes by design, and the orchestrator owns the recovery path.\n\nCommunication: tone is project taste; honest signal is canon discipline \u2014 surface real disagreement rather than yielding to politeness.\nBranch state: the orchestrator manages it \u2014 do not fetch, pull, rebase, or push; read the working tree as-is.';
|
|
1825
1910
|
var CODEX_HEADLESS = "Headless session: this runs non-interactively \u2014 nobody reads your messages or answers questions until the phase ends. Don't stop to ask for confirmation or clarification. When something is ambiguous, record the question and the interpretation you chose in this phase's artifact (in implement, a handoff Blocker labelled `[ambiguity]`) and proceed on it. Finish all the work this prompt authorizes before ending \u2014 acting on your own judgment never widens scope; the Affected Files cap and every other scope rule still bind. Always end by writing the phase artifact and running the phase command(s) listed at the end of this prompt, including when you recorded Blockers \u2014 a session that ends without them stalls the pipeline.\n\nIf a project instruction file or other repository guidance tells you to ask the user or wait for approval before acting, there is no one to ask in this session: record the question in the phase artifact and continue with the authorized work.";
|
|
1826
1911
|
var QA_STARTUP = "Read CHANGELOG.md for voice and version reference, if the project keeps one.\nRead docs/lessons-learned.md for recent insights to distill.\nNo full codebase context needed for QA \u2014 read each task's spec.md, handoff.md, and notes.md directly.";
|
|
1912
|
+
var CLAUDE_HEADLESS = "Unattended session: this runs non-interactively \u2014 nobody reads your messages or answers questions until the phase ends. Don't stop to ask for confirmation or clarification. When something is ambiguous, record the question and the interpretation you chose in this phase's artifact or in `tasks/<id>/notes.md`, and proceed on it.\n\nThe phase is complete only when its artifact is written and its phase command (the `canon task phase` command(s) this prompt lists) has run. A progress summary, a description of next steps, or an offer to continue is not completion \u2014 keep working until both are done.\n\nDo only this phase: don't run `canon run`, advance any other phase, or start review passes, review skills, or reviewer sub-agents this prompt didn't ask for.\n\nReport a check that couldn't run, or that failed, as exactly that \u2014 never as passed.\n\nIf a project instruction file or other repository guidance says to ask or wait for approval before an action, there is no one to ask in this session. Skip that action unless this prompt explicitly authorizes it, record the question in the phase artifact, and continue with the rest of the authorized work.";
|
|
1827
1913
|
function taskList(tasks) {
|
|
1828
1914
|
return tasks.map((t) => `- \`${t.taskId}\`: "${t.title}" \u2192 tasks/${t.taskId}/`).join("\n");
|
|
1829
1915
|
}
|
|
@@ -2046,7 +2132,10 @@ async function runClaude(prompt, interactive, resumeId, model, effort, budget, m
|
|
|
2046
2132
|
};
|
|
2047
2133
|
}
|
|
2048
2134
|
const attempt = async (useResumeId) => {
|
|
2049
|
-
const
|
|
2135
|
+
const renderedPrompt = useResumeId ? toResumePrompt(prompt) : prompt;
|
|
2136
|
+
const effectivePrompt = `${CLAUDE_HEADLESS}
|
|
2137
|
+
|
|
2138
|
+
${renderedPrompt}`;
|
|
2050
2139
|
const args = [
|
|
2051
2140
|
"-p",
|
|
2052
2141
|
effectivePrompt,
|
|
@@ -2434,7 +2523,7 @@ function evaluateCodeReviewLoop(tasks, cap) {
|
|
|
2434
2523
|
}
|
|
2435
2524
|
|
|
2436
2525
|
// src/orchestrator/validation.ts
|
|
2437
|
-
import
|
|
2526
|
+
import fs8 from "fs";
|
|
2438
2527
|
import path7 from "path";
|
|
2439
2528
|
|
|
2440
2529
|
// src/orchestrator/markdown-table.ts
|
|
@@ -2751,7 +2840,7 @@ function normalizeCheckLabel(value) {
|
|
|
2751
2840
|
}
|
|
2752
2841
|
function parseValidationRequiredChecks(specPath) {
|
|
2753
2842
|
try {
|
|
2754
|
-
const content =
|
|
2843
|
+
const content = fs8.readFileSync(specPath, "utf8");
|
|
2755
2844
|
const section = content.match(/## Validation Required\n\n([\s\S]*?)(?:\n## |\n# |$)/);
|
|
2756
2845
|
if (!section) return null;
|
|
2757
2846
|
const checks = [];
|
|
@@ -2838,16 +2927,17 @@ function checkRerouteEvidence(phase, artifactContent, status) {
|
|
|
2838
2927
|
}
|
|
2839
2928
|
return { reroute: true, ok: true };
|
|
2840
2929
|
}
|
|
2930
|
+
var HUMAN_AMENDMENT_HEADING_RE = /^#{2,6}[ \t]+Amendment\b(?![ \t]*\(full-send scope\))/im;
|
|
2841
2931
|
function verifyRerouteAmendment(taskId, requiredRound) {
|
|
2842
2932
|
const specPath = path7.join(taskDirFor(taskId), "spec.md");
|
|
2843
2933
|
let content;
|
|
2844
2934
|
try {
|
|
2845
|
-
content =
|
|
2935
|
+
content = fs8.readFileSync(specPath, "utf8");
|
|
2846
2936
|
} catch {
|
|
2847
2937
|
return { amended: false, reason: `spec.md missing at ${specPath}` };
|
|
2848
2938
|
}
|
|
2849
2939
|
if (requiredRound === 1) {
|
|
2850
|
-
if (
|
|
2940
|
+
if (HUMAN_AMENDMENT_HEADING_RE.test(content)) {
|
|
2851
2941
|
return { amended: true, reason: "" };
|
|
2852
2942
|
}
|
|
2853
2943
|
return {
|
|
@@ -2872,7 +2962,7 @@ function verifyRerouteAmendment(taskId, requiredRound) {
|
|
|
2872
2962
|
reason: `found \`## Amendment Round ${seenRound}\` in ${specPath}, expected \`## Amendment Round ${requiredRound}\``
|
|
2873
2963
|
};
|
|
2874
2964
|
}
|
|
2875
|
-
if (
|
|
2965
|
+
if (HUMAN_AMENDMENT_HEADING_RE.test(content)) {
|
|
2876
2966
|
return {
|
|
2877
2967
|
amended: false,
|
|
2878
2968
|
reason: `found \`## Amendment\` in ${specPath}, expected \`## Amendment Round ${requiredRound}\``
|
|
@@ -3017,7 +3107,7 @@ function classifyPreflightBlockers(taskId, changedFiles, bundleDiffIssues = [])
|
|
|
3017
3107
|
const handoffPath = path7.join(taskDirFor(taskId), "handoff.md");
|
|
3018
3108
|
const specPath = path7.join(taskDirFor(taskId), "spec.md");
|
|
3019
3109
|
try {
|
|
3020
|
-
const content =
|
|
3110
|
+
const content = fs8.readFileSync(handoffPath, "utf8");
|
|
3021
3111
|
const latestResults = computeLatestValidationResults(content);
|
|
3022
3112
|
const requiredChecks = parseValidationRequiredChecks(specPath);
|
|
3023
3113
|
const { malformed } = parseHandoffChangesRows(taskId);
|
|
@@ -3049,7 +3139,7 @@ function validateHandoffAgainstSpec(specPath, handoffPath, latestResults, change
|
|
|
3049
3139
|
rowMap = latestResults;
|
|
3050
3140
|
} else {
|
|
3051
3141
|
try {
|
|
3052
|
-
const content =
|
|
3142
|
+
const content = fs8.readFileSync(handoffPath, "utf8");
|
|
3053
3143
|
rowMap = computeLatestValidationResults(content);
|
|
3054
3144
|
} catch {
|
|
3055
3145
|
rowMap = /* @__PURE__ */ new Map();
|
|
@@ -3103,7 +3193,7 @@ function isTemplateUnfilled(content) {
|
|
|
3103
3193
|
function isDoneMdTemplate(donePath) {
|
|
3104
3194
|
let content;
|
|
3105
3195
|
try {
|
|
3106
|
-
content =
|
|
3196
|
+
content = fs8.readFileSync(donePath, "utf8");
|
|
3107
3197
|
} catch {
|
|
3108
3198
|
return true;
|
|
3109
3199
|
}
|
|
@@ -3112,7 +3202,7 @@ function isDoneMdTemplate(donePath) {
|
|
|
3112
3202
|
function isPrBodyTemplate(prBodyPath) {
|
|
3113
3203
|
let content;
|
|
3114
3204
|
try {
|
|
3115
|
-
content =
|
|
3205
|
+
content = fs8.readFileSync(prBodyPath, "utf8");
|
|
3116
3206
|
} catch {
|
|
3117
3207
|
return true;
|
|
3118
3208
|
}
|
|
@@ -3158,7 +3248,7 @@ function checkPhaseGate(taskId, phase, verdict, taskDirOverride) {
|
|
|
3158
3248
|
const artifactPath = path7.join(taskDir, config3.artifactName);
|
|
3159
3249
|
let content;
|
|
3160
3250
|
try {
|
|
3161
|
-
content =
|
|
3251
|
+
content = fs8.readFileSync(artifactPath, "utf8");
|
|
3162
3252
|
} catch {
|
|
3163
3253
|
return { ok: false, reason: `${config3.artifactName} is missing for phase '${phase}'` };
|
|
3164
3254
|
}
|
|
@@ -3170,7 +3260,7 @@ function checkPhaseGate(taskId, phase, verdict, taskDirOverride) {
|
|
|
3170
3260
|
if (phase === "spec_review" || phase === "plan") {
|
|
3171
3261
|
let statusRaw;
|
|
3172
3262
|
try {
|
|
3173
|
-
statusRaw =
|
|
3263
|
+
statusRaw = fs8.readFileSync(path7.join(taskDir, "status.json"), "utf8");
|
|
3174
3264
|
} catch {
|
|
3175
3265
|
return { ok: false, reason: `cannot determine reroute state for '${phase}': status.json in ${taskDir} is missing or unreadable` };
|
|
3176
3266
|
}
|
|
@@ -3208,7 +3298,7 @@ function checkPhaseGate(taskId, phase, verdict, taskDirOverride) {
|
|
|
3208
3298
|
const handoffPath = path7.join(taskDir, "handoff.md");
|
|
3209
3299
|
let handoffContent;
|
|
3210
3300
|
try {
|
|
3211
|
-
handoffContent =
|
|
3301
|
+
handoffContent = fs8.readFileSync(handoffPath, "utf8");
|
|
3212
3302
|
} catch {
|
|
3213
3303
|
return { ok: false, reason: `closing human_review requires a handoff.md \u2014 none found in ${taskDir}` };
|
|
3214
3304
|
}
|
|
@@ -3217,7 +3307,7 @@ function checkPhaseGate(taskId, phase, verdict, taskDirOverride) {
|
|
|
3217
3307
|
const donePath = path7.join(taskDir, "done.md");
|
|
3218
3308
|
let doneContent = "";
|
|
3219
3309
|
try {
|
|
3220
|
-
doneContent =
|
|
3310
|
+
doneContent = fs8.readFileSync(donePath, "utf8");
|
|
3221
3311
|
} catch {
|
|
3222
3312
|
}
|
|
3223
3313
|
if (hasHumanPendingWaiver(doneContent)) return { ok: true };
|
|
@@ -3238,7 +3328,7 @@ function parseHandoffChangesRows(taskId) {
|
|
|
3238
3328
|
const handoffPath = path7.join(taskDirFor(taskId), "handoff.md");
|
|
3239
3329
|
let content;
|
|
3240
3330
|
try {
|
|
3241
|
-
content =
|
|
3331
|
+
content = fs8.readFileSync(handoffPath, "utf8");
|
|
3242
3332
|
} catch {
|
|
3243
3333
|
return { files: [], malformed: [] };
|
|
3244
3334
|
}
|
|
@@ -3265,7 +3355,7 @@ function parseAffectedFilesFromSpec(taskId) {
|
|
|
3265
3355
|
const specPath = path7.join(taskDirFor(taskId), "spec.md");
|
|
3266
3356
|
let content;
|
|
3267
3357
|
try {
|
|
3268
|
-
content =
|
|
3358
|
+
content = fs8.readFileSync(specPath, "utf8");
|
|
3269
3359
|
} catch {
|
|
3270
3360
|
return { files: [], malformed: [] };
|
|
3271
3361
|
}
|
|
@@ -3567,6 +3657,24 @@ function verifyHandoffAgainstDiffFromData(taskIds, inputs) {
|
|
|
3567
3657
|
}
|
|
3568
3658
|
return issues;
|
|
3569
3659
|
}
|
|
3660
|
+
function buildAffectedFilesAllowlist(taskIds, options) {
|
|
3661
|
+
const paths = new Set(PIPELINE_TELEMETRY_FILES);
|
|
3662
|
+
const prefixes = [];
|
|
3663
|
+
for (const taskId of taskIds) {
|
|
3664
|
+
const parsed = parseAffectedFilesFromSpec(taskId);
|
|
3665
|
+
for (const filePath of parsed.files) {
|
|
3666
|
+
if (filePath.endsWith("/")) prefixes.push(filePath);
|
|
3667
|
+
else paths.add(filePath);
|
|
3668
|
+
}
|
|
3669
|
+
for (const malformed of parsed.malformed) {
|
|
3670
|
+
warn(`${taskId} spec.md Affected Files row malformed: ${malformed.reason}`);
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
if (options.admitManagedDocs) {
|
|
3674
|
+
for (const doc of PIPELINE_MANAGED_DOCS) paths.add(doc);
|
|
3675
|
+
}
|
|
3676
|
+
return { paths, prefixes };
|
|
3677
|
+
}
|
|
3570
3678
|
function verifyBaseDriftFromData(diffFiles, allowedPaths, taskIds, allowedPrefixes = []) {
|
|
3571
3679
|
const drift = [];
|
|
3572
3680
|
for (const filePath of diffFiles) {
|
|
@@ -3577,6 +3685,35 @@ function verifyBaseDriftFromData(diffFiles, allowedPaths, taskIds, allowedPrefix
|
|
|
3577
3685
|
}
|
|
3578
3686
|
return drift;
|
|
3579
3687
|
}
|
|
3688
|
+
function classifyBaseDriftFilesFromData(driftFiles, taskChangedFiles) {
|
|
3689
|
+
const baseAdvanced = [];
|
|
3690
|
+
const taskChangedOutOfScope = [];
|
|
3691
|
+
for (const filePath of driftFiles) {
|
|
3692
|
+
if (taskChangedFiles.has(filePath)) taskChangedOutOfScope.push(filePath);
|
|
3693
|
+
else baseAdvanced.push(filePath);
|
|
3694
|
+
}
|
|
3695
|
+
return { baseAdvanced, taskChangedOutOfScope };
|
|
3696
|
+
}
|
|
3697
|
+
function buildBaseDriftAbortMessage(baseBranch, classification) {
|
|
3698
|
+
const sections = ["--pr aborted: base-drift detected."];
|
|
3699
|
+
if (classification.baseAdvanced.length > 0) sections.push(
|
|
3700
|
+
`The base branch advanced. These files changed only on origin/${baseBranch}:
|
|
3701
|
+
` + classification.baseAdvanced.map((file) => ` ${file}`).join("\n") + `
|
|
3702
|
+
Merge or rebase the base branch into the task branch, then rerun:
|
|
3703
|
+
git fetch origin ${baseBranch} && git rebase origin/${baseBranch}
|
|
3704
|
+
(or: git merge origin/${baseBranch})`
|
|
3705
|
+
);
|
|
3706
|
+
if (classification.taskChangedOutOfScope.length > 0) sections.push(
|
|
3707
|
+
`These task-changed files are not in the spec's Affected Files:
|
|
3708
|
+
` + classification.taskChangedOutOfScope.map((file) => ` ${file}`).join("\n") + `
|
|
3709
|
+
The allowlist covers task artifacts, pipeline telemetry (PIPELINE_TELEMETRY_FILES), and spec Affected Files entries, including directory entries.
|
|
3710
|
+
Add legitimate paths to the spec's '### Affected Files' table (both sides of a rename), then rerun. If a sibling pipeline or stray task-branch commit introduced a file, remove it with:
|
|
3711
|
+
git checkout origin/${baseBranch} -- <path> && git commit -m 'revert drift on <path>'
|
|
3712
|
+
or: git revert <sha>`
|
|
3713
|
+
);
|
|
3714
|
+
sections.push(`Bypass with --force if you've verified the drift is intentional.`);
|
|
3715
|
+
return sections.join("\n\n");
|
|
3716
|
+
}
|
|
3580
3717
|
function parseDiffNameStatus(stdout) {
|
|
3581
3718
|
const diffFiles = [];
|
|
3582
3719
|
const renamePairs = [];
|
|
@@ -3608,7 +3745,7 @@ function verifyHandoffAgainstDiff(taskIds, baseRef) {
|
|
|
3608
3745
|
for (const taskId of taskIds) {
|
|
3609
3746
|
let content;
|
|
3610
3747
|
try {
|
|
3611
|
-
content =
|
|
3748
|
+
content = fs8.readFileSync(path7.join(taskDirFor(taskId), "handoff.md"), "utf8");
|
|
3612
3749
|
} catch {
|
|
3613
3750
|
continue;
|
|
3614
3751
|
}
|
|
@@ -3640,31 +3777,18 @@ function verifyBaseDrift(taskIds, baseBranch, cwd) {
|
|
|
3640
3777
|
if (!driftResult.ok) {
|
|
3641
3778
|
return { drift: [], fetchFailed: false, diffFailed: true, diffError: driftResult.stderr };
|
|
3642
3779
|
}
|
|
3643
|
-
|
|
3644
|
-
const allowedPrefixes = [];
|
|
3780
|
+
let admitManagedDocs = false;
|
|
3645
3781
|
for (const taskId of taskIds) {
|
|
3646
|
-
const parsed = parseAffectedFilesFromSpec(taskId);
|
|
3647
|
-
for (const filePath of parsed.files) {
|
|
3648
|
-
if (filePath.endsWith("/")) {
|
|
3649
|
-
allowedPrefixes.push(filePath);
|
|
3650
|
-
} else {
|
|
3651
|
-
allowedPaths.add(filePath);
|
|
3652
|
-
}
|
|
3653
|
-
}
|
|
3654
|
-
for (const malformed of parsed.malformed) {
|
|
3655
|
-
warn(`${taskId} spec.md Affected Files row malformed: ${malformed.reason}`);
|
|
3656
|
-
}
|
|
3657
3782
|
try {
|
|
3658
3783
|
if (readStatus(taskId).phases.qa?.status === "done") {
|
|
3659
|
-
|
|
3660
|
-
allowedPaths.add(doc);
|
|
3661
|
-
}
|
|
3784
|
+
admitManagedDocs = true;
|
|
3662
3785
|
}
|
|
3663
3786
|
} catch {
|
|
3664
3787
|
}
|
|
3665
3788
|
}
|
|
3789
|
+
const allowlist = buildAffectedFilesAllowlist(taskIds, { admitManagedDocs });
|
|
3666
3790
|
return {
|
|
3667
|
-
drift: verifyBaseDriftFromData(driftResult.files,
|
|
3791
|
+
drift: verifyBaseDriftFromData(driftResult.files, allowlist.paths, taskIds, allowlist.prefixes),
|
|
3668
3792
|
fetchFailed: false,
|
|
3669
3793
|
diffFailed: false
|
|
3670
3794
|
};
|
|
@@ -3682,7 +3806,7 @@ function verifyBaseDivergenceFromData(commits) {
|
|
|
3682
3806
|
function verifyBaseDivergence(baseBranch, cwd) {
|
|
3683
3807
|
const fetchResult = gitSafeAt(cwd, "fetch", "origin", baseBranch);
|
|
3684
3808
|
if (!fetchResult.ok) {
|
|
3685
|
-
if (!
|
|
3809
|
+
if (!fs8.existsSync(cwd)) {
|
|
3686
3810
|
return { commits: [], ok: false, stderr: fetchResult.stderr, fetchFailed: false };
|
|
3687
3811
|
}
|
|
3688
3812
|
warn(
|
|
@@ -3766,11 +3890,11 @@ function buildSharedDocAbortMessage(abortedFiles) {
|
|
|
3766
3890
|
}
|
|
3767
3891
|
|
|
3768
3892
|
// src/orchestrator/prompts/index.ts
|
|
3769
|
-
import
|
|
3893
|
+
import fs12 from "fs";
|
|
3770
3894
|
import path11 from "path";
|
|
3771
3895
|
|
|
3772
3896
|
// src/orchestrator/context.ts
|
|
3773
|
-
import
|
|
3897
|
+
import fs9 from "fs";
|
|
3774
3898
|
import path8 from "path";
|
|
3775
3899
|
function extractAffectedFiles(taskId) {
|
|
3776
3900
|
try {
|
|
@@ -3784,7 +3908,7 @@ function isSafeRepoPath(file) {
|
|
|
3784
3908
|
const resolved = path8.resolve(REPO_ROOT, file);
|
|
3785
3909
|
if (!resolved.startsWith(REPO_ROOT + path8.sep)) return false;
|
|
3786
3910
|
try {
|
|
3787
|
-
const real =
|
|
3911
|
+
const real = fs9.realpathSync(resolved);
|
|
3788
3912
|
if (!real.startsWith(REPO_ROOT + path8.sep)) return false;
|
|
3789
3913
|
} catch {
|
|
3790
3914
|
}
|
|
@@ -3798,7 +3922,7 @@ function buildContextBlock(taskIds) {
|
|
|
3798
3922
|
if (!isSafeRepoPath(file)) continue;
|
|
3799
3923
|
const filePath = path8.join(REPO_ROOT, file);
|
|
3800
3924
|
try {
|
|
3801
|
-
allFiles.set(file,
|
|
3925
|
+
allFiles.set(file, fs9.readFileSync(filePath, "utf8"));
|
|
3802
3926
|
} catch {
|
|
3803
3927
|
}
|
|
3804
3928
|
}
|
|
@@ -3829,7 +3953,7 @@ ${content}
|
|
|
3829
3953
|
function buildKnownPitfalls(taskIds) {
|
|
3830
3954
|
const patternsPath = process.env.CANON_PATTERNS_MD_PATH ?? path8.join(getActiveCwd(taskIds), "docs/patterns.md");
|
|
3831
3955
|
try {
|
|
3832
|
-
const content =
|
|
3956
|
+
const content = fs9.readFileSync(patternsPath, "utf8");
|
|
3833
3957
|
const match = content.match(/## Known Pitfalls\n\n([\s\S]*?)(?:\n## |\n---|\n# |$)/);
|
|
3834
3958
|
if (!match) return "";
|
|
3835
3959
|
return `
|
|
@@ -3846,7 +3970,7 @@ function buildKnownRisks(taskIds) {
|
|
|
3846
3970
|
const riskBlocks = taskIds.map((taskId) => {
|
|
3847
3971
|
const specPath = path8.join(taskDirFor(taskId), "spec.md");
|
|
3848
3972
|
try {
|
|
3849
|
-
const content =
|
|
3973
|
+
const content = fs9.readFileSync(specPath, "utf8");
|
|
3850
3974
|
const match = content.match(/## Known Risks\n\n([\s\S]*?)(?:\n## |\n# |$)/);
|
|
3851
3975
|
if (!match) return "";
|
|
3852
3976
|
const risks = match[1].trim();
|
|
@@ -3873,7 +3997,7 @@ function summarizePreloadStatus(taskIds) {
|
|
|
3873
3997
|
if (!isSafeRepoPath(file)) continue;
|
|
3874
3998
|
const filePath = path8.join(REPO_ROOT, file);
|
|
3875
3999
|
try {
|
|
3876
|
-
files.set(file,
|
|
4000
|
+
files.set(file, fs9.statSync(filePath).size);
|
|
3877
4001
|
} catch {
|
|
3878
4002
|
files.set(file, 0);
|
|
3879
4003
|
}
|
|
@@ -3890,7 +4014,7 @@ function summarizePreloadStatus(taskIds) {
|
|
|
3890
4014
|
function extractValidationChecks(taskId) {
|
|
3891
4015
|
const specPath = path8.join(taskDirFor(taskId), "spec.md");
|
|
3892
4016
|
try {
|
|
3893
|
-
const content =
|
|
4017
|
+
const content = fs9.readFileSync(specPath, "utf8");
|
|
3894
4018
|
const section = content.match(/## Validation Required\n\n([\s\S]*?)(?:\n## |\n# |$)/);
|
|
3895
4019
|
if (!section) return [];
|
|
3896
4020
|
const checks = [];
|
|
@@ -3906,7 +4030,7 @@ function extractValidationChecks(taskId) {
|
|
|
3906
4030
|
function extractAcSummary(taskId) {
|
|
3907
4031
|
const specPath = path8.join(taskDirFor(taskId), "spec.md");
|
|
3908
4032
|
try {
|
|
3909
|
-
const content =
|
|
4033
|
+
const content = fs9.readFileSync(specPath, "utf8");
|
|
3910
4034
|
const lines = [];
|
|
3911
4035
|
for (const line of content.split("\n")) {
|
|
3912
4036
|
const match = line.match(/^-\s+\[[ x]\]\s+(AC-[\w.-]+):\s+(.+)$/);
|
|
@@ -3996,11 +4120,11 @@ ${acSection}`;
|
|
|
3996
4120
|
}
|
|
3997
4121
|
|
|
3998
4122
|
// src/orchestrator/review-archive.ts
|
|
3999
|
-
import
|
|
4123
|
+
import fs11 from "fs";
|
|
4000
4124
|
import path10 from "path";
|
|
4001
4125
|
|
|
4002
4126
|
// src/task/templates.ts
|
|
4003
|
-
import
|
|
4127
|
+
import fs10 from "fs";
|
|
4004
4128
|
import path9 from "path";
|
|
4005
4129
|
function tasksRoot() {
|
|
4006
4130
|
return process.env.CANON_TASKS_DIR_OVERRIDE ?? "tasks";
|
|
@@ -4013,17 +4137,17 @@ function taskTemplateOverrideRoot() {
|
|
|
4013
4137
|
}
|
|
4014
4138
|
function resolveTaskTemplateSource(basename) {
|
|
4015
4139
|
const override = path9.join(taskTemplateOverrideRoot(), basename);
|
|
4016
|
-
if (
|
|
4140
|
+
if (fs10.existsSync(override)) return override;
|
|
4017
4141
|
const managed = path9.join(templatesRoot(), basename);
|
|
4018
|
-
return
|
|
4142
|
+
return fs10.existsSync(managed) ? managed : null;
|
|
4019
4143
|
}
|
|
4020
4144
|
function renderTaskTemplate(source, taskId, title) {
|
|
4021
|
-
return
|
|
4145
|
+
return fs10.readFileSync(source, "utf8").replaceAll("[TASK-ID]", taskId).replaceAll("[Title]", title);
|
|
4022
4146
|
}
|
|
4023
4147
|
function scaffoldTaskArtifact(taskDir, basename, taskId, title) {
|
|
4024
4148
|
const source = resolveTaskTemplateSource(basename);
|
|
4025
4149
|
if (!source) return null;
|
|
4026
|
-
|
|
4150
|
+
fs10.writeFileSync(path9.join(taskDir, basename), renderTaskTemplate(source, taskId, title), "utf8");
|
|
4027
4151
|
return source;
|
|
4028
4152
|
}
|
|
4029
4153
|
function isPristineTaskArtifact(content, basename, taskId, title) {
|
|
@@ -4039,10 +4163,79 @@ function isPristineTaskArtifact(content, basename, taskId, title) {
|
|
|
4039
4163
|
|
|
4040
4164
|
// src/orchestrator/review-archive.ts
|
|
4041
4165
|
var REVIEW_ARCHIVE_PREFIX = "review-prior-";
|
|
4166
|
+
var COLD_CODEX_ARCHIVE_PREFIX = "review-cold-codex-run-";
|
|
4167
|
+
var COLD_CODEX_ARCHIVE_RE = /^review-cold-codex-run-(\d+)\.md$/;
|
|
4168
|
+
function writeColdCodexArchive(taskDir, header, findings) {
|
|
4169
|
+
let newest = 0;
|
|
4170
|
+
for (const name2 of fs11.readdirSync(taskDir)) {
|
|
4171
|
+
const match = COLD_CODEX_ARCHIVE_RE.exec(name2);
|
|
4172
|
+
if (match) newest = Math.max(newest, Number(match[1]));
|
|
4173
|
+
}
|
|
4174
|
+
const name = `${COLD_CODEX_ARCHIVE_PREFIX}${newest + 1}.md`;
|
|
4175
|
+
const reason = header.reason.replaceAll('"', "'");
|
|
4176
|
+
fs11.writeFileSync(
|
|
4177
|
+
path10.join(taskDir, name),
|
|
4178
|
+
`<!-- round=${header.round} reviewed_sha=${header.reviewedSha} scope=${header.scope} base=${header.base} reason="${reason}" -->
|
|
4179
|
+
|
|
4180
|
+
${findings}`,
|
|
4181
|
+
"utf8"
|
|
4182
|
+
);
|
|
4183
|
+
return name;
|
|
4184
|
+
}
|
|
4185
|
+
function parseColdCodexArchiveHeader(content) {
|
|
4186
|
+
const match = /^<!-- round=(\d+) reviewed_sha=([a-f\d]{40,64}) scope=(full|delta) base=(\S+) reason="([^"\n]*)" -->\r?\n\r?\n/.exec(content);
|
|
4187
|
+
if (!match) return null;
|
|
4188
|
+
return {
|
|
4189
|
+
round: Number(match[1]),
|
|
4190
|
+
reviewedSha: match[2],
|
|
4191
|
+
scope: match[3],
|
|
4192
|
+
base: match[4],
|
|
4193
|
+
reason: match[5]
|
|
4194
|
+
};
|
|
4195
|
+
}
|
|
4196
|
+
function findColdCodexArchiveForRound(taskDir, round) {
|
|
4197
|
+
let names;
|
|
4198
|
+
try {
|
|
4199
|
+
names = fs11.readdirSync(taskDir);
|
|
4200
|
+
} catch {
|
|
4201
|
+
return null;
|
|
4202
|
+
}
|
|
4203
|
+
const numbered = names.flatMap((name) => {
|
|
4204
|
+
const match = COLD_CODEX_ARCHIVE_RE.exec(name);
|
|
4205
|
+
return match ? [{ name, number: Number(match[1]) }] : [];
|
|
4206
|
+
}).sort((a, b) => b.number - a.number);
|
|
4207
|
+
for (const { name } of numbered) {
|
|
4208
|
+
let content;
|
|
4209
|
+
try {
|
|
4210
|
+
content = fs11.readFileSync(path10.join(taskDir, name), "utf8");
|
|
4211
|
+
} catch {
|
|
4212
|
+
continue;
|
|
4213
|
+
}
|
|
4214
|
+
const header = parseColdCodexArchiveHeader(content);
|
|
4215
|
+
if (header?.round === round) return header;
|
|
4216
|
+
}
|
|
4217
|
+
return null;
|
|
4218
|
+
}
|
|
4219
|
+
function hasMalformedColdCodexArchive(taskDir) {
|
|
4220
|
+
let names;
|
|
4221
|
+
try {
|
|
4222
|
+
names = fs11.readdirSync(taskDir);
|
|
4223
|
+
} catch {
|
|
4224
|
+
return false;
|
|
4225
|
+
}
|
|
4226
|
+
return names.some((name) => {
|
|
4227
|
+
if (!COLD_CODEX_ARCHIVE_RE.test(name)) return false;
|
|
4228
|
+
try {
|
|
4229
|
+
return parseColdCodexArchiveHeader(fs11.readFileSync(path10.join(taskDir, name), "utf8")) === null;
|
|
4230
|
+
} catch {
|
|
4231
|
+
return true;
|
|
4232
|
+
}
|
|
4233
|
+
});
|
|
4234
|
+
}
|
|
4042
4235
|
var REVIEW_ARCHIVE_RE = new RegExp(`^${REVIEW_ARCHIVE_PREFIX}(\\d+)\\.md$`);
|
|
4043
4236
|
function newestReviewArchiveNumber(taskDir) {
|
|
4044
4237
|
let newest = 0;
|
|
4045
|
-
for (const name of
|
|
4238
|
+
for (const name of fs11.readdirSync(taskDir)) {
|
|
4046
4239
|
const match = REVIEW_ARCHIVE_RE.exec(name);
|
|
4047
4240
|
if (match) newest = Math.max(newest, Number(match[1]));
|
|
4048
4241
|
}
|
|
@@ -4054,21 +4247,21 @@ function findNewestReviewArchive(taskDir) {
|
|
|
4054
4247
|
}
|
|
4055
4248
|
function archivePriorReview(taskDir, options = {}) {
|
|
4056
4249
|
const reviewPath = path10.join(taskDir, "review.md");
|
|
4057
|
-
if (!
|
|
4250
|
+
if (!fs11.existsSync(reviewPath)) return null;
|
|
4058
4251
|
if (options.skipUnfilledTemplate) {
|
|
4059
|
-
const content =
|
|
4252
|
+
const content = fs11.readFileSync(reviewPath, "utf8");
|
|
4060
4253
|
if (isTemplateUnfilled(content)) return null;
|
|
4061
4254
|
if (options.scaffold && isPristineTaskArtifact(content, "review.md", options.scaffold.taskId, options.scaffold.title)) {
|
|
4062
4255
|
return null;
|
|
4063
4256
|
}
|
|
4064
4257
|
}
|
|
4065
4258
|
const archiveName = `${REVIEW_ARCHIVE_PREFIX}${newestReviewArchiveNumber(taskDir) + 1}.md`;
|
|
4066
|
-
|
|
4259
|
+
fs11.renameSync(reviewPath, path10.join(taskDir, archiveName));
|
|
4067
4260
|
return archiveName;
|
|
4068
4261
|
}
|
|
4069
4262
|
function rescaffoldReview(taskDir, identity) {
|
|
4070
4263
|
try {
|
|
4071
|
-
if (
|
|
4264
|
+
if (fs11.existsSync(path10.join(taskDir, "review.md"))) return { outcome: "exists" };
|
|
4072
4265
|
const source = scaffoldTaskArtifact(taskDir, "review.md", identity.taskId, identity.title);
|
|
4073
4266
|
return source ? { outcome: "written", source } : { outcome: "no-template" };
|
|
4074
4267
|
} catch (error) {
|
|
@@ -4542,10 +4735,10 @@ function renderTemplate(template, view) {
|
|
|
4542
4735
|
}
|
|
4543
4736
|
|
|
4544
4737
|
// src/orchestrator/prompts/templates/code-review-foreman.md
|
|
4545
|
-
var code_review_foreman_default = "You are the synthesis foreman for the code review phase for {{taskScope}} for {{projectName}}.\n\n{{{startup}}}\n\n## Code-Review Rules of Thumb (Foreman)\n\n- **Reviewer diffs against the task baseline, not `main`, on release branches**: on a shared release branch ahead of `main`, always diff against the task's baseline \u2014 diffing against `main` attributes unrelated work to the task.\n- **Use `git -C <absolute-path>` for every worktree git op, not `cd` + git**: when operating across REPO_ROOT and a task worktree, `git -C /absolute/path` avoids silent cwd reversion between tool calls.\n- **Don't infer one git invariant from another**: `git status --porcelain` empty \u2260 origin matches HEAD; `origin/<branch>` exists \u2260 origin matches HEAD; PR exists \u2260 PR is in the expected state. Do the actual check directly.\n- **A cross-cutting invariant belongs in one shared helper, not patched per call site**: when the same rule must hold at multiple enforcement points, implement it once. The tell: findings come back round after round as the same bug class at a new location. At \u22653 sites, extract the shared helper and route all sites through it.\n\nYour job is to synthesize three review inputs: the anchored Claude lens, the cold-Claude lens, and the pre-obtained cold-Codex findings injected below. You spawn the Claude lenses as isolated sub-agents, collect their findings, adjudicate all three inputs using the spec (which you hold and the cold lenses do not), then write one `review.md` and set the verdict. Do not run `codex` yourself.\n\nTasks:\n{{{taskLines}}}\n\n{{#isRound1}}\nThis is Round 1, the initial code review.\n{{/isRound1}}\n{{^isRound1}}\nThis is Round {{roundN}}: re-review after iteration {{priorIteration}}. The lenses re-run from scratch. Direct the anchored lens to read the Iteration {{priorIteration}} section of `handoff.md` that addresses review round {{priorIteration}}.\n{{#tightenLine}}\n{{{tightenLine}}}\n{{/tightenLine}}\n{{/isRound1}}\n\n{{#hasDiff}}\nTask diff against {{{baseBranch}}}:\n\n```diff\n{{{diffContent}}}\n```\n{{#diffTruncated}}\n> Diff truncated at 50 000 bytes. Give the Claude lenses the visible diff first; for the omitted remainder, direct them to inspect only the changed files named in the handoff Changes table. Do not give the cold-Claude lens spec, AC, or canon-doc context.\n{{/diffTruncated}}\n{{/hasDiff}}\n{{^hasDiff}}\nRetrieve the task diff with `git diff {{{baseBranch}}}...HEAD`.\n{{/hasDiff}}\n\n## Injected Cold-Codex Findings\n\n{{#hasColdCodexFindings}}\nThe orchestrator ran `codex review` over the task's branch diff before spawning you. Its findings are reproduced below. These are unanchored: Codex reviewed adversarially without the spec as a checklist. Treat them as the third lens input. Do not re-run Codex; synthesize these findings alongside the Claude lens outputs.\n\n{{{coldCodexFindings}}}\n{{/hasColdCodexFindings}}\n{{^hasColdCodexFindings}}\nNo cold-Codex findings were provided to this prompt. In production code_review, the orchestrator must obtain that artifact before foreman synthesis; do not treat a missing cold-Codex lens as approval evidence.\n{{/hasColdCodexFindings}}\n\n## Foreman Protocol\n\n### 1. Spawn Claude Lenses In Parallel\n\nSpawn both Claude lenses with the sub-agent tool (`Agent`, called `Task` in older harnesses) in a single message so they run concurrently, and run them **in the foreground** (`run_in_background: false`) so the call returns their findings. Do not spawn them in the background and do not end your turn to wait for them: a turn that ends while a lens is still running ends the code review with no `review.md` and no verdict, and the phase is retried from scratch. Your turn ends only after step 5 below has run.\n\n**Anchored lens** (`subagent_type: code-review-anchored`)\n- Give it the full diff, `spec.md`, `handoff.md`, and prior `review.md` if this is a re-review.\n- It applies canon's anchored Stage 1 / Stage 2 code-review charter.\n- It returns structured findings to you. It must not write `review.md` or run `canon task phase`.\n\n**Cold-Claude lens** (`subagent_type: code-review-cold`)\n- Give it the full diff and base ref only.\n- Do not give it `spec.md`, ACs, handoff rationale, canon docs, known risks, or your anchored-lens prompt.\n- If it needs to inspect files for truncated diff context, constrain it to changed files only and preserve the spec-blind framing.\n- It returns structured findings to you. It must not write `review.md` or run `canon task phase`.\n\nThe injected cold-Codex findings above are the third lens input. Do not spawn a Codex agent or shell out to Codex yourself. Do not let a Claude lens see another lens's output.\n\n### 2. Adjudicate\n\nUse the three lens inputs and the spec. Do not perform a new full diff review for novel bugs; your role is synthesis and adjudication.\n\nThe lenses are instructed to over-report \u2014 to surface low-confidence and low-severity findings rather than self-censor. Filtering is **your** job, not theirs: a quiet lens output is a bug in the lens, not a clean diff. Rank surviving findings by confidence \xD7 severity. A low-confidence, low-severity finding is a nit or gets dismissed; it does not by itself drive `changes_requested`. Do not discard a finding merely because a lens marked it low-confidence \u2014 verify it against the spec/diff first, then rank.\n\n1. Dedup: if 2+ lenses flagged the same behavior, collapse it to one finding and record \"flagged by N lenses.\" A finding flagged by 2+ lenses is higher-confidence regardless of any lens's self-tag. Cross-model agreement \u2014 the same behavior flagged by cold-Claude and cold-Codex \u2014 must not be dismissed as spec-intended without explicit spec evidence cited in `review.md`.\n2. Keep the two reconciliation checks separate:\n - Does it hold against the code? For cold findings (cold-Claude and cold-Codex), verify each against the diff/code. Codex P-levels are claims to check, not verdicts. A finding that does not hold gets recorded as `Dismissed (cold-Claude): <finding> - <reason>` or `Dismissed (cold-Codex): <finding> - <reason>`.\n - Is it in spec scope? Apply this only to anchored-lens findings as part of the Stage 1 / Stage 2 charter.\n - Forbidden: do not dismiss a verified cold-Claude or cold-Codex finding merely for being off-AC or out of spec scope. A real bug caught by a cold lens is still a bug even if no AC named it.\n3. Altitude classification: every surviving finding is either:\n - `code-bug`: the implementation is wrong or test integrity is compromised.\n - `spec-gap`: the implementation may match the written spec, but the spec is missing, wrong, or too ambiguous for the implementer to fix.\n\n### 3. Choose Verdict\n\n- Any `code-bug` finding -> `changes_requested`.\n- Any `spec-gap` finding and no code-bugs -> `spec_gap`.\n- Optional nits or cleanup without blocking findings -> `approved_with_nits`.\n- No surviving findings -> `approved`.\n\nTest-integrity findings are always code-bugs.\n\n### 4. Write `review.md`\n\nFor each task, write `tasks/<id>/review.md`.\n\nRound 1 fills the existing template structure directly \u2014 do **not** wrap it in a `## Round 1` section; the `## Stage 1` and `## Stage 2` headings stay at H2. Re-review appends a new `## Round {{roundN}}` section near the bottom (with `### Stage 1` / `### Stage 2` sub-headings), preserving earlier rounds.\n\nInclude:\n- Stage 1: anchored lens validation gate result and AC table.\n- Stage 2 / Findings: surviving findings with altitude (`code-bug` or `spec-gap`), source lens, and file:line.\n- Dismissed Cold Findings: every dropped cold finding plus the reason, including `Dismissed (cold-Claude): ...` and `Dismissed (cold-Codex): ...` entries where applicable.\n- Final Verdict: check exactly one verdict checkbox, including `Spec gap` when applicable.\n\n### 5. Set Phase Verdict\n\nRun one command per task with the actual verdict:\n{{{phaseCommands}}}\n";
|
|
4738
|
+
var code_review_foreman_default = "You are the synthesis foreman for the code review phase for {{taskScope}} for {{projectName}}.\n\n{{{startup}}}\n\n## Code-Review Rules of Thumb (Foreman)\n\n- **Reviewer diffs against the task baseline, not `main`, on release branches**: on a shared release branch ahead of `main`, always diff against the task's baseline \u2014 diffing against `main` attributes unrelated work to the task.\n- **Use `git -C <absolute-path>` for every worktree git op, not `cd` + git**: when operating across REPO_ROOT and a task worktree, `git -C /absolute/path` avoids silent cwd reversion between tool calls.\n- **Don't infer one git invariant from another**: `git status --porcelain` empty \u2260 origin matches HEAD; `origin/<branch>` exists \u2260 origin matches HEAD; PR exists \u2260 PR is in the expected state. Do the actual check directly.\n- **A cross-cutting invariant belongs in one shared helper, not patched per call site**: when the same rule must hold at multiple enforcement points, implement it once. The tell: findings come back round after round as the same bug class at a new location. At \u22653 sites, extract the shared helper and route all sites through it.\n\nYour job is to synthesize three review inputs: the anchored Claude lens, the cold-Claude lens, and the pre-obtained cold-Codex findings injected below. You spawn the Claude lenses as isolated sub-agents, collect their findings, adjudicate all three inputs using the spec (which you hold and the cold lenses do not), then write one `review.md` and set the verdict. Do not run `codex` yourself.\n\nTasks:\n{{{taskLines}}}\n\n{{#isRound1}}\nThis is Round 1, the initial code review.\n{{/isRound1}}\n{{^isRound1}}\nThis is Round {{roundN}}: re-review after iteration {{priorIteration}}. All three lenses run again this round. Direct the anchored lens to read the Iteration {{priorIteration}} section of `handoff.md` that addresses review round {{priorIteration}}.\n{{#tightenLine}}\n{{{tightenLine}}}\n{{/tightenLine}}\n{{/isRound1}}\n\n**Scope:** {{{scopeWord}}} \u2014 base `{{{scopeBase}}}` \u2014 reason: {{{scopeReason}}}\n\n{{#hasDiff}}\nTask diff against {{{baseBranch}}}:\n\n```diff\n{{{diffContent}}}\n```\n{{#diffTruncated}}\n> Diff truncated at 50 000 bytes. Give the Claude lenses the visible diff first; for the omitted remainder, direct them to inspect only the changed files named in the handoff Changes table. Do not give the cold-Claude lens spec, AC, or canon-doc context.\n{{/diffTruncated}}\n{{/hasDiff}}\n{{^hasDiff}}\nRetrieve the task diff with `git diff {{{baseBranch}}}...HEAD`.\n{{/hasDiff}}\n{{#isDeltaScope}}\n\n{{#hasDeltaDiff}}\nDelta diff (`{{{deltaBase}}}..HEAD`) \u2014 give this delta to the cold-Claude lens:\n\n```diff\n{{{deltaDiffContent}}}\n```\n{{#deltaDiffTruncated}}\n> Delta diff truncated at 50 000 bytes. Give the cold-Claude lens the visible delta first; for the remainder, direct it to `git diff {{{deltaBase}}}..HEAD`.\n{{/deltaDiffTruncated}}\n{{/hasDeltaDiff}}\n{{^hasDeltaDiff}}\nThe delta diff could not be injected. Retrieve it with `git diff {{{deltaBase}}}..HEAD` and give it to the cold-Claude lens before it reviews.\n{{/hasDeltaDiff}}\n{{/isDeltaScope}}\n\n## Injected Cold-Codex Findings\n\n{{#hasColdCodexFindings}}\nThe orchestrator ran `codex review` before spawning you. Its findings are reproduced below. These are unanchored: Codex reviewed adversarially without the spec as a checklist. Treat them as the third lens input. Do not re-run Codex; synthesize these findings alongside the Claude lens outputs.\n{{#isDeltaScope}}\nFor this round, cold-Codex reviewed only `{{{deltaBase}}}..HEAD`.\n{{/isDeltaScope}}\n{{^isDeltaScope}}\nFor this round, cold-Codex reviewed the full task diff against `{{{baseBranch}}}`.\n{{/isDeltaScope}}\n\n{{{coldCodexFindings}}}\n{{/hasColdCodexFindings}}\n{{^hasColdCodexFindings}}\nNo cold-Codex findings were provided to this prompt. In production code_review, the orchestrator must obtain that artifact before foreman synthesis; do not treat a missing cold-Codex lens as approval evidence.\n{{/hasColdCodexFindings}}\n\n{{#hasOutOfScopeFiles}}\n## Full-Send Scope Judgment\n\nThe task changed these files outside the spec's `### Affected Files` table:\n\n{{{outOfScopeFilesList}}}\n\nJudge each file against the owning task's ACs and record its outcome and reason in `review.md`:\n\n1. **Appropriate scope expansion:** If needed to meet the ACs and consistent with the spec's intent, append an `## Amendment (full-send scope)` section to the owning task's `spec.md` (use exactly that heading; a bare `## Amendment` is reserved for human reroute amendments). Include an exact `### Affected Files` heading and table row naming the file, plus a one-line reason that calls it a full-send scope expansion.\n2. **Spec miss:** If the spec should have listed the file, append the same `## Amendment (full-send scope)` section and exact `### Affected Files` table row, with a one-line reason that calls it a spec miss.\n3. **Should not have been changed:** Do not amend the spec. Write a finding requiring the file to be reverted or removed, and set the verdict to `changes_requested`.\n\nFor a bundle, amend the member spec whose ACs the file serves. The orchestrator checks after code review completes: every listed file must be in a member's Affected Files or a `changes_requested` verdict must route it back to implement. The legacy `needs_re_review` verdict routes the same way. A `spec_gap` verdict blocks for human triage and names any files still outside Affected Files; blessing that verdict does not amend the spec. An approved review with unjudged files auto-blocks.\n\n{{/hasOutOfScopeFiles}}\n## Foreman Protocol\n\n### 1. Spawn Claude Lenses In Parallel\n\nSpawn both Claude lenses with the sub-agent tool (`Agent`, called `Task` in older harnesses) in a single message so they run concurrently, and run them **in the foreground** (`run_in_background: false`) so the call returns their findings. Do not spawn them in the background and do not end your turn to wait for them: a turn that ends while a lens is still running ends the code review with no `review.md` and no verdict, and the phase is retried from scratch. Your turn ends only after step 5 below has run.\n\n**Anchored lens** (`subagent_type: code-review-anchored`)\n{{#isDeltaScope}}\n- Retrieve the full task diff with `git diff {{{baseBranch}}}...HEAD` and give it that full diff, not the delta, plus `spec.md`, `handoff.md`, and prior `review.md`.\n{{/isDeltaScope}}\n{{^isDeltaScope}}\n- Give it the full diff, `spec.md`, `handoff.md`, and prior `review.md` if this is a re-review.\n{{/isDeltaScope}}\n- It applies canon's anchored Stage 1 / Stage 2 code-review charter.\n- It returns structured findings to you. It must not write `review.md` or run `canon task phase`.\n\n**Cold-Claude lens** (`subagent_type: code-review-cold`)\n{{#isDeltaScope}}\n- This round is delta-scoped. Give it the delta (`{{{deltaBase}}}..HEAD`) and the delta base ref only.\n{{/isDeltaScope}}\n{{^isDeltaScope}}\n- Give it the full diff and base ref only.\n{{/isDeltaScope}}\n- Do not give it `spec.md`, ACs, handoff rationale, canon docs, known risks, or your anchored-lens prompt.\n- If it needs to inspect files for truncated diff context, constrain it to changed files only and preserve the spec-blind framing.\n- Separately, its sibling-site sweep may search other code files for guards, checks, and invariants that need the same treatment. Keep the search spec-blind.\n- It returns structured findings to you. It must not write `review.md` or run `canon task phase`.\n\nThe injected cold-Codex findings above are the third lens input. Do not spawn a Codex agent or shell out to Codex yourself. Do not let a Claude lens see another lens's output.\n\nSpawn exactly the two Claude lenses above each round, plus at most one re-spawn of a lens whose return is missing. Spawn no other reviewers or review agents, run no review skills, and never substitute your own review for a lens's return.\n\nA lens return is valid when it follows that lens's charter return format. This includes a clean empty form (`STAGE_2_FINDINGS: (none)` or `COLD_FINDINGS: (none)`) and the anchored Stage 1 fail form (`STAGE_1: fail`, no Stage 2 findings, `OVERALL_SIGNAL: changes_requested`). A return is missing if absent or outside the charter format. Re-spawn a missing lens once with the same inputs. If still missing, do not approve or guess at its findings: it is not approval evidence. Write `review.md` recording which lens was missing and what came back, check no verdict box, then run the `code_review blocked` commands in step 5 for every task. Other lenses' findings may be recorded but not adjudicated into a verdict.\n\n### 2. Adjudicate\n\nUse the three lens inputs and the spec. Do not perform a new full diff review for novel bugs; your role is synthesis and adjudication.\n\nThe lenses are instructed to over-report \u2014 to surface low-confidence and low-severity findings rather than self-censor. Filtering is **your** job, not theirs. A clean return in the charter's empty form is a valid result; only a missing return (step 1) is a lens failure. Rank surviving findings by confidence \xD7 severity. A low-confidence, low-severity finding is a nit or gets dismissed; it does not by itself drive `changes_requested`. Do not discard a finding merely because a lens marked it low-confidence \u2014 verify it against the spec/diff first, then rank.\n\n1. Dedup: if 2+ lenses flagged the same behavior, collapse it to one finding and record \"flagged by N lenses.\" A finding flagged by 2+ lenses is higher-confidence regardless of any lens's self-tag. Cross-model agreement \u2014 the same behavior flagged by cold-Claude and cold-Codex \u2014 must not be dismissed as spec-intended without explicit spec evidence cited in `review.md`.\n2. Keep the two reconciliation checks separate:\n - Does it hold against the code? For cold findings (cold-Claude and cold-Codex), verify each against the diff/code. Codex P-levels are claims to check, not verdicts. A finding that does not hold gets recorded as `Dismissed (cold-Claude): <finding> - <reason>` or `Dismissed (cold-Codex): <finding> - <reason>`.\n - Is it in spec scope? Apply this only to anchored-lens findings as part of the Stage 1 / Stage 2 charter.\n - Forbidden: do not dismiss a verified cold-Claude or cold-Codex finding merely for being off-AC or out of spec scope. A real bug caught by a cold lens is still a bug even if no AC named it.\n3. Altitude classification: every surviving finding is either:\n - `code-bug`: the implementation is wrong or test integrity is compromised.\n - `spec-gap`: the implementation may match the written spec, but the spec is missing, wrong, or too ambiguous for the implementer to fix.\n\n### 3. Choose Verdict\n\n- Any `code-bug` finding -> `changes_requested`.\n- Any `spec-gap` finding and no code-bugs -> `spec_gap`.\n- Optional nits or cleanup without blocking findings -> `approved_with_nits`.\n- No surviving findings -> `approved`.\n\nTest-integrity findings are always code-bugs.\n\n### 4. Write `review.md`\n\nFor each task, write `tasks/<id>/review.md`.\n\nRound 1 fills the existing template structure directly \u2014 do **not** wrap it in a `## Round 1` section; the `## Stage 1` and `## Stage 2` headings stay at H2. Re-review appends a new `## Round {{roundN}}` section near the bottom (with `### Stage 1` / `### Stage 2` sub-headings), preserving earlier rounds.\n\nEvery round \u2014 the Round 1 body and each `## Round N` section \u2014 includes one scope line near the top: **Scope:** {{{scopeWord}}} \u2014 base `{{{scopeBase}}}` \u2014 reason: {{{scopeReason}}}. The numbered cold-Codex archive header is the machine-readable record.\n\nInclude:\n- Stage 1: anchored lens validation gate result and AC table.\n- Stage 2 / Findings: surviving findings with altitude (`code-bug` or `spec-gap`), source lens, and file:line.\n- Dismissed Cold Findings: every dropped cold finding plus the reason, including `Dismissed (cold-Claude): ...` and `Dismissed (cold-Codex): ...` entries where applicable.\n- Final Verdict: check exactly one verdict checkbox, including `Spec gap` when applicable (except the missing-lens stop in step 1, which checks none).\n\n### 5. Set Phase Verdict\n\nRun one command per task with the actual verdict:\n{{{phaseCommands}}}\n\nIf a lens was still missing after its re-spawn (step 1), write `review.md` first, then run these instead, with no verdict \u2014 this stops the run for the human:\n{{{blockedCommands}}}\n";
|
|
4546
4739
|
|
|
4547
4740
|
// src/orchestrator/prompts/templates/implement.md
|
|
4548
|
-
var implement_default = 'You are implementing {{taskScope}} for {{projectName}}.\n\n{{{stateHeader}}}\n{{{startup}}}\n{{{risksBlock}}}{{{pitfallsBlock}}}{{{contextBlock}}}\n{{{affectedFilesBlock}}}\nTasks to implement:\n{{{taskLines}}}{{#isBundle}}\nThese tasks are related \u2014 implement them together. Consider shared code paths and cross-task interactions.{{/isBundle}}\n\nGrounding rule: before you write handoff.md, re-open the files you changed and verify the current diff against the spec. Do not treat a previous session\'s memory as proof that the work is already in place.\n\n**Spec ACs are binding. Plan approach is guidance.**\n- Every Acceptance Criterion in spec.md MUST be met \u2014 these are non-negotiable.\n- If you find a better implementation approach than what\'s in the plan, use it. Document every deviation in handoff.md under "Deviations" with specific rationale.\n- You may NOT silently drop an AC, skip a required validation check, or omit a spec requirement.\n- If an AC is infeasible as written, document it in Blockers \u2014 do not silently skip.\n- If an AC is ambiguous enough that two reasonable implementations exist, document your interpretation in handoff.md under Blockers with label `[ambiguity]` \u2014 do not silently guess. Claude will evaluate whether the interpretation was correct.\n\n## Implementation Rules\n\n**Safe-First Rules** \u2014 always applicable regardless of stack:\n1. For storage, reload, sync, or data-affecting flows: ship the safer guarded behavior first.\n2. Behavior that reloads the app, replaces local state, or dismisses user work must be gated by explicit user action.\n3. Prefer shared types over duplicating signatures.\n\n**Scope Discipline** \u2014 always applicable; the spec is the contract:\n1. **Affected Files is the scope cap.** If satisfying an AC genuinely requires editing files outside the spec\'s *Affected Files* table, do not make that edit. Record the gap in `handoff.md` under *Blockers* \u2014 the handoff is how it reaches a human \u2014 then finish the remaining in-scope work, the handoff, and the phase command. Do not silently expand scope.\n2. **No unauthorized new abstractions.** Do not introduce new top-level modules, services, packages, or routing layers that the spec did not authorize. Minor refactors within an authorized file are fine; new abstractions are an architecture decision and belong in the spec.\n3. **No incidental dependency changes.** Do not add, remove, upgrade, or downgrade dependencies (or their pinned versions) unless the spec explicitly requests it.\n\n**Lint & Type Safety Policy** \u2014 always applicable:\n1. **Suppressing a lint or type error is a last resort**, not a convenience escape hatch. Never add a suppression without a same-line justification explaining *why the rule is wrong for this specific case*.\n2. **`any` / dynamic typing**: When the shape is truly unknown at the boundary, type as `unknown` and narrow explicitly.\n\n**Bug/Flake-Fix Red-First Checkpoint** \u2014 applicable when a spec\'s ACs include a red-first regression test:\nWrite the test and run it against the pre-fix code first; confirm it fails *for the reason the spec states*, then apply the fix and confirm it passes. Report the red run (command + observed failure) in handoff.md. If the test cannot be made to fail on the pre-fix code for the stated reason, do not implement a fix on a premise you could not reproduce \u2014 record a `[wrong-premise]` Blocker in handoff.md instead, then finish the remaining in-scope work, the handoff, and the phase command. If the spec instead uses the environment-bound-and-impractical escape, run its named deterministic alternative before fixing if it is executable in your sandbox (e.g. an integration fixture) and report the outcome in handoff.md the same way; if it is not executable (e.g. a documented manual repro), state that in handoff.md instead \u2014 never report an outcome for a run that did not happen.\n\n**Parsing Structured Input** \u2014 always applicable when implementing a parser for author-facing structured input:\nParse cell-by-cell with explicit rejection, not a permissive whole-string regex. Anchor each cell to exactly one expected shape and reject malformed cells with a specific reason at the parse boundary.\n\nRun ALL applicable validation checks before writing handoff. See "Validation Required" in each spec.md. The universal change-type \u2192 check-category matrix:\n\n| Change Type | Required Check Categories |\n|---|---|\n| Most changes | Linting, type checking, unit tests |\n| Docs references | Docs references |\n| Routes / config / build | Full build |\n| UI / interaction changes | End-to-end tests |\n| Content / SEO / metadata | Prerender / sitemap / feed regeneration |\n| Schema / migration | Migration runner + manual review |\n| Cross-platform | Subset of the above on each platform |\n\nFor which command runs each category: see `docs/architecture.md` \xA7Validation (project command bindings). Required checks must be recorded as Pass or Fail; do not mark a required check N/A unless the spec explicitly removed it.\n\n**Test flakiness in your sandbox.** Validation suites \u2014 especially E2E or integration tests \u2014 can hit transient failures (timing races, environment quirks, network jitter) that have nothing to do with the code in your spec\'s Affected Files. **If a failure is in a test / file outside your Affected Files table, do NOT fix it.** Note the observed test name, file, line, and a one-line repro hint in handoff.md \u2192 Blockers (or "Validation Outcomes" Notes column with status `Fail \u2013 unrelated`), then continue. `Fail \u2013 unrelated` is only valid for failures in files outside your Affected Files; a failure in a file you changed is yours to fix. Scope discipline > fixing adjacent bugs you spot during validation. The reviewer/operator will decide whether to triage the unrelated failure separately.\n\nFor each task, write tasks/<id>/handoff.md using the template. The Validation Outcomes table must have no Fail results EXCEPT for unrelated-flake rows clearly labeled in the Notes column.\nAppend to tasks/<id>/notes.md for any surprising codebase behavior (prefix: [implement]).\n\nWhen done, run:\n{{{phaseCommands}}}\n';
|
|
4741
|
+
var implement_default = 'You are implementing {{taskScope}} for {{projectName}}.\n\n{{{stateHeader}}}\n{{{startup}}}\n{{{risksBlock}}}{{{pitfallsBlock}}}{{{contextBlock}}}\n{{{affectedFilesBlock}}}\nTasks to implement:\n{{{taskLines}}}{{#isBundle}}\nThese tasks are related \u2014 implement them together. Consider shared code paths and cross-task interactions.{{/isBundle}}\n\nGrounding rule: before you write handoff.md, re-open the files you changed and verify the current diff against the spec. Do not treat a previous session\'s memory as proof that the work is already in place.\n\n**Spec ACs are binding. Plan approach is guidance.**\n- Every Acceptance Criterion in spec.md MUST be met \u2014 these are non-negotiable.\n- If you find a better implementation approach than what\'s in the plan, use it. Document every deviation in handoff.md under "Deviations" with specific rationale.\n- You may NOT silently drop an AC, skip a required validation check, or omit a spec requirement.\n- If an AC is infeasible as written, document it in Blockers \u2014 do not silently skip.\n- If an AC is ambiguous enough that two reasonable implementations exist, document your interpretation in handoff.md under Blockers with label `[ambiguity]` \u2014 do not silently guess. Claude will evaluate whether the interpretation was correct.\n\n## Implementation Rules\n\n**Safe-First Rules** \u2014 always applicable regardless of stack:\n1. For storage, reload, sync, or data-affecting flows: ship the safer guarded behavior first.\n2. Behavior that reloads the app, replaces local state, or dismisses user work must be gated by explicit user action.\n3. Prefer shared types over duplicating signatures.\n\n**Scope Discipline** \u2014 always applicable; the spec is the contract:\n1. **Affected Files is the scope cap.** If satisfying an AC genuinely requires editing files outside the spec\'s *Affected Files* table, do not make that edit. Record the gap in `handoff.md` under *Blockers* \u2014 the handoff is how it reaches a human \u2014 then finish the remaining in-scope work, the handoff, and the phase command. Do not silently expand scope. New helpers, fixtures, and test files introduced during implementation count against this cap too.\n2. **No unauthorized new abstractions.** Do not introduce new top-level modules, services, packages, or routing layers that the spec did not authorize. Minor refactors within an authorized file are fine; new abstractions are an architecture decision and belong in the spec.\n3. **No incidental dependency changes.** Do not add, remove, upgrade, or downgrade dependencies (or their pinned versions) unless the spec explicitly requests it.\n\n**Lint & Type Safety Policy** \u2014 always applicable:\n1. **Suppressing a lint or type error is a last resort**, not a convenience escape hatch. Never add a suppression without a same-line justification explaining *why the rule is wrong for this specific case*.\n2. **`any` / dynamic typing**: When the shape is truly unknown at the boundary, type as `unknown` and narrow explicitly.\n\n**Bug/Flake-Fix Red-First Checkpoint** \u2014 applicable when a spec\'s ACs include a red-first regression test:\nWrite the test and run it against the pre-fix code first; confirm it fails *for the reason the spec states*, then apply the fix and confirm it passes. Report the red run (command + observed failure) in handoff.md. If the test cannot be made to fail on the pre-fix code for the stated reason, do not implement a fix on a premise you could not reproduce \u2014 record a `[wrong-premise]` Blocker in handoff.md instead, then finish the remaining in-scope work, the handoff, and the phase command. If the spec instead uses the environment-bound-and-impractical escape, run its named deterministic alternative before fixing if it is executable in your sandbox (e.g. an integration fixture) and report the outcome in handoff.md the same way; if it is not executable (e.g. a documented manual repro), state that in handoff.md instead \u2014 never report an outcome for a run that did not happen.\n\n**Parsing Structured Input** \u2014 always applicable when implementing a parser for author-facing structured input:\nParse cell-by-cell with explicit rejection, not a permissive whole-string regex. Anchor each cell to exactly one expected shape and reject malformed cells with a specific reason at the parse boundary.\n\nRun ALL applicable validation checks before writing handoff. See "Validation Required" in each spec.md. The universal change-type \u2192 check-category matrix:\n\n| Change Type | Required Check Categories |\n|---|---|\n| Most changes | Linting, type checking, unit tests |\n| Docs references | Docs references |\n| Routes / config / build | Full build |\n| UI / interaction changes | End-to-end tests |\n| Content / SEO / metadata | Prerender / sitemap / feed regeneration |\n| Schema / migration | Migration runner + manual review |\n| Cross-platform | Subset of the above on each platform |\n\nFor which command runs each category: see `docs/architecture.md` \xA7Validation (project command bindings). Required checks must be recorded as Pass or Fail; do not mark a required check N/A unless the spec explicitly removed it.\n\n**Test flakiness in your sandbox.** Validation suites \u2014 especially E2E or integration tests \u2014 can hit transient failures (timing races, environment quirks, network jitter) that have nothing to do with the code in your spec\'s Affected Files. **If a failure is in a test / file outside your Affected Files table, do NOT fix it.** Note the observed test name, file, line, and a one-line repro hint in handoff.md \u2192 Blockers (or "Validation Outcomes" Notes column with status `Fail \u2013 unrelated`), then continue. `Fail \u2013 unrelated` is only valid for failures in files outside your Affected Files; a failure in a file you changed is yours to fix. Scope discipline > fixing adjacent bugs you spot during validation. The reviewer/operator will decide whether to triage the unrelated failure separately.\n\nFor each task, write tasks/<id>/handoff.md using the template. The Validation Outcomes table must have no Fail results EXCEPT for unrelated-flake rows clearly labeled in the Notes column.\nAppend to tasks/<id>/notes.md for any surprising codebase behavior (prefix: [implement]).\n\nWhen done, run:\n{{{phaseCommands}}}\n';
|
|
4549
4742
|
|
|
4550
4743
|
// src/orchestrator/prompts/templates/implement-reroute.md
|
|
4551
4744
|
var implement_reroute_default = "A human wrote a spec amendment and rerouted {{taskScope}} for {{projectName}}.\n\n{{{stateHeader}}}\n{{{roundBanner}}}{{{preamble}}}\n\n{{#startup}}{{{startup}}}\n{{/startup}}{{{risksBlock}}}{{{pitfallsBlock}}}{{{contextBlock}}}\n{{{affectedFilesBlock}}}\nTasks with amended specs:\n{{{taskLines}}}\n\n{{{groundingRule}}}\n\n**How to approach this:**\n0. If a task's line above marks it EXEMPT, skip steps 1-2 for that task entirely \u2014 its spec has NO Amendment section and its plan has NO Reroute Plan section, by design. For exempt tasks, follow the task-specific line above: approved siblings only need shared-behavior re-verification, while siblings with prior review findings must still address those findings.\n1. For each task above, read `tasks/<id>/spec.md` from your current working directory (the worktree). REPO_ROOT's copy is the pre-implement scaffold and does NOT contain operator amendments. Locate the exact heading named in its entry \u2014 `## Amendment` for round 1, or `## Amendment Round N` for round 2+. Each task carries its own reroute round (bundles may mix rounds), so use the heading specified in that task's line, not a bundle-wide assumption. Treat that section's content as the new requirements; ignore prior-round sections when implementing this one.\n2. Check `tasks/<id>/plan.md` for `## Reroute Plan` (round 1) or `## Reroute Plan Round N` (N = that task's reroute round). If present, use that section as the delta guide. If absent (fast-tier reroute with no conversational reroute plan), read the base plan for orientation.\n3. Read tasks/<id>/handoff.md to understand what you previously shipped. Do NOT assume the handoff covers the amendment \u2014 it was written before the amendment existed.\n4. Identify the delta: which ACs are new, which changed, which were already addressed by the previous implementation.\n5. Implement the delta. Previously-correct work stays; only change what the amendment requires. If the amendment conflicts with a prior AC, the amendment wins.\n6. Re-run ALL applicable validation checks (lint, type-check, test, build, e2e as applicable per the spec's Validation Required). Required checks must be recorded as Pass or Fail; do not mark a required check N/A.\n7. **Rewrite handoff.md** to reflect the complete current state of the implementation \u2014 including the round-1 work that still applies plus the new amendment work. The reviewer reads handoff.md as the single source of truth, not your prior session's context.\n\n**Spec ACs are binding** \u2014 including both original ACs and amendment ACs. If you think an amendment AC is infeasible as written, document it under Blockers in handoff.md. Do not silently drop any AC.\n\n**Bug/flake-fix red-first checkpoint** \u2014 if the amendment adds or modifies a red-first regression-test AC: write and run the test against the task's current state **before** making this round's fix edits, and confirm it fails *for the reason the spec states*; then apply the fix and confirm it passes. Report the red run in handoff.md. If it cannot be made to fail for that reason, record a `[wrong-premise]` Blocker instead of implementing on an unconfirmed premise. If the amendment instead relies on the environment-bound-and-impractical escape, run its named deterministic alternative if it is executable in your sandbox and report the outcome in handoff.md; if it is not executable (e.g. a documented manual repro), state that instead \u2014 never report an outcome for a run that did not happen.\n\nAppend to tasks/<id>/notes.md for any surprising behavior found while re-reading the codebase (prefix: `[implement-reroute]`).\n\nWhen done, run:\n{{{phaseCommands}}}\n";
|
|
@@ -4554,10 +4747,10 @@ var implement_reroute_default = "A human wrote a spec amendment and rerouted {{t
|
|
|
4554
4747
|
var implement_revisions_default = '{{{iterBanner}}}\n\n{{{stateHeader}}}\n{{{startup}}}\n\n{{{affectedFilesBlock}}}\n\n{{#hasReviewFindings}}\nYour prior iteration shipped; the reviewer (Claude) appended findings to `review.md` as `## Round {{priorRound}}`. If you\'re resuming the prior session, the full task framing (spec, plan, repo conventions) is already in context \u2014 skip the re-read. If your context is cold, re-read `tasks/<id>/spec.md` and `tasks/<id>/plan.md` before addressing findings.\n\nTasks with new review feedback:\n{{{reviewLines}}}\n\nFor each task:\n1. Read the most recent `## Round {{priorRound}}` section of `tasks/<id>/review.md`. That is the entire scope of this iteration.\n2. Address every `correctness bug`, `risk/guardrail`, and `spec gap` finding from that round (blocking). `optional cleanup/nit` is at your discretion{{#tightenLine}}{{{tightenLine}}}{{/tightenLine}}\n3. Re-run only the validation checks affected by your changes (typically lint, type-check, plus whatever the diff touches).\n4. **APPEND** to `tasks/<id>/handoff.md` a new section `{{{handoffAppend}}}` (the template\'s "On revision rounds" comment shows the shape). Do NOT rewrite the file from scratch \u2014 earlier iterations stay as the cumulative record. Include only the delta: findings addressed, AC deltas, re-run validation results.\n{{/hasReviewFindings}}\n{{#hasPreflightFindings}}\nYour prior iteration was rejected by the orchestrator\'s pre-flight gate **before any Claude review ran**. The rejection details are recorded in `review.md` under `## Validation Gate` / `## Pre-Flight Rejection`.\n\nTasks with pre-flight rejection feedback:\n{{{reviewLines}}}\n\nFor each task:\n1. Read the pre-flight block in `tasks/<id>/review.md` and follow **whichever framing it carries**:\n - **"Fix the handoff"** items \u2192 fix `handoff.md` (Validation Outcomes rows, AC Coverage table, Changes table).\n - **"Fix the code"** items \u2192 a required check failed on a file you changed. Fix the regression, re-run the check, and update the handoff.\n - Both framings may be present \u2014 address all items from both before resubmitting.\n2. **APPEND** to `tasks/<id>/handoff.md` a new section `{{{handoffAppend}}}`. Include the delta: which items you addressed and how.\n{{/hasPreflightFindings}}\n\nSpec ACs remain binding. If the review identifies a dropped AC, restore it.\n\n**Iteration rules:**\n\n- **Reverting a file**: For a byte-perfect revert to the task baseline, use `git show origin/<base-branch>:<path>` (read-only git, always allowed) and write the output to the file.\n - *Perfect revert* (file no longer in `git diff base...HEAD`): delete it from all prior iteration Changes tables in `handoff.md`.\n - *Imperfect revert* (trailing newline or other residual remains): add it to the current iteration\'s Changes table with "Reverted to original (describe residual diff)".\n- **Referencing deleted (or not-yet-created) files in artifacts**: `docs-refs-check` flags a backtick path-ref to a file that does not exist. Referencing deleted paths in the handoff Changes-table first column must use `[path](path)` markdown-link form only \u2014 backtick form fails both checks.\n- **Rerouted / revised tasks \u2014 the pre-flight diff is cumulative**: the verifier checks the union of all Changes tables against `git diff <base>...HEAD`. Before submitting, run `git diff <base>...HEAD --name-only` and confirm every listed path is covered by at least one Changes-table row across ALL iterations.\n- **Bug/flake-fix red-first checkpoint**: if this round adds or modifies the spec\'s red-first regression test, write and run the test **before** making this round\'s fix edits and confirm it fails *for the reason the spec states*; then apply the fix and confirm it passes. Report the red run in the handoff delta. If it cannot be made to fail for that reason, do not implement around an unconfirmed premise \u2014 record a `[wrong-premise]` Blocker in handoff.md instead, then finish the remaining in-scope work, the handoff, and the phase command. If the spec instead uses the environment-bound-and-impractical escape, run its named deterministic alternative if it is executable in your sandbox and report the outcome in the handoff delta; if it is not executable (e.g. a documented manual repro), state that instead \u2014 never report an outcome for a run that did not happen.\n\nAppend to `tasks/<id>/notes.md` for new pitfalls found (prefix: `[implement-revision]`).\n\nWhen done, run:\n{{{phaseCommands}}}\n';
|
|
4555
4748
|
|
|
4556
4749
|
// src/orchestrator/prompts/templates/plan-reroute.md
|
|
4557
|
-
var plan_reroute_default = "You are updating the implementation plan for {{taskScope}} for {{projectName}} after a human reroute.\n\n{{{startup}}}\n\nA human amended the spec and rerouted the task after a completed implementation round. Codex has reviewed the amendment. Your job is to **append** a reroute plan section to `plan.md`; do not rewrite or remove existing plan content.\n\n{{{roundBanner}}}Amendment review verdicts:\n{{{verdictLines}}}\n\nFor each task \u2014 EXCEPT tasks whose line above marks them EXEMPT (those have no amendment; skip every step below for them and append NO Reroute Plan section to their plan.md):\n1. Read `tasks/<id>/spec.md` from your current directory, including the amendment for the round listed above.\n2. Read `tasks/<id>/plan.md` to understand the prior plan.\n3. Read `tasks/<id>/handoff.md` to understand what Codex previously shipped.\n4. Read `tasks/<id>/spec-review.md` for the latest reroute amendment review and any nits to incorporate.\n5. Append a new section to `tasks/<id>/plan.md`:\n - Round 1: `## Reroute Plan`\n - Round N >= 2: `## Reroute Plan Round N`\n6. Plan only the delta from the amendment. Reference specific files, functions, and existing patterns. Acknowledge prior plan steps that still apply without re-planning them.\n\nDo **not** rewrite or remove existing sections from `plan.md`. The appended reroute plan is what implement-reroute reads as its delta guide.\n\nWhen done, run:\n{{{phaseCommands}}}\n\n<!-- per-round append shape:\n## Reroute Plan [Round N]\n### Delta\n- ...ordered steps for the amendment delta only...\n-->\n";
|
|
4750
|
+
var plan_reroute_default = "You are updating the implementation plan for {{taskScope}} for {{projectName}} after a human reroute.\n\n{{{startup}}}\n\nA human amended the spec and rerouted the task after a completed implementation round. Codex has reviewed the amendment. Your job is to **append** a reroute plan section to `plan.md`; do not rewrite or remove existing plan content.\n\n{{{roundBanner}}}Amendment review verdicts:\n{{{verdictLines}}}\n\nFor each task \u2014 EXCEPT tasks whose line above marks them EXEMPT (those have no amendment; skip every step below for them and append NO Reroute Plan section to their plan.md):\n1. Read `tasks/<id>/spec.md` from your current directory, including the amendment for the round listed above.\n2. Read `tasks/<id>/plan.md` to understand the prior plan.\n3. Read `tasks/<id>/handoff.md` to understand what Codex previously shipped.\n4. Read `tasks/<id>/spec-review.md` for the latest reroute amendment review and any nits to incorporate.\n5. Append a new section to `tasks/<id>/plan.md`:\n - Round 1: `## Reroute Plan`\n - Round N >= 2: `## Reroute Plan Round N`\n6. Plan only the delta from the amendment. Reference specific files, functions, and existing patterns. Acknowledge prior plan steps that still apply without re-planning them.\n7. {{{feasibilityCheck}}}\n\nDo **not** rewrite or remove existing sections from `plan.md`. The appended reroute plan is what implement-reroute reads as its delta guide.\n\nWhen done, run:\n{{{phaseCommands}}}\n\n<!-- per-round append shape:\n## Reroute Plan [Round N]\n### Delta\n- Feasibility: ...\n- ...ordered steps for the amendment delta only...\n-->\n";
|
|
4558
4751
|
|
|
4559
4752
|
// src/orchestrator/prompts/templates/plan.md
|
|
4560
|
-
var plan_default = "You are writing implementation plans for {{taskScope}} for {{projectName}}.\n\n{{{startup}}}\n\n{{{verdictLines}}}\n\nFor each task, read tasks/<id>/spec.md and tasks/<id>/spec-review.md. Address any `changes_requested` items before writing the plan. If the verdict is `approved_with_nits`, incorporate the nits into the plan \u2014 they don't require spec changes but should inform implementation decisions.\n\nWrite tasks/<id>/plan.md for each task with ordered implementation steps. Reference specific files, existing patterns, and code examples from the codebase. Codex implements directly from this plan.\n\nIf you encounter spec gaps, append to tasks/<id>/notes.md (prefix: [plan]).\n\nWhen done, run:\n{{{phaseCommands}}}\n";
|
|
4753
|
+
var plan_default = "You are writing implementation plans for {{taskScope}} for {{projectName}}.\n\n{{{startup}}}\n\n{{{verdictLines}}}\n\nFor each task, read tasks/<id>/spec.md and tasks/<id>/spec-review.md. Address any `changes_requested` items before writing the plan. If the verdict is `approved_with_nits`, incorporate the nits into the plan \u2014 they don't require spec changes but should inform implementation decisions.\n\nWrite tasks/<id>/plan.md for each task with ordered implementation steps. Reference specific files, existing patterns, and code examples from the codebase. Codex implements directly from this plan.\n\n{{{feasibilityCheck}}}\n\nIf you encounter spec gaps, append to tasks/<id>/notes.md (prefix: [plan]).\n\nWhen done, run:\n{{{phaseCommands}}}\n";
|
|
4561
4754
|
|
|
4562
4755
|
// src/orchestrator/prompts/templates/qa.md
|
|
4563
4756
|
var qa_default = 'You are writing QA summaries for {{taskScope}} for {{projectName}}.\n\n{{{startup}}}\n\nTasks:\n{{{taskLines}}}\n\nFor each task:\n1. **Use the Write tool** to create tasks/<id>/done.md \u2014 plain-English summary for the human. Include: what changed, files changed, how to test, test results, human verification required, decisions made, open questions.\n \u26A0\uFE0F CRITICAL: Use the `Write` tool \u2014 do NOT simply output the done.md content as text in your response. Content in your chat reply does not get saved to disk. The pipeline validates that done.md contains real content (not the template) before advancing. Write the file.\n2. Read the latest `## Validation Outcomes` table in `tasks/<id>/handoff.md`, including any later iteration `### Re-run validation` tables. If any check\'s latest result is `human_pending`, include a **Human Verification Required** section in done.md that lists each pending check and its Notes. If none remain, write `None.` in that section. Do not hide `human_pending` checks inside the generic Test Results table.\n - If the human chooses to waive or defer a pending check later, the waiver line in done.md must begin with `Acknowledged:`. The `human_review` gate only treats that explicit prefix as a waiver.\n - Preserve `deferred_by_spec` rows in Test Results with the spec citation from Notes; do not translate them to `Pass`.\n3. Include a **Proposed Changelog** section in done.md \u2014 the changelog **entry text only**. Do **NOT** include a version number, a "Proposed version" line, or a bump tier (Patch/Minor/Major): the version is decided at the release step (`/canon-changelog` + human), never by QA.\n - **Canon release rules (non-negotiable)**: (1) Agents do not bump versions or land changelog edits without explicit scope authorization. (2) The QA step proposes a draft changelog entry text only \u2014 not the version number. (3) Changelog + version bump are committed separately from code changes (when a project versions its releases). (4) No major versioning surprises: if a task introduces a breaking change the spec didn\'t flag, raise it before shipping.\n - Read `docs/decisions.md` \xA7"Versioning and release policy" for this project\'s changelog **scope** (which changes warrant an entry). The bump tier / SemVer interpretation is the release step\'s call, not QA\'s \u2014 do not propose one.\n - If CHANGELOG.md exists, read the top of it (the most recent version section) to calibrate on scope and voice.\n - Apply the "would a user notice" test to every candidate bullet (or the project\'s equivalent scope test): if a candidate falls outside the project\'s defined changelog scope, omit it. If a task is entirely out of scope, say so explicitly ("no user-facing change \u2014 omit from changelog") rather than inventing a bullet.\n - Implementation mechanics belong in the "What Changed" section above \u2014 not in the proposed changelog.\n The human finalizes the changelog entry.\n4. **For single tasks only \u2014 use the Write tool** to create `tasks/<id>/pr-body.md` \u2014 the outward-facing PR body draft for `--pr`. Write it as if a human wrote it after doing the work.\n {{#prTemplate}}\n The repo has this PR template. Fill every section with specifics from what shipped. Keep the headings; replace every placeholder:\n\n {{{prTemplate}}}\n {{/prTemplate}}\n {{^prTemplate}}\n No PR template found. Use this structure:\n\n ## Summary\n 1\u20133 bullets: what changed and why.\n\n ## Changes\n Key files or areas touched, described for a reviewer.\n\n ## How to Test\n Steps a reviewer can follow to verify the change.\n\n ## Notes for Reviewer\n Any context, caveats, or follow-up items.\n {{/prTemplate}}\n \u26A0\uFE0F Write as the human engineer who did the work \u2014 not as the AI or tool that produced it (Claude, Codex, canon, an LLM). \u2705 e.g. "Fix the pagination off-by-one that dropped the last row." \u274C e.g. "\u{1F916} Generated with Claude Code."\n Skip this step entirely for bundle tasks \u2014 per-task bodies are not combined for bundle PRs.\n\nAfter writing all done.md files:\n- Read tasks/<id>/notes.md for each task. For each insight, ask: "would this have changed how a *different* task was approached?" If yes, **append** one new entry for *this* task to docs/lessons-learned.md. If no, the detail stays in notes.md only. Append-only: never edit, prune, promote, reorganize, or delete existing entries \u2014 not this task\'s earlier entries, and never another task\'s. Promoting entries into permanent docs (patterns.md / decisions.md) and pruning the buffer is a **human-initiated, human-approved** action \u2014 never perform it during QA, and no entry count ever triggers it. (See docs/lessons-learned.md \u2192 "How to use this doc".)\n- Add a `## Quality Log` section to done.md with the five judgment cells the pipeline cannot derive from status.json. Do **not** edit docs/task-quality-log.md directly \u2014 the qa \u2192 done transition writes or upserts that row automatically from status.json plus this section.\n\n ```markdown\n ## Quality Log\n - Spec verdict: <the FIRST spec_review verdict this task ever received \u2014 approved / approved_with_nits / changes_requested; leave blank only if truly unknown>\n - Human reroute?: <Yes/No \u2014 did the human reject at human_review and force a re-implement? Do not infer this from any reroute counter; if this is a fresh QA pass with no human_review rejection yet, answer No>\n - Dropped ACs: <count of ACs the implementation missed, caught in code review>\n - Validation gaps: <count of validation checks that should have run but did not>\n - Notes: <one-line summary of anything notable \u2014 single line, no embedded line breaks>\n ```\n\n On a re-upsert after a reroute, only fill in cells that changed or that you are correcting \u2014 an omitted cell keeps its previously recorded value. In particular, do not overwrite an already-recorded `Spec verdict` with the current status.json verdict; that column means the first spec_review verdict, and status.json only retains the latest.\n\n**Handoff Validation pre-merge checklist** (include in `done.md` Human Verification section if any item cannot be confirmed):\n- [ ] Version correct (per project policy; skip if unversioned)\n- [ ] Changelog updated if needed (per project policy; skip if unversioned)\n- [ ] PR body current\n- [ ] Final CI/CD checks green\n- [ ] Final diff matches spec intent\n\n**Output Format for Human** \u2014 `done.md` must contain:\n1. One-paragraph plain-English summary\n2. Files changed\n3. How to test (product-level steps, not code)\n4. Test results table\n5. Decisions made during implementation\n6. Open questions needing human input\n\n**Code is Canonical; Docs Reference Symbols**: Code is the source of truth for anything derivable from code: numbers, thresholds, file locations, function signatures, type shapes, observable behavior. Docs that restate these facts inline rot silently \u2014 reference the symbol or path; do not restate the value.\n\n**Commit Ownership** \u2014 three change categories:\n- Code changes \u2192 task branch, committed by the orchestrator after Codex static validation.\n- Pre-implement scaffold \u2192 base branch, committed by the orchestrator before first implement.\n- Changelog + version bump (if versioned) \u2192 separate commit, human + Claude, after human_review.\n\n- **Docs freshness \u2014 Two-checkpoint**: scan the five protected docs (`docs/architecture.md`, `docs/codebase-map.md`, `docs/patterns.md`, `docs/product-context.md`, `docs/decisions.md`) for references that {{docsScope}} *contradicts* \u2014 a renamed symbol, a moved file, a behavior this task changed \u2014 and correct those stale references. That is the only edit QA makes to permanent docs. Do not add new lessons, pitfalls, or decisions here, and do not promote buffer entries \u2014 promotion is the human sweep, not Docs freshness.\n- **Buffer signal** (not an action): after appending, if docs/lessons-learned.md now holds more than ~15 entries, add one line to this task\'s done.md \u2014 `Maintenance: lessons-learned.md has N entries; a human lessons sweep is due (see docs/lessons-learned.md \u2192 "How to use this doc").` Do not perform the sweep yourself.\n\nWhen done, run (use the Bash tool \u2014 do not just output the command as text):\n{{{phaseCommands}}}\n';
|
|
@@ -4618,6 +4811,20 @@ function buildAffectedFilesBlock(affectedFiles, baseBranch) {
|
|
|
4618
4811
|
""
|
|
4619
4812
|
].join("\n");
|
|
4620
4813
|
}
|
|
4814
|
+
function planFeasibilityCheck(placement, indent = "") {
|
|
4815
|
+
const lines = [
|
|
4816
|
+
`**Feasibility check.** Before writing steps, confirm each item below and record the results in a few lines ${placement}. Mark an item that doesn't apply \`N/A\` in a word.`,
|
|
4817
|
+
"- **Exists:** the functions, files, and patterns the plan relies on exist \u2014 found by searching the code, not recalled.",
|
|
4818
|
+
"- **Real path:** the actual runtime call path the change sits on, including any existing code on it that already does part of the work.",
|
|
4819
|
+
"- **Callers:** every caller of each function whose behavior or error contract changes, found and accounted for.",
|
|
4820
|
+
"- **Tests that can fail:** for each regression test the plan prescribes, the input or state that sends it through the changed path, so it fails without the change.",
|
|
4821
|
+
"- **Predicates:** for each condition or state check the plan writes out, its boundary values (for example zero, empty, or non-numeric) and every state it must handle.",
|
|
4822
|
+
"- **Scope:** every file the steps change is inside the spec's Affected Files.",
|
|
4823
|
+
"- **Async/stateful:** for async or stateful changes \u2014 re-entry, cancellation or unmount, stale state, and ownership.",
|
|
4824
|
+
"A finding that changes a step goes in that step. If the check contradicts the spec, record the contradiction in the plan and in `tasks/<id>/notes.md` with a `[plan]` prefix. Do not change the spec or widen scope to resolve it."
|
|
4825
|
+
];
|
|
4826
|
+
return lines.map((line, index) => index === 0 ? line : indent + line).join("\n");
|
|
4827
|
+
}
|
|
4621
4828
|
function promptSpec(state) {
|
|
4622
4829
|
const { tasks, tier, isBundle } = state;
|
|
4623
4830
|
const combined = tier === "fast";
|
|
@@ -4633,9 +4840,13 @@ Directory: tasks/${task.taskId}/`,
|
|
|
4633
4840
|
startup: CLAUDE_STARTUP,
|
|
4634
4841
|
instructions: isBundle ? tasks.map(
|
|
4635
4842
|
(t) => `**Task \`${t.taskId}\`**: Write tasks/${t.taskId}/spec.md using the template.` + (combined ? ` Also write tasks/${t.taskId}/plan.md with ordered implementation steps, specific file references, and existing patterns.` : "")
|
|
4636
|
-
).join("\n\n")
|
|
4843
|
+
).join("\n\n") + (combined ? `
|
|
4844
|
+
|
|
4845
|
+
${planFeasibilityCheck("in each plan.md's Approach section")}` : "") : `Write tasks/${task.taskId}/spec.md using the template in .canon/templates/spec.md. Be concrete \u2014 Codex implements directly from this.` + (combined ? `
|
|
4846
|
+
|
|
4847
|
+
Also write tasks/${task.taskId}/plan.md with ordered implementation steps, specific file references, and existing patterns to use.
|
|
4637
4848
|
|
|
4638
|
-
|
|
4849
|
+
${planFeasibilityCheck("in plan.md's Approach section")}` : ""),
|
|
4639
4850
|
bundleNote: isBundle ? "\nThese tasks are related \u2014 consider cross-task interactions while speccing." : "",
|
|
4640
4851
|
doneNote: combined ? "The orchestrator will handle spec_review and plan-phase advancement automatically for fast-tier tasks." : "",
|
|
4641
4852
|
selfCheck: [
|
|
@@ -4643,6 +4854,7 @@ Also write tasks/${task.taskId}/plan.md with ordered implementation steps, speci
|
|
|
4643
4854
|
'- Every AC is verifiable with a specific test (not just "it works" \u2014 state exactly how to verify)',
|
|
4644
4855
|
"- Affected Files lists specific files (not directories) with specific, actionable change descriptions",
|
|
4645
4856
|
combined ? "- Plan steps reference actual function/file names from the codebase (not just concepts)" : null,
|
|
4857
|
+
combined ? "- plan.md's Approach records the feasibility check (exists, real path, callers, tests that can fail, predicates, Affected Files, async/stateful; N/A where it doesn't apply)" : null,
|
|
4646
4858
|
"- Known Risks covers failure modes for the trickiest ACs",
|
|
4647
4859
|
"- Human Test Plan describes product behavior only (no code, no file names, no TypeScript)",
|
|
4648
4860
|
'- Validation Required has at least one entry checked (or explicitly "None" with a reason)',
|
|
@@ -4746,6 +4958,7 @@ function promptPlan(state) {
|
|
|
4746
4958
|
taskScope: tasks.length > 1 ? "a bundle of tasks" : `task "${tasks[0].taskId}"`,
|
|
4747
4959
|
roundBanner,
|
|
4748
4960
|
verdictLines: verdictLines2,
|
|
4961
|
+
feasibilityCheck: planFeasibilityCheck("at the top of the appended `### Delta` section", " "),
|
|
4749
4962
|
phaseCommands: phaseCommands(tasks.map((t) => t.taskId), "plan", "done")
|
|
4750
4963
|
});
|
|
4751
4964
|
}
|
|
@@ -4757,6 +4970,7 @@ function promptPlan(state) {
|
|
|
4757
4970
|
startup: CLAUDE_STARTUP,
|
|
4758
4971
|
taskScope: tasks.length > 1 ? "a bundle of tasks" : `task "${tasks[0].taskId}"`,
|
|
4759
4972
|
verdictLines,
|
|
4973
|
+
feasibilityCheck: planFeasibilityCheck("in the plan's Approach section"),
|
|
4760
4974
|
phaseCommands: phaseCommands(tasks.map((t) => t.taskId), "plan", "done")
|
|
4761
4975
|
});
|
|
4762
4976
|
}
|
|
@@ -4882,7 +5096,7 @@ function bundleHasRealPriorReview(taskIds) {
|
|
|
4882
5096
|
return taskIds.every((taskId) => {
|
|
4883
5097
|
const reviewPath = path11.join(taskDirFor(taskId), "review.md");
|
|
4884
5098
|
try {
|
|
4885
|
-
const content =
|
|
5099
|
+
const content = fs12.readFileSync(reviewPath, "utf8");
|
|
4886
5100
|
const hasH2 = /^## Stage 1\b/m.test(content);
|
|
4887
5101
|
const hasNested = /^## Round \d+\b/m.test(content) && /^### Stage 1\b/m.test(content);
|
|
4888
5102
|
return (hasH2 || hasNested) && !content.includes("[TASK-ID]");
|
|
@@ -4891,20 +5105,24 @@ function bundleHasRealPriorReview(taskIds) {
|
|
|
4891
5105
|
}
|
|
4892
5106
|
});
|
|
4893
5107
|
}
|
|
4894
|
-
function
|
|
4895
|
-
const { tasks } = state;
|
|
5108
|
+
function resolveCodeReviewRound(tasks) {
|
|
4896
5109
|
const rawMaxIter = tasks.reduce((max, t) => Math.max(max, t.iterations), 0);
|
|
4897
5110
|
const maxIter = bundleHasRealPriorReview(tasks.map((t) => t.taskId)) ? rawMaxIter : 0;
|
|
5111
|
+
return { isRound1: maxIter === 0, roundN: maxIter + 1, maxIter };
|
|
5112
|
+
}
|
|
5113
|
+
function promptCodeReview(state, baseBranch, scopedDiff = null, coldCodexFindings = null, scopeInfo = null, outOfScopeFiles = []) {
|
|
5114
|
+
const { tasks } = state;
|
|
5115
|
+
const { maxIter, isRound1, roundN } = resolveCodeReviewRound(tasks);
|
|
4898
5116
|
const resolvedBaseBranch = baseBranch ?? getBaseBranch(tasks.map((t) => t.taskId));
|
|
4899
|
-
const
|
|
4900
|
-
const
|
|
4901
|
-
const
|
|
5117
|
+
const isDeltaScope = scopeInfo?.scope === "delta";
|
|
5118
|
+
const effectiveScopedDiff = isDeltaScope ? null : scopedDiff;
|
|
5119
|
+
const hasDiff = effectiveScopedDiff !== null;
|
|
4902
5120
|
const priorIteration = maxIter;
|
|
4903
5121
|
const diffView = hasDiff ? {
|
|
4904
5122
|
hasDiff,
|
|
4905
5123
|
baseBranch: resolvedBaseBranch,
|
|
4906
|
-
diffContent:
|
|
4907
|
-
diffTruncated:
|
|
5124
|
+
diffContent: effectiveScopedDiff.diff,
|
|
5125
|
+
diffTruncated: effectiveScopedDiff.truncated
|
|
4908
5126
|
} : {
|
|
4909
5127
|
hasDiff,
|
|
4910
5128
|
baseBranch: resolvedBaseBranch,
|
|
@@ -4929,9 +5147,20 @@ function promptCodeReview(state, baseBranch, scopedDiff = null, coldCodexFinding
|
|
|
4929
5147
|
maxIter,
|
|
4930
5148
|
tightenLine,
|
|
4931
5149
|
...diffView,
|
|
5150
|
+
isDeltaScope,
|
|
5151
|
+
scopeWord: isDeltaScope ? "Delta" : "Full",
|
|
5152
|
+
scopeBase: scopeInfo?.base ?? resolvedBaseBranch,
|
|
5153
|
+
scopeReason: scopeInfo?.reason === "delta" ? "small fix within the files already under review" : scopeInfo?.reason ?? (isRound1 ? "Round 1 (initial review)" : "full review"),
|
|
5154
|
+
deltaBase: scopeInfo?.base ?? "",
|
|
5155
|
+
deltaDiffContent: scopeInfo?.deltaDiff?.diff ?? "",
|
|
5156
|
+
hasDeltaDiff: scopeInfo?.deltaDiff !== null && scopeInfo?.deltaDiff !== void 0,
|
|
5157
|
+
deltaDiffTruncated: scopeInfo?.deltaDiff?.truncated ?? false,
|
|
4932
5158
|
coldCodexFindings: coldCodexFindings ?? "",
|
|
4933
5159
|
hasColdCodexFindings: coldCodexFindings !== null,
|
|
4934
|
-
|
|
5160
|
+
hasOutOfScopeFiles: outOfScopeFiles.length > 0,
|
|
5161
|
+
outOfScopeFilesList: outOfScopeFiles.map((file) => `- \`${file}\``).join("\n"),
|
|
5162
|
+
phaseCommands: phaseCommands(tasks.map((t) => t.taskId), "code_review", "done", "<verdict>"),
|
|
5163
|
+
blockedCommands: phaseCommands(tasks.map((t) => t.taskId), "code_review", "blocked")
|
|
4935
5164
|
});
|
|
4936
5165
|
}
|
|
4937
5166
|
function promptQa(state, prTemplate) {
|
|
@@ -4953,19 +5182,19 @@ function promptQa(state, prTemplate) {
|
|
|
4953
5182
|
|
|
4954
5183
|
// src/task/index.ts
|
|
4955
5184
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
4956
|
-
import
|
|
5185
|
+
import fs15 from "fs";
|
|
4957
5186
|
import path14 from "path";
|
|
4958
5187
|
|
|
4959
5188
|
// src/orchestrator/canon-snapshot.ts
|
|
4960
5189
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
4961
|
-
import
|
|
5190
|
+
import fs13 from "fs";
|
|
4962
5191
|
import path12 from "path";
|
|
4963
5192
|
var CANON_UPSTREAM_REPO = "tstraub89/canon-ai";
|
|
4964
5193
|
function isInstalledSourcePath(sourcePath) {
|
|
4965
5194
|
return sourcePath.includes("/node_modules/") || sourcePath.includes("\\node_modules\\") || sourcePath.includes("/_npx/") || sourcePath.includes("\\_npx\\");
|
|
4966
5195
|
}
|
|
4967
5196
|
function resolveCanonVersion(explicit) {
|
|
4968
|
-
return explicit ?? "3.
|
|
5197
|
+
return explicit ?? "3.5.0" ?? "dev";
|
|
4969
5198
|
}
|
|
4970
5199
|
function resolveOrchestratorCommit(repoRoot, upstreamCommit, runGitAt) {
|
|
4971
5200
|
const ownToplevel = captureGitOutput(repoRoot, ["rev-parse", "--show-toplevel"], runGitAt);
|
|
@@ -5044,14 +5273,14 @@ function applyCanonSnapshot(status, canon) {
|
|
|
5044
5273
|
return next;
|
|
5045
5274
|
}
|
|
5046
5275
|
function refreshCanonSnapshotAtPath(statusFilePath, options = {}) {
|
|
5047
|
-
const status = JSON.parse(
|
|
5276
|
+
const status = JSON.parse(fs13.readFileSync(statusFilePath, "utf8"));
|
|
5048
5277
|
const canon = captureCanonSnapshot(REPO_ROOT, options);
|
|
5049
5278
|
const next = applyCanonSnapshot(status, canon);
|
|
5050
5279
|
const serialized = `${JSON.stringify(next, null, 2)}
|
|
5051
5280
|
`;
|
|
5052
|
-
const current =
|
|
5281
|
+
const current = fs13.readFileSync(statusFilePath, "utf8");
|
|
5053
5282
|
if (current !== serialized) {
|
|
5054
|
-
|
|
5283
|
+
fs13.writeFileSync(statusFilePath, serialized, "utf8");
|
|
5055
5284
|
}
|
|
5056
5285
|
return canon;
|
|
5057
5286
|
}
|
|
@@ -5060,7 +5289,7 @@ function refreshCanonSnapshotsAtPaths(statusFilePaths, options = {}) {
|
|
|
5060
5289
|
}
|
|
5061
5290
|
|
|
5062
5291
|
// src/orchestrator/quality-log.ts
|
|
5063
|
-
import
|
|
5292
|
+
import fs14 from "fs";
|
|
5064
5293
|
import path13 from "path";
|
|
5065
5294
|
var CANON_LOG_HEADERS = [
|
|
5066
5295
|
"Date",
|
|
@@ -5255,11 +5484,11 @@ function renderRowLine(headerCells, row) {
|
|
|
5255
5484
|
function writeFileAtomic(filePath, content) {
|
|
5256
5485
|
const tempPath = `${filePath}.tmp`;
|
|
5257
5486
|
try {
|
|
5258
|
-
|
|
5259
|
-
|
|
5487
|
+
fs14.writeFileSync(tempPath, content, "utf8");
|
|
5488
|
+
fs14.renameSync(tempPath, filePath);
|
|
5260
5489
|
} finally {
|
|
5261
5490
|
try {
|
|
5262
|
-
|
|
5491
|
+
fs14.unlinkSync(tempPath);
|
|
5263
5492
|
} catch {
|
|
5264
5493
|
}
|
|
5265
5494
|
}
|
|
@@ -5268,7 +5497,7 @@ function upsertQualityLogRow(logFilePath, derived, qaSupplied) {
|
|
|
5268
5497
|
try {
|
|
5269
5498
|
let content;
|
|
5270
5499
|
try {
|
|
5271
|
-
content =
|
|
5500
|
+
content = fs14.readFileSync(logFilePath, "utf8");
|
|
5272
5501
|
} catch (error) {
|
|
5273
5502
|
if (error.code === "ENOENT") {
|
|
5274
5503
|
content = STANDARD_QUALITY_LOG_SKELETON;
|
|
@@ -5327,7 +5556,7 @@ function writeQualityLogForTask(taskId, activeCwd, donePath, status) {
|
|
|
5327
5556
|
try {
|
|
5328
5557
|
let doneContent = "";
|
|
5329
5558
|
try {
|
|
5330
|
-
doneContent =
|
|
5559
|
+
doneContent = fs14.readFileSync(donePath, "utf8");
|
|
5331
5560
|
} catch {
|
|
5332
5561
|
}
|
|
5333
5562
|
upsertQualityLogRow(
|
|
@@ -5381,7 +5610,7 @@ function taskRootForGate(cwd) {
|
|
|
5381
5610
|
}
|
|
5382
5611
|
function readJsonFile(filePath) {
|
|
5383
5612
|
try {
|
|
5384
|
-
return JSON.parse(
|
|
5613
|
+
return JSON.parse(fs15.readFileSync(filePath, "utf8"));
|
|
5385
5614
|
} catch (error) {
|
|
5386
5615
|
const message = error instanceof Error ? error.message : String(error);
|
|
5387
5616
|
throw new Error(`Error: failed to read ${filePath}: ${message}`);
|
|
@@ -5389,9 +5618,9 @@ function readJsonFile(filePath) {
|
|
|
5389
5618
|
}
|
|
5390
5619
|
function writeJsonAtomic(filePath, data) {
|
|
5391
5620
|
const tmpFile = `${filePath}.tmp`;
|
|
5392
|
-
|
|
5621
|
+
fs15.writeFileSync(tmpFile, `${JSON.stringify(data, null, 2)}
|
|
5393
5622
|
`, "utf8");
|
|
5394
|
-
|
|
5623
|
+
fs15.renameSync(tmpFile, filePath);
|
|
5395
5624
|
}
|
|
5396
5625
|
function writeStatusAtomic(filePath, status) {
|
|
5397
5626
|
status.status = deriveTopLevelStatus(status);
|
|
@@ -5464,7 +5693,7 @@ function taskPhase(id, phaseArg, statusArg, verdictArg) {
|
|
|
5464
5693
|
assertValidVerdict(phaseArg, verdictArg);
|
|
5465
5694
|
const taskCwd = resolveTaskCwd(id);
|
|
5466
5695
|
const statusPath = taskStatusFileForCwd(taskCwd, id);
|
|
5467
|
-
if (!
|
|
5696
|
+
if (!fs15.existsSync(statusPath)) {
|
|
5468
5697
|
throw new Error(`Error: No status.json found for task ${id} (looked in ${taskDirForCwd(taskCwd, id)}/)`);
|
|
5469
5698
|
}
|
|
5470
5699
|
const status = readJsonFile(statusPath);
|
|
@@ -5526,7 +5755,7 @@ function taskPhasePreflightRejected(id, phaseArg) {
|
|
|
5526
5755
|
}
|
|
5527
5756
|
const taskCwd = resolveTaskCwd(id);
|
|
5528
5757
|
const statusPath = taskStatusFileForCwd(taskCwd, id);
|
|
5529
|
-
if (!
|
|
5758
|
+
if (!fs15.existsSync(statusPath)) {
|
|
5530
5759
|
throw new Error(`Error: No status.json found for task ${id} (looked in ${taskDirForCwd(taskCwd, id)}/)`);
|
|
5531
5760
|
}
|
|
5532
5761
|
const status = readJsonFile(statusPath);
|
|
@@ -5549,6 +5778,14 @@ function taskPhasePreflightRejected(id, phaseArg) {
|
|
|
5549
5778
|
}
|
|
5550
5779
|
|
|
5551
5780
|
// src/orchestrator/phases/code-review.ts
|
|
5781
|
+
function findUnjudgedFullSendFilesFromData(changedFiles, allowlist, taskIds, verdicts) {
|
|
5782
|
+
if (verdicts.some((verdict) => verdict === "changes_requested" || verdict === "needs_re_review" || verdict === "spec_gap")) return [];
|
|
5783
|
+
return verifyBaseDriftFromData(changedFiles, allowlist.paths, taskIds, allowlist.prefixes);
|
|
5784
|
+
}
|
|
5785
|
+
function fullSendScopeBlockReason(taskIds, files, recordedVerdicts) {
|
|
5786
|
+
const verdictNote = recordedVerdicts.length > 0 ? ` Recorded code-review verdict(s): ${recordedVerdicts.join(", ")}; inspect the review before resuming.` : "";
|
|
5787
|
+
return `Full-send code review left out-of-scope files unjudged: ${files.join(", ")}. Add them to a task spec's ### Affected Files table, then re-run \`canon run ${taskIds.join(" ")}\`; or reroute with a note telling the implementer to remove them.${verdictNote}`;
|
|
5788
|
+
}
|
|
5552
5789
|
var defaultDeps = {
|
|
5553
5790
|
verifyBranch,
|
|
5554
5791
|
getBaseBranch,
|
|
@@ -5556,6 +5793,15 @@ var defaultDeps = {
|
|
|
5556
5793
|
getAffectedFiles,
|
|
5557
5794
|
verifyHandoffAgainstDiff,
|
|
5558
5795
|
getScopedDiff,
|
|
5796
|
+
getScopedDiffInRange,
|
|
5797
|
+
getPathsInRange,
|
|
5798
|
+
getDeltaLineStats,
|
|
5799
|
+
resolveCommit,
|
|
5800
|
+
isAncestorCommit,
|
|
5801
|
+
getEffectiveSize: getEffectiveSize2,
|
|
5802
|
+
findColdCodexArchiveForRound,
|
|
5803
|
+
hasMalformedColdCodexArchive,
|
|
5804
|
+
writeColdCodexArchive,
|
|
5559
5805
|
getClaudeConfig,
|
|
5560
5806
|
getMaxReviewLoops,
|
|
5561
5807
|
getCodexConfig,
|
|
@@ -5684,7 +5930,7 @@ function writePreflightReviewArtifacts(tasks, preflightFailed, route) {
|
|
|
5684
5930
|
const reviewPath = path15.join(taskDirFor(t.taskId), "review.md");
|
|
5685
5931
|
let existing = "";
|
|
5686
5932
|
try {
|
|
5687
|
-
existing =
|
|
5933
|
+
existing = fs16.readFileSync(reviewPath, "utf8");
|
|
5688
5934
|
} catch {
|
|
5689
5935
|
}
|
|
5690
5936
|
const hasH2Stage1 = /^## Stage 1\b/m.test(existing);
|
|
@@ -5700,7 +5946,7 @@ function writePreflightReviewArtifacts(tasks, preflightFailed, route) {
|
|
|
5700
5946
|
${blockedBlock}` : `# Code Review: ${t.taskId}
|
|
5701
5947
|
|
|
5702
5948
|
${blockedBlock}`;
|
|
5703
|
-
|
|
5949
|
+
fs16.writeFileSync(reviewPath, reviewContent2, "utf8");
|
|
5704
5950
|
continue;
|
|
5705
5951
|
}
|
|
5706
5952
|
const currentPreflight = t.status.phases.code_review?.preflight_rejections_current_loop ?? 0;
|
|
@@ -5711,7 +5957,7 @@ ${blockedBlock}`;
|
|
|
5711
5957
|
---
|
|
5712
5958
|
|
|
5713
5959
|
${stub}` : stub;
|
|
5714
|
-
|
|
5960
|
+
fs16.writeFileSync(reviewPath, reviewContent, "utf8");
|
|
5715
5961
|
}
|
|
5716
5962
|
return true;
|
|
5717
5963
|
}
|
|
@@ -5756,11 +6002,63 @@ async function runCodeReviewPhase(state, interactive, resumeId, deps = defaultDe
|
|
|
5756
6002
|
}
|
|
5757
6003
|
return { agent: "claude", sessionId: null, exitCode: 0 };
|
|
5758
6004
|
}
|
|
6005
|
+
const allowlist = buildAffectedFilesAllowlist(taskIds, { admitManagedDocs: true });
|
|
6006
|
+
const outOfScopeFiles = verifyBaseDriftFromData(
|
|
6007
|
+
[...changedFiles],
|
|
6008
|
+
allowlist.paths,
|
|
6009
|
+
taskIds,
|
|
6010
|
+
allowlist.prefixes
|
|
6011
|
+
);
|
|
6012
|
+
const scopeFullSend = tasks.every((t) => t.status.full_send === true);
|
|
6013
|
+
if (outOfScopeFiles.length > 0 && !scopeFullSend) {
|
|
6014
|
+
const reason = `Code review pre-flight found files outside the spec's Affected Files for ${taskIds.join(", ")}:
|
|
6015
|
+
` + outOfScopeFiles.map((file) => ` ${file}`).join("\n") + `
|
|
6016
|
+
Add them to the affected task spec's ### Affected Files table in the active checkout, then re-run \`canon run ${taskIds.join(" ")}\`; or reroute with a note telling the implementer to remove them.`;
|
|
6017
|
+
warn(reason);
|
|
6018
|
+
autoBlockPhase(taskIds, "code_review", codeReviewCheck.count, reason);
|
|
6019
|
+
process.exit(2);
|
|
6020
|
+
}
|
|
6021
|
+
const headSha = deps.resolveCommit("HEAD", activeCwd);
|
|
6022
|
+
if (!headSha) {
|
|
6023
|
+
setExitReason(`Cannot start code_review for task(s) ${taskIds.join(", ")}: HEAD does not resolve to a commit.`);
|
|
6024
|
+
process.exit(1);
|
|
6025
|
+
}
|
|
5759
6026
|
info(`Phase: code_review (Claude${state.isBundle ? " bundle" : ""}, iteration ${maxIter + 1})`);
|
|
5760
6027
|
for (const t of tasks) taskPhase(t.taskId, "code_review", "in_progress");
|
|
6028
|
+
const { isRound1, roundN } = resolveCodeReviewRound(tasks);
|
|
6029
|
+
const effectiveSize = deps.getEffectiveSize(tasks);
|
|
6030
|
+
const delicate = tasks.some((t) => t.status.delicate === true);
|
|
6031
|
+
const records = !isRound1 && effectiveSize !== "XL" ? tasks.map((t) => deps.findColdCodexArchiveForRound(taskDirFor(t.taskId), roundN - 1)) : [];
|
|
6032
|
+
const agreed = records.length === tasks.length && records.every((record) => record !== null && record.reviewedSha === records[0]?.reviewedSha);
|
|
6033
|
+
const previousRecordsDisagree = records.length === tasks.length && records.every((record) => record !== null) && !agreed;
|
|
6034
|
+
const previousArchiveMalformed = records.some((record, index) => record === null && deps.hasMalformedColdCodexArchive(taskDirFor(tasks[index].taskId)));
|
|
6035
|
+
const reviewedSha = agreed ? records[0]?.reviewedSha : void 0;
|
|
6036
|
+
const resolved = reviewedSha ? deps.resolveCommit(reviewedSha, activeCwd) : null;
|
|
6037
|
+
const prevRecord = reviewedSha ? {
|
|
6038
|
+
reviewedSha,
|
|
6039
|
+
exists: resolved !== null,
|
|
6040
|
+
isAncestor: resolved !== null && deps.isAncestorCommit(reviewedSha, headSha, activeCwd),
|
|
6041
|
+
equalsHead: resolved !== null && resolved === headSha
|
|
6042
|
+
} : null;
|
|
6043
|
+
const canProbe = prevRecord?.exists === true && prevRecord.isAncestor && !prevRecord.equalsHead;
|
|
6044
|
+
const scope = resolveCodeReviewScope({
|
|
6045
|
+
isRound1,
|
|
6046
|
+
effectiveSize,
|
|
6047
|
+
delicate,
|
|
6048
|
+
baseBranch,
|
|
6049
|
+
prevRecord,
|
|
6050
|
+
previousArchiveMalformed,
|
|
6051
|
+
previousRecordsDisagree,
|
|
6052
|
+
deltaPaths: canProbe ? deps.getPathsInRange(`${reviewedSha}..${headSha}`, activeCwd) : [],
|
|
6053
|
+
priorChangeSetPaths: canProbe ? deps.getPathsInRange(`${baseBranch}...${reviewedSha}`, activeCwd) : [],
|
|
6054
|
+
deltaFileStats: canProbe && reviewedSha ? deps.getDeltaLineStats(reviewedSha, activeCwd, headSha) : [],
|
|
6055
|
+
taskIds,
|
|
6056
|
+
telemetryFiles: PIPELINE_TELEMETRY_FILES
|
|
6057
|
+
});
|
|
6058
|
+
const deltaDiff = scope.scope === "delta" ? deps.getScopedDiffInRange(`${scope.base}..${headSha}`, activeCwd) : null;
|
|
5761
6059
|
const coldCfg = deps.getCodexConfig("code_review", tasks);
|
|
5762
6060
|
const coldReviewStartMs = Date.now();
|
|
5763
|
-
const coldReview = await deps.runColdCodexReview(
|
|
6061
|
+
const coldReview = await deps.runColdCodexReview(scope.base, coldCfg.model, coldCfg.effort, activeCwd, {
|
|
5764
6062
|
taskId: taskIds.join("+"),
|
|
5765
6063
|
phase: "code_review",
|
|
5766
6064
|
iteration: maxIter,
|
|
@@ -5774,17 +6072,26 @@ async function runCodeReviewPhase(state, interactive, resumeId, deps = defaultDe
|
|
|
5774
6072
|
process.exit(1);
|
|
5775
6073
|
}
|
|
5776
6074
|
for (const t of tasks) {
|
|
5777
|
-
|
|
6075
|
+
fs16.writeFileSync(
|
|
5778
6076
|
path15.join(taskDirFor(t.taskId), "review-cold-codex.md"),
|
|
5779
6077
|
coldReview.findings,
|
|
5780
6078
|
"utf8"
|
|
5781
6079
|
);
|
|
5782
6080
|
}
|
|
6081
|
+
for (const t of tasks) {
|
|
6082
|
+
deps.writeColdCodexArchive(taskDirFor(t.taskId), {
|
|
6083
|
+
round: roundN,
|
|
6084
|
+
reviewedSha: headSha,
|
|
6085
|
+
scope: scope.scope,
|
|
6086
|
+
base: scope.base,
|
|
6087
|
+
reason: scope.reason
|
|
6088
|
+
}, coldReview.findings);
|
|
6089
|
+
}
|
|
5783
6090
|
info(`\u2192 cold-codex review (${taskIds.join(", ")}): ${Math.round(coldReviewDurationMs / 1e3)}s`);
|
|
5784
6091
|
const cfg = deps.getClaudeConfig("code_review", tasks);
|
|
5785
6092
|
const reviewResumeId = maxIter > 0 ? resumeId : null;
|
|
5786
|
-
const scopedDiff = deps.getScopedDiff(baseBranch, activeCwd);
|
|
5787
|
-
const result = await deps.runClaude(promptCodeReview(state, baseBranch, scopedDiff, coldReview.findings), interactive, reviewResumeId, cfg.model, cfg.effort, cfg.budget, {
|
|
6093
|
+
const scopedDiff = scope.scope === "delta" ? null : deps.getScopedDiff(baseBranch, activeCwd);
|
|
6094
|
+
const result = await deps.runClaude(promptCodeReview(state, baseBranch, scopedDiff, coldReview.findings, { ...scope, deltaDiff }, outOfScopeFiles), interactive, reviewResumeId, cfg.model, cfg.effort, cfg.budget, {
|
|
5788
6095
|
taskId: taskIds.join("+"),
|
|
5789
6096
|
phase: "code_review",
|
|
5790
6097
|
iteration: maxIter,
|
|
@@ -5794,7 +6101,7 @@ async function runCodeReviewPhase(state, interactive, resumeId, deps = defaultDe
|
|
|
5794
6101
|
const reviewPath = path15.join(taskDirFor(t.taskId), "review.md");
|
|
5795
6102
|
let reviewContent = null;
|
|
5796
6103
|
try {
|
|
5797
|
-
reviewContent =
|
|
6104
|
+
reviewContent = fs16.readFileSync(reviewPath, "utf8");
|
|
5798
6105
|
} catch {
|
|
5799
6106
|
}
|
|
5800
6107
|
if (isTemplateUnfilled(reviewContent)) {
|
|
@@ -5895,7 +6202,7 @@ async function runImplementPhase(state, interactive, resumeId, force = false) {
|
|
|
5895
6202
|
}
|
|
5896
6203
|
|
|
5897
6204
|
// src/orchestrator/phases/plan.ts
|
|
5898
|
-
import
|
|
6205
|
+
import fs17 from "fs";
|
|
5899
6206
|
import path16 from "path";
|
|
5900
6207
|
async function runPlanPhase(state, interactive) {
|
|
5901
6208
|
const { tasks } = state;
|
|
@@ -5914,7 +6221,7 @@ async function runPlanPhase(state, interactive) {
|
|
|
5914
6221
|
const planPath = path16.join(taskDirFor(t.taskId), "plan.md");
|
|
5915
6222
|
let planContent = null;
|
|
5916
6223
|
try {
|
|
5917
|
-
planContent =
|
|
6224
|
+
planContent = fs17.readFileSync(planPath, "utf8");
|
|
5918
6225
|
} catch {
|
|
5919
6226
|
}
|
|
5920
6227
|
if (isTemplateUnfilled(planContent)) {
|
|
@@ -5926,7 +6233,7 @@ async function runPlanPhase(state, interactive) {
|
|
|
5926
6233
|
}
|
|
5927
6234
|
|
|
5928
6235
|
// src/orchestrator/phases/qa.ts
|
|
5929
|
-
import
|
|
6236
|
+
import fs18 from "fs";
|
|
5930
6237
|
import path17 from "path";
|
|
5931
6238
|
async function runQaPhase(state, interactive, resolvedPrTemplate) {
|
|
5932
6239
|
const { tasks } = state;
|
|
@@ -5948,7 +6255,7 @@ async function runQaPhase(state, interactive, resolvedPrTemplate) {
|
|
|
5948
6255
|
if (isDoneMdTemplate(donePath)) {
|
|
5949
6256
|
const salvaged = extractDoneMdFromStdout(result.capturedStdout);
|
|
5950
6257
|
if (salvaged) {
|
|
5951
|
-
|
|
6258
|
+
fs18.writeFileSync(donePath, salvaged);
|
|
5952
6259
|
warn(`Salvaged tasks/${taskId}/done.md from captured stdout \u2014 QA sub-agent streamed content instead of using the Write tool.`);
|
|
5953
6260
|
const phaseStatus = readStatus(taskId).phases.qa?.status ?? "pending";
|
|
5954
6261
|
if (phaseStatus !== "done") {
|
|
@@ -6000,7 +6307,7 @@ async function runSpecPhase(state, interactive, resumeId) {
|
|
|
6000
6307
|
}
|
|
6001
6308
|
|
|
6002
6309
|
// src/orchestrator/phases/spec-review.ts
|
|
6003
|
-
import
|
|
6310
|
+
import fs19 from "fs";
|
|
6004
6311
|
import path18 from "path";
|
|
6005
6312
|
function autoBlockSpecReview(taskIds, iterationCount, reason) {
|
|
6006
6313
|
autoBlockPhase(taskIds, "spec_review", iterationCount, reason);
|
|
@@ -6009,14 +6316,14 @@ function recordFastTierSpecApproval(taskId) {
|
|
|
6009
6316
|
const artifactPath = path18.join(taskDirFor(taskId), "spec-review.md");
|
|
6010
6317
|
let content;
|
|
6011
6318
|
try {
|
|
6012
|
-
content =
|
|
6319
|
+
content = fs19.readFileSync(artifactPath, "utf8");
|
|
6013
6320
|
} catch {
|
|
6014
6321
|
return;
|
|
6015
6322
|
}
|
|
6016
6323
|
if (extractCheckedVerdict(content)) return;
|
|
6017
6324
|
const checked = content.replace(/^- \[ \] (\*\*Approved\*\*)/m, "- [x] $1");
|
|
6018
6325
|
const note = "\n> Fast tier: Codex spec review skipped \u2014 human conversational spec approval recorded by the orchestrator.\n";
|
|
6019
|
-
|
|
6326
|
+
fs19.writeFileSync(
|
|
6020
6327
|
artifactPath,
|
|
6021
6328
|
(checked !== content ? checked : `${content}
|
|
6022
6329
|
## Verdict
|
|
@@ -6088,7 +6395,7 @@ ${promptSpecReview(state)}` : promptSpecReview(state);
|
|
|
6088
6395
|
const reviewPath = path18.join(resolveTaskCwd(t.taskId), "tasks", t.taskId, "spec-review.md");
|
|
6089
6396
|
let reviewContent = null;
|
|
6090
6397
|
try {
|
|
6091
|
-
reviewContent =
|
|
6398
|
+
reviewContent = fs19.readFileSync(reviewPath, "utf8");
|
|
6092
6399
|
} catch {
|
|
6093
6400
|
}
|
|
6094
6401
|
if (isTemplateUnfilled(reviewContent)) {
|
|
@@ -6101,7 +6408,7 @@ ${promptSpecReview(state)}` : promptSpecReview(state);
|
|
|
6101
6408
|
|
|
6102
6409
|
// src/orchestrator/detach.ts
|
|
6103
6410
|
import { spawn as spawn3 } from "child_process";
|
|
6104
|
-
import
|
|
6411
|
+
import fs20 from "fs";
|
|
6105
6412
|
import path19 from "path";
|
|
6106
6413
|
var DETACH_CHILD_FLAG = "CANON_DETACHED";
|
|
6107
6414
|
var DETACH_DISABLE_FLAG = "CANON_NO_DETACH";
|
|
@@ -6138,7 +6445,7 @@ function detachAndExit(options) {
|
|
|
6138
6445
|
}
|
|
6139
6446
|
const primaryDir = options.resolveTaskDir(options.taskIds[0]);
|
|
6140
6447
|
try {
|
|
6141
|
-
|
|
6448
|
+
fs20.mkdirSync(primaryDir, { recursive: true });
|
|
6142
6449
|
} catch (error) {
|
|
6143
6450
|
stderrWrite(`canon: cannot create task dir for log file: ${error.message}
|
|
6144
6451
|
`);
|
|
@@ -6147,7 +6454,7 @@ function detachAndExit(options) {
|
|
|
6147
6454
|
const logPath = path19.join(primaryDir, LOG_FILENAME);
|
|
6148
6455
|
let logFd;
|
|
6149
6456
|
try {
|
|
6150
|
-
logFd =
|
|
6457
|
+
logFd = fs20.openSync(logPath, "a");
|
|
6151
6458
|
} catch (error) {
|
|
6152
6459
|
stderrWrite(`canon: cannot open ${logPath}: ${error.message}
|
|
6153
6460
|
`);
|
|
@@ -6160,7 +6467,7 @@ function detachAndExit(options) {
|
|
|
6160
6467
|
env: { ...process.env, [DETACH_CHILD_FLAG]: "1" }
|
|
6161
6468
|
});
|
|
6162
6469
|
try {
|
|
6163
|
-
|
|
6470
|
+
fs20.closeSync(logFd);
|
|
6164
6471
|
} catch {
|
|
6165
6472
|
}
|
|
6166
6473
|
if (child.pid == null) {
|
|
@@ -6171,8 +6478,8 @@ function detachAndExit(options) {
|
|
|
6171
6478
|
for (const taskId of options.taskIds) {
|
|
6172
6479
|
try {
|
|
6173
6480
|
const dir = options.resolveTaskDir(taskId);
|
|
6174
|
-
|
|
6175
|
-
|
|
6481
|
+
fs20.mkdirSync(dir, { recursive: true });
|
|
6482
|
+
fs20.writeFileSync(path19.join(dir, PID_FILENAME), `${child.pid}
|
|
6176
6483
|
`, "utf8");
|
|
6177
6484
|
} catch (error) {
|
|
6178
6485
|
pidWriteFailures.push({
|
|
@@ -6228,7 +6535,7 @@ ${rule}
|
|
|
6228
6535
|
function readCanonPid(taskDir) {
|
|
6229
6536
|
const file = path19.join(taskDir, PID_FILENAME);
|
|
6230
6537
|
try {
|
|
6231
|
-
const raw =
|
|
6538
|
+
const raw = fs20.readFileSync(file, "utf8").trim();
|
|
6232
6539
|
const pid = Number.parseInt(raw, 10);
|
|
6233
6540
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
6234
6541
|
} catch {
|
|
@@ -6237,7 +6544,7 @@ function readCanonPid(taskDir) {
|
|
|
6237
6544
|
}
|
|
6238
6545
|
function removeCanonPid(taskDir) {
|
|
6239
6546
|
try {
|
|
6240
|
-
|
|
6547
|
+
fs20.unlinkSync(path19.join(taskDir, PID_FILENAME));
|
|
6241
6548
|
} catch {
|
|
6242
6549
|
}
|
|
6243
6550
|
}
|
|
@@ -6499,16 +6806,14 @@ function autoCommitCode(taskIds, cwd = REPO_ROOT2) {
|
|
|
6499
6806
|
const toStage = handoffFiles.filter((f) => dirtyFiles.has(f));
|
|
6500
6807
|
const gitIgnoredHandoffFiles = filterGitIgnoredPaths(handoffFiles, cwd);
|
|
6501
6808
|
const missing = [];
|
|
6502
|
-
const settledDeletions = /* @__PURE__ */ new Set();
|
|
6503
6809
|
const baseRefForLog = getBaseBranch(taskIds);
|
|
6504
6810
|
for (const f of allHandoffFiles) {
|
|
6505
6811
|
if (dirtyFiles.has(f)) continue;
|
|
6506
6812
|
if (gitIgnoredHandoffFiles.has(f)) continue;
|
|
6507
|
-
const exists =
|
|
6813
|
+
const exists = fs21.existsSync(path20.join(cwd, f));
|
|
6508
6814
|
if (!exists) {
|
|
6509
6815
|
const committed = gitSafeAt(cwd, "log", "--format=%H", "--max-count=1", `${baseRefForLog}..HEAD`, "--", f);
|
|
6510
6816
|
if (committed.ok && committed.stdout.trim()) {
|
|
6511
|
-
settledDeletions.add(f);
|
|
6512
6817
|
continue;
|
|
6513
6818
|
}
|
|
6514
6819
|
missing.push(`${f} \u2014 listed in handoff but missing from working tree (and no commit in ${baseRefForLog}..HEAD touches this path)`);
|
|
@@ -6544,14 +6849,11 @@ ${stagedBeforeUnexpected.map((f) => ` ${f}`).join("\n")}
|
|
|
6544
6849
|
info("Handoff files are already committed or unchanged \u2014 skipping auto-commit.");
|
|
6545
6850
|
return;
|
|
6546
6851
|
}
|
|
6547
|
-
const stageable = handoffFiles
|
|
6548
|
-
if (stageable.length
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
return;
|
|
6852
|
+
const stageable = filterStageablePaths(handoffFiles, cwd);
|
|
6853
|
+
if (stageable.length > 0) {
|
|
6854
|
+
const addResult = gitSafeAt(cwd, "add", "-A", "--", ...stageable);
|
|
6855
|
+
if (!addResult.ok) die2(`Failed to stage files: ${addResult.stderr || "unknown error"}`);
|
|
6552
6856
|
}
|
|
6553
|
-
const addResult = gitSafeAt(cwd, "add", "-A", "--", ...stageable);
|
|
6554
|
-
if (!addResult.ok) die2(`Failed to stage files: ${addResult.stderr || "unknown error"}`);
|
|
6555
6857
|
const preCheck = gitSafeAtRaw(cwd, "status", "--porcelain=v1", "-uall");
|
|
6556
6858
|
const remaining = preCheck.ok ? findUncoveredTrackedChanges(preCheck.stdout, allHandoffFiles) : [];
|
|
6557
6859
|
const stagedAfter = gitSafeAt(cwd, "diff", "--cached", "--name-only");
|
|
@@ -6702,8 +7004,8 @@ function exemptNodeModulesPath(entry, cwd, resolvedWorkspaceDirs) {
|
|
|
6702
7004
|
let cwdReal;
|
|
6703
7005
|
let repoRootReal;
|
|
6704
7006
|
try {
|
|
6705
|
-
cwdReal =
|
|
6706
|
-
repoRootReal =
|
|
7007
|
+
cwdReal = fs21.realpathSync(cwd);
|
|
7008
|
+
repoRootReal = fs21.realpathSync(REPO_ROOT2);
|
|
6707
7009
|
} catch {
|
|
6708
7010
|
return null;
|
|
6709
7011
|
}
|
|
@@ -6767,7 +7069,7 @@ function findPullRequestTemplate(repoRoot) {
|
|
|
6767
7069
|
path20.join(repoRoot, "PULL_REQUEST_TEMPLATE.md")
|
|
6768
7070
|
];
|
|
6769
7071
|
for (const candidate of candidates) {
|
|
6770
|
-
if (
|
|
7072
|
+
if (fs21.existsSync(candidate)) return candidate;
|
|
6771
7073
|
}
|
|
6772
7074
|
return null;
|
|
6773
7075
|
}
|
|
@@ -6787,7 +7089,7 @@ function resolveQaPrBody(taskIds, activeCwd) {
|
|
|
6787
7089
|
}
|
|
6788
7090
|
return {
|
|
6789
7091
|
kind: "fallback",
|
|
6790
|
-
reason:
|
|
7092
|
+
reason: fs21.existsSync(prBodyPath) ? "pr-body.md is still the stub template" : "pr-body.md not found"
|
|
6791
7093
|
};
|
|
6792
7094
|
}
|
|
6793
7095
|
function commitQaArtifacts(taskIds, cwd) {
|
|
@@ -6832,7 +7134,7 @@ Source or test edits must be committed during the implement phase, not left dirt
|
|
|
6832
7134
|
` + stagedBeforeUnexpected.map((filePath) => ` ${filePath}`).join("\n")
|
|
6833
7135
|
);
|
|
6834
7136
|
}
|
|
6835
|
-
for (const relPath of stagePaths) {
|
|
7137
|
+
for (const relPath of filterStageablePaths([...stagePaths], cwd)) {
|
|
6836
7138
|
const exclusions = nodeModulesExclusionArgs(relPath, exemptNodeModulesPaths);
|
|
6837
7139
|
const addResult = gitSafeAt2(cwd, "add", "-A", "--", relPath, ...exclusions);
|
|
6838
7140
|
if (!addResult.ok) {
|
|
@@ -6915,8 +7217,8 @@ function recordPinnedPRNumber(taskIds, prNum) {
|
|
|
6915
7217
|
if (alreadyPinned) return;
|
|
6916
7218
|
for (const taskId of taskIds) {
|
|
6917
7219
|
const sidecarPath = sidecarPathFor(taskId);
|
|
6918
|
-
|
|
6919
|
-
|
|
7220
|
+
fs21.mkdirSync(path20.dirname(sidecarPath), { recursive: true });
|
|
7221
|
+
fs21.writeFileSync(sidecarPath, String(prNum), "utf8");
|
|
6920
7222
|
}
|
|
6921
7223
|
}
|
|
6922
7224
|
function reportOrCreatePR(taskIds, branchName) {
|
|
@@ -7037,7 +7339,7 @@ function commitHumanReviewFiles(taskIds, cwd, createPR) {
|
|
|
7037
7339
|
);
|
|
7038
7340
|
}
|
|
7039
7341
|
const docsRefsScript = path20.join(REPO_ROOT2, "scripts", "docs-refs-check.mjs");
|
|
7040
|
-
if (
|
|
7342
|
+
if (fs21.existsSync(docsRefsScript)) {
|
|
7041
7343
|
const docsRefsResult = spawnSync6("node", [docsRefsScript], {
|
|
7042
7344
|
cwd,
|
|
7043
7345
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -7067,27 +7369,12 @@ This failure cannot be bypassed with --force.`
|
|
|
7067
7369
|
);
|
|
7068
7370
|
} else if (baseDriftResult.drift.length > 0) {
|
|
7069
7371
|
if (!cliArgs.force) {
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
The allowlist is: tasks/<id>/**, PIPELINE_TELEMETRY_FILES, files listed in
|
|
7075
|
-
your spec's '### Affected Files' table (directory-form entries like 'dist/' match
|
|
7076
|
-
subpaths), and PIPELINE_MANAGED_DOCS (auto-allowlisted once qa.status = done).
|
|
7077
|
-
If this is a legitimate task change, add the path to spec.md '### Affected Files'
|
|
7078
|
-
and rerun. For a rename, list BOTH the old and new paths. If the drift is
|
|
7079
|
-
unexpected (likely cross-pipeline contamination from a sibling worktree's
|
|
7080
|
-
managed-doc sync, OR a third-party commit landed on origin/${baseBranch} while
|
|
7081
|
-
this pipeline was running), recover with one of:
|
|
7082
|
-
- rebase onto current origin/${baseBranch} to absorb the base advance:
|
|
7083
|
-
git fetch origin ${baseBranch} && git rebase origin/${baseBranch}
|
|
7084
|
-
- reset a specific file to base's content if a stray task-branch commit
|
|
7085
|
-
introduced it:
|
|
7086
|
-
git checkout origin/${baseBranch} -- <path> && git commit -m 'revert drift on <path>'
|
|
7087
|
-
- revert the offending task-branch commit entirely:
|
|
7088
|
-
git revert <sha>
|
|
7089
|
-
Bypass with --force if you've verified the drift is intentional.`
|
|
7372
|
+
const taskChangedFiles = new Set(getAffectedFiles(`origin/${baseBranch}`, cwd));
|
|
7373
|
+
const classification = classifyBaseDriftFilesFromData(
|
|
7374
|
+
baseDriftResult.drift,
|
|
7375
|
+
taskChangedFiles
|
|
7090
7376
|
);
|
|
7377
|
+
die2(buildBaseDriftAbortMessage(baseBranch, classification));
|
|
7091
7378
|
}
|
|
7092
7379
|
warn2(
|
|
7093
7380
|
`--force override: base-drift detected; proceeding at user request. Drifted files:
|
|
@@ -7191,7 +7478,7 @@ If this is a source or test file, it should have been committed during the imple
|
|
|
7191
7478
|
Unstage them or list them in the task artifacts before rerunning.`
|
|
7192
7479
|
);
|
|
7193
7480
|
}
|
|
7194
|
-
for (const relPath of stagePaths) {
|
|
7481
|
+
for (const relPath of filterStageablePaths([...stagePaths], cwd)) {
|
|
7195
7482
|
const exclusions = nodeModulesExclusionArgs(relPath, exemptNodeModulesPaths);
|
|
7196
7483
|
const addResult = gitSafeAt2(cwd, "add", "-A", "--", relPath, ...exclusions);
|
|
7197
7484
|
if (!addResult.ok) {
|
|
@@ -7399,7 +7686,7 @@ function readSidecarPRNumber(taskId, taskDir = taskDirFor2(taskId)) {
|
|
|
7399
7686
|
const sidecarPath = sidecarPathFor(taskId, taskDir);
|
|
7400
7687
|
let raw;
|
|
7401
7688
|
try {
|
|
7402
|
-
raw =
|
|
7689
|
+
raw = fs21.readFileSync(sidecarPath, "utf8").trim();
|
|
7403
7690
|
} catch {
|
|
7404
7691
|
return null;
|
|
7405
7692
|
}
|
|
@@ -7546,7 +7833,7 @@ function mergeOpenPRsAndPull(taskIds, baseBranch, branchByTaskId) {
|
|
|
7546
7833
|
}
|
|
7547
7834
|
function runPostMergeHook() {
|
|
7548
7835
|
const hookPath = path20.join(REPO_ROOT2, ".canon/hooks/post-merge.sh");
|
|
7549
|
-
if (!
|
|
7836
|
+
if (!fs21.existsSync(hookPath)) return;
|
|
7550
7837
|
info2("Running .canon/hooks/post-merge.sh...");
|
|
7551
7838
|
const result = runCommand2("bash", [hookPath]);
|
|
7552
7839
|
if (!result.ok) {
|
|
@@ -7574,8 +7861,8 @@ function rewriteArchivedTaskRefs(taskIds) {
|
|
|
7574
7861
|
path20.join(REPO_ROOT2, "docs", "task-quality-log.md")
|
|
7575
7862
|
];
|
|
7576
7863
|
for (const filePath of targets) {
|
|
7577
|
-
if (!
|
|
7578
|
-
let content =
|
|
7864
|
+
if (!fs21.existsSync(filePath)) continue;
|
|
7865
|
+
let content = fs21.readFileSync(filePath, "utf8");
|
|
7579
7866
|
let changed = false;
|
|
7580
7867
|
for (const taskId of taskIds) {
|
|
7581
7868
|
const stale = `tasks/${taskId}/`;
|
|
@@ -7586,7 +7873,7 @@ function rewriteArchivedTaskRefs(taskIds) {
|
|
|
7586
7873
|
}
|
|
7587
7874
|
}
|
|
7588
7875
|
if (changed) {
|
|
7589
|
-
|
|
7876
|
+
fs21.writeFileSync(filePath, content, "utf8");
|
|
7590
7877
|
info2(`Updated stale task refs in ${path20.relative(REPO_ROOT2, filePath)}.`);
|
|
7591
7878
|
}
|
|
7592
7879
|
}
|
|
@@ -7620,7 +7907,7 @@ function classifyAndPreserveSharedDocDirt() {
|
|
|
7620
7907
|
if (porcelainCode !== " M") {
|
|
7621
7908
|
return { relPath, docClass, porcelainCode, headContent: null, workingContent: null };
|
|
7622
7909
|
}
|
|
7623
|
-
const workingContent =
|
|
7910
|
+
const workingContent = fs21.readFileSync(path20.join(REPO_ROOT2, relPath), "utf8");
|
|
7624
7911
|
const headResult = gitSafeAtRaw(REPO_ROOT2, "show", `HEAD:${relPath}`);
|
|
7625
7912
|
return {
|
|
7626
7913
|
relPath,
|
|
@@ -7637,11 +7924,11 @@ function classifyAndPreserveSharedDocDirt() {
|
|
|
7637
7924
|
}
|
|
7638
7925
|
const preserve = verdict.preserve;
|
|
7639
7926
|
if (preserve.length === 0) return [];
|
|
7640
|
-
const backupDir =
|
|
7927
|
+
const backupDir = fs21.mkdtempSync(path20.join(os.tmpdir(), "canon-ship-shared-doc-backup-"));
|
|
7641
7928
|
const preserved = [];
|
|
7642
7929
|
for (const { relPath, suffix } of preserve) {
|
|
7643
7930
|
const backupPath = path20.join(backupDir, relPath.replace(/[\\/]/g, "__"));
|
|
7644
|
-
|
|
7931
|
+
fs21.writeFileSync(backupPath, suffix, "utf8");
|
|
7645
7932
|
info2(`Preserving uncommitted ${relPath} dirt during --ship; backup: ${backupPath}`);
|
|
7646
7933
|
const checkoutResult = gitSafe("checkout", "HEAD", "--", relPath);
|
|
7647
7934
|
if (!checkoutResult.ok) {
|
|
@@ -7669,7 +7956,7 @@ function shipTasks(taskIds) {
|
|
|
7669
7956
|
path20.join(taskDirForRepoRoot2(taskId), "status.json")
|
|
7670
7957
|
];
|
|
7671
7958
|
for (const candidate of candidates) {
|
|
7672
|
-
if (
|
|
7959
|
+
if (fs21.existsSync(candidate)) return readStatusFromPath(candidate, taskId);
|
|
7673
7960
|
}
|
|
7674
7961
|
const snapshot = taskStatuses.get(taskId);
|
|
7675
7962
|
if (snapshot) return snapshot;
|
|
@@ -7810,7 +8097,7 @@ Recovery:
|
|
|
7810
8097
|
);
|
|
7811
8098
|
}
|
|
7812
8099
|
const archiveDir = path20.join(TASKS_DIR2, "_archive");
|
|
7813
|
-
if (!
|
|
8100
|
+
if (!fs21.existsSync(archiveDir)) fs21.mkdirSync(archiveDir, { recursive: true });
|
|
7814
8101
|
const localBranchesToDelete = [];
|
|
7815
8102
|
for (const taskId of taskIds) {
|
|
7816
8103
|
const { worktree: hasWorktree } = taskSnapshot(taskId);
|
|
@@ -7822,7 +8109,7 @@ Recovery:
|
|
|
7822
8109
|
writeStatusToFile(path20.join(REPO_ROOT2, "tasks", taskId, "status.json"), status);
|
|
7823
8110
|
const src = taskDirForRepoRoot2(taskId);
|
|
7824
8111
|
const dest = path20.join(archiveDir, taskId);
|
|
7825
|
-
|
|
8112
|
+
fs21.renameSync(src, dest);
|
|
7826
8113
|
info2(`\u{1F4E6} ${taskId} \u2192 tasks/_archive/${taskId}`);
|
|
7827
8114
|
const branchName = taskSnapshot(taskId).branch;
|
|
7828
8115
|
if (branchExistsLocally(branchName)) localBranchesToDelete.push(branchName);
|
|
@@ -7838,8 +8125,8 @@ Recovery:
|
|
|
7838
8125
|
]);
|
|
7839
8126
|
stageArchiveChanges(stagedPaths);
|
|
7840
8127
|
for (const { relPath, suffix, backupPath } of preservedSharedDocDirt) {
|
|
7841
|
-
|
|
7842
|
-
|
|
8128
|
+
fs21.appendFileSync(path20.join(REPO_ROOT2, relPath), suffix, "utf8");
|
|
8129
|
+
fs21.rmSync(backupPath, { force: true });
|
|
7843
8130
|
info2(`Re-applied preserved ${relPath} dirt as uncommitted changes; backup removed.`);
|
|
7844
8131
|
}
|
|
7845
8132
|
const archiveCommit = commitArchiveChanges(taskIds, baseBranch);
|
|
@@ -8090,7 +8377,7 @@ async function runPhase(phase, state) {
|
|
|
8090
8377
|
if (phase === "qa") {
|
|
8091
8378
|
const activeCwd = getActiveCwd(taskIds);
|
|
8092
8379
|
const qaTemplatePath = state.isBundle ? null : findPullRequestTemplate(activeCwd) ?? findPullRequestTemplate(REPO_ROOT2);
|
|
8093
|
-
const resolvedPrTemplate = qaTemplatePath ?
|
|
8380
|
+
const resolvedPrTemplate = qaTemplatePath ? fs21.readFileSync(qaTemplatePath, "utf8") : null;
|
|
8094
8381
|
return runQaPhase(state, cliArgs.interactive, resolvedPrTemplate);
|
|
8095
8382
|
}
|
|
8096
8383
|
if (phase === "human_review") {
|
|
@@ -8173,7 +8460,7 @@ var extractCheckedVerdict2 = extractCheckedVerdict;
|
|
|
8173
8460
|
function readArtifact(taskId, name) {
|
|
8174
8461
|
const p = path20.join(taskDirFor2(taskId), name);
|
|
8175
8462
|
try {
|
|
8176
|
-
return
|
|
8463
|
+
return fs21.readFileSync(p, "utf8");
|
|
8177
8464
|
} catch {
|
|
8178
8465
|
return null;
|
|
8179
8466
|
}
|
|
@@ -8197,7 +8484,7 @@ function checkImplementEvidence(taskId) {
|
|
|
8197
8484
|
const sForEvidence = readStatus(taskId);
|
|
8198
8485
|
if (sForEvidence.worktree === true) {
|
|
8199
8486
|
const wt = worktreePath(taskId);
|
|
8200
|
-
if (
|
|
8487
|
+
if (fs21.existsSync(wt)) checkRoots.push(wt);
|
|
8201
8488
|
}
|
|
8202
8489
|
const ignoreCwd = checkRoots[checkRoots.length - 1];
|
|
8203
8490
|
const gitIgnored = filterGitIgnoredPaths(files, ignoreCwd);
|
|
@@ -8209,12 +8496,13 @@ function checkImplementEvidence(taskId) {
|
|
|
8209
8496
|
};
|
|
8210
8497
|
}
|
|
8211
8498
|
const existingFiles = verifiableFiles.filter(
|
|
8212
|
-
(f) => checkRoots.some((root) =>
|
|
8499
|
+
(f) => checkRoots.some((root) => fs21.existsSync(path20.join(root, f)))
|
|
8213
8500
|
);
|
|
8214
8501
|
if (existingFiles.length === 0) {
|
|
8215
8502
|
const evidenceCwd = checkRoots[checkRoots.length - 1];
|
|
8216
|
-
const
|
|
8217
|
-
|
|
8503
|
+
const deletedDiff = gitSafeAt(evidenceCwd, "diff", "HEAD", "--name-only", "--no-renames", "--diff-filter=D");
|
|
8504
|
+
const deletedAgainstHead = new Set(
|
|
8505
|
+
(deletedDiff.ok ? deletedDiff.stdout : "").split("\n").map((l) => l.trim()).filter(Boolean)
|
|
8218
8506
|
);
|
|
8219
8507
|
const baseBranch = sForEvidence.base_branch || getDefaultBaseBranch();
|
|
8220
8508
|
const committedDiff = gitSafeAt(
|
|
@@ -8227,7 +8515,7 @@ function checkImplementEvidence(taskId) {
|
|
|
8227
8515
|
committedDiff.stdout.split("\n").filter((l) => l.startsWith("D")).map((l) => l.split(" ")[1]?.trim()).filter((p) => Boolean(p))
|
|
8228
8516
|
);
|
|
8229
8517
|
const deletedFiles = verifiableFiles.filter(
|
|
8230
|
-
(f) =>
|
|
8518
|
+
(f) => deletedAgainstHead.has(f) || deletedInCommits.has(f)
|
|
8231
8519
|
);
|
|
8232
8520
|
if (deletedFiles.length === 0) {
|
|
8233
8521
|
return { advanced: false, note: `handoff.md lists ${files.length} file(s) but none exist on disk or are git-tracked deletions` };
|
|
@@ -8394,8 +8682,30 @@ async function recoverPhaseForTask(taskId, phase, initialStatus) {
|
|
|
8394
8682
|
warn2(`Retry did not recover '${taskId}' ${phase} (${postEvidence.note}).`);
|
|
8395
8683
|
return false;
|
|
8396
8684
|
}
|
|
8685
|
+
function stopForReviewerBlock(taskIds, blockedIds, statuses) {
|
|
8686
|
+
const maxIter = statuses.reduce((max, status) => Math.max(max, getIterations(status)), 0);
|
|
8687
|
+
const recovery = taskIds.map((id) => `canon task reset-code-review ${id}`).join("; ");
|
|
8688
|
+
const reason = `Code review stopped without a verdict for task(s): ${blockedIds.join(", ")}. The reviewer set code_review blocked; see ${taskIds.map((id) => `tasks/${id}/review.md`).join(", ")}. No evidence recovery or retry was attempted. Recovery for the full blocked bundle [${taskIds.join(" ")}]: ${recovery}; then canon run ${taskIds.join(" ")}.`;
|
|
8689
|
+
console.log("");
|
|
8690
|
+
console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
8691
|
+
console.log(" \u270B CODE REVIEW STOPPED \u2014 the reviewer could not reach a verdict.");
|
|
8692
|
+
console.log(" Read the stopped review:");
|
|
8693
|
+
for (const id of taskIds) console.log(` tasks/${id}/review.md`);
|
|
8694
|
+
console.log(" Reset every task in the bundle, then run again:");
|
|
8695
|
+
for (const id of taskIds) console.log(` canon task reset-code-review ${id}`);
|
|
8696
|
+
console.log(` canon run ${taskIds.join(" ")}`);
|
|
8697
|
+
console.log(" Re-running without the reset starts a fresh review pass without archiving the stopped review.md.");
|
|
8698
|
+
console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
8699
|
+
console.log("");
|
|
8700
|
+
autoBlockPhase(taskIds, "code_review", maxIter, reason);
|
|
8701
|
+
process.exit(2);
|
|
8702
|
+
}
|
|
8397
8703
|
async function checkAndRoute(phase, taskIds) {
|
|
8398
8704
|
let statuses = taskIds.map(readStatus);
|
|
8705
|
+
if (phase === "code_review") {
|
|
8706
|
+
const blockedIds = taskIds.filter((_, index) => getPhaseStatus(statuses[index], "code_review") === "blocked");
|
|
8707
|
+
if (blockedIds.length > 0) stopForReviewerBlock(taskIds, blockedIds, statuses);
|
|
8708
|
+
}
|
|
8399
8709
|
for (let i = 0; i < taskIds.length; i += 1) {
|
|
8400
8710
|
const phaseStatus = getPhaseStatus(statuses[i], phase);
|
|
8401
8711
|
if (phase === "implement" && phaseStatus === "done") {
|
|
@@ -8430,6 +8740,35 @@ async function checkAndRoute(phase, taskIds) {
|
|
|
8430
8740
|
}
|
|
8431
8741
|
}
|
|
8432
8742
|
statuses = taskIds.map(readStatus);
|
|
8743
|
+
let specGapScopeFiles = [];
|
|
8744
|
+
if (phase === "code_review" && statuses.every((status) => status.full_send === true)) {
|
|
8745
|
+
const cwd = getActiveCwd(taskIds);
|
|
8746
|
+
const baseBranch = getBaseBranch(taskIds);
|
|
8747
|
+
const changedFiles = getAffectedFiles(baseBranch, cwd);
|
|
8748
|
+
const allowlist = buildAffectedFilesAllowlist(taskIds, { admitManagedDocs: true });
|
|
8749
|
+
const verdicts = statuses.map((status) => getVerdict(status, "code_review"));
|
|
8750
|
+
if (verdicts.includes("spec_gap")) {
|
|
8751
|
+
specGapScopeFiles = verifyBaseDriftFromData(
|
|
8752
|
+
changedFiles,
|
|
8753
|
+
allowlist.paths,
|
|
8754
|
+
taskIds,
|
|
8755
|
+
allowlist.prefixes
|
|
8756
|
+
);
|
|
8757
|
+
}
|
|
8758
|
+
const unjudged = findUnjudgedFullSendFilesFromData(
|
|
8759
|
+
changedFiles,
|
|
8760
|
+
allowlist,
|
|
8761
|
+
taskIds,
|
|
8762
|
+
verdicts
|
|
8763
|
+
);
|
|
8764
|
+
if (unjudged.length > 0) {
|
|
8765
|
+
const reason = fullSendScopeBlockReason(taskIds, unjudged, verdicts.filter(Boolean));
|
|
8766
|
+
warn2(reason);
|
|
8767
|
+
const maxIter = statuses.reduce((max, status) => Math.max(max, getIterations(status)), 0);
|
|
8768
|
+
autoBlockPhase(taskIds, "code_review", maxIter, reason);
|
|
8769
|
+
process.exit(2);
|
|
8770
|
+
}
|
|
8771
|
+
}
|
|
8433
8772
|
if (lastCodexExitStatus !== 0) {
|
|
8434
8773
|
warn2(`Phase '${phase}' completed despite Codex exit status ${lastCodexExitStatus} (likely MCP warnings). Continuing.`);
|
|
8435
8774
|
lastCodexExitStatus = 0;
|
|
@@ -8505,7 +8844,8 @@ async function checkAndRoute(phase, taskIds) {
|
|
|
8505
8844
|
const specGapIds = taskIds.filter((_, index) => getVerdict(statuses[index], "code_review") === "spec_gap");
|
|
8506
8845
|
if (specGapIds.length > 0) {
|
|
8507
8846
|
const maxIter = statuses.reduce((max, s) => Math.max(max, getIterations(s)), 0);
|
|
8508
|
-
const
|
|
8847
|
+
const scopeNote = specGapScopeFiles.length > 0 ? ` Full-send files remain outside Affected Files: ${specGapScopeFiles.join(", ")}. A BLESS accepts the verdict but does not amend the spec, so --pr will still reject these files: before blessing, add them to the spec's Affected Files (as an ## Amendment) or remove them from the branch.` : "";
|
|
8848
|
+
const reason = `Code review surfaced a spec_gap verdict for task(s): ${specGapIds.join(", ")}. The implementation cannot resolve this \u2014 the root cause is in the spec. ` + scopeNote + `Recovery options (both operate on the full blocked bundle [${taskIds.join(" ")}]):
|
|
8509
8849
|
FIX: amend spec.md with ## Amendment, then: canon run ${taskIds.join(" ")} --reroute
|
|
8510
8850
|
BLESS: canon task accept ${taskIds.join(" ")} code_review --reason "<why>"`;
|
|
8511
8851
|
console.log("");
|
|
@@ -8515,6 +8855,12 @@ async function checkAndRoute(phase, taskIds) {
|
|
|
8515
8855
|
console.log(" The code review found a problem in the spec, not a fixable");
|
|
8516
8856
|
console.log(" implementation bug. Review the findings:");
|
|
8517
8857
|
for (const id of specGapIds) console.log(` tasks/${id}/review.md`);
|
|
8858
|
+
if (specGapScopeFiles.length > 0) {
|
|
8859
|
+
console.log(" Full-send files still outside Affected Files:");
|
|
8860
|
+
for (const file of specGapScopeFiles) console.log(` ${file}`);
|
|
8861
|
+
console.log(" BLESS does not amend the spec, so --pr will still reject these files.");
|
|
8862
|
+
console.log(" Before blessing, add them to Affected Files (## Amendment) or remove them.");
|
|
8863
|
+
}
|
|
8518
8864
|
console.log("");
|
|
8519
8865
|
console.log(" Two recovery options:");
|
|
8520
8866
|
console.log("");
|
|
@@ -8565,8 +8911,8 @@ function checkDeps(taskIds, skipAgentDeps = false) {
|
|
|
8565
8911
|
for (const taskId of taskIds) {
|
|
8566
8912
|
validateTaskId(taskId);
|
|
8567
8913
|
const repoRootStatusFile = path20.join(REPO_ROOT2, "tasks", taskId, "status.json");
|
|
8568
|
-
const statusFile = cliArgs.ship &&
|
|
8569
|
-
if (!
|
|
8914
|
+
const statusFile = cliArgs.ship && fs21.existsSync(repoRootStatusFile) ? repoRootStatusFile : statusFileFor(taskId);
|
|
8915
|
+
if (!fs21.existsSync(statusFile)) {
|
|
8570
8916
|
die(`No status.json at tasks/${taskId}/status.json \u2014 run canon task new ${taskId} first`);
|
|
8571
8917
|
}
|
|
8572
8918
|
}
|
|
@@ -8608,7 +8954,7 @@ async function main() {
|
|
|
8608
8954
|
let heartbeatStarted = false;
|
|
8609
8955
|
const earlyHeartbeatResolver = (id) => {
|
|
8610
8956
|
const repoRootStatusFile = path20.join(REPO_ROOT2, "tasks", id, "status.json");
|
|
8611
|
-
return path20.dirname(cliArgs.ship &&
|
|
8957
|
+
return path20.dirname(cliArgs.ship && fs21.existsSync(repoRootStatusFile) ? repoRootStatusFile : statusFileFor(id));
|
|
8612
8958
|
};
|
|
8613
8959
|
if (!cliArgs.ship && !cliArgs.dryRun) {
|
|
8614
8960
|
guardConcurrentRun(cliArgs.taskIds, earlyHeartbeatResolver);
|