coding-agent-harness 1.0.2 → 1.0.4
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 +25 -0
- package/CONTRIBUTING.md +98 -0
- package/README.md +211 -86
- package/README.zh-CN.md +54 -34
- package/SKILL.md +25 -18
- package/docs-release/README.md +9 -5
- package/docs-release/architecture/overview.md +17 -5
- package/docs-release/architecture/overview.zh-CN.md +9 -5
- package/docs-release/assets/dashboard-overview.png +0 -0
- package/docs-release/guides/agent-installation.en-US.md +31 -8
- package/docs-release/guides/agent-installation.md +34 -9
- package/docs-release/guides/contributing.md +100 -0
- package/docs-release/guides/contributing.zh-CN.md +99 -0
- package/docs-release/guides/document-audience-and-surfaces.en-US.md +3 -2
- package/docs-release/guides/document-audience-and-surfaces.md +3 -2
- package/docs-release/guides/full-legacy-migration-subagent-strategy.md +2 -2
- package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +2 -2
- package/docs-release/guides/legacy-migration-agent-prompt.md +0 -11
- package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +0 -11
- package/docs-release/guides/migration-playbook.en-US.md +14 -15
- package/docs-release/guides/migration-playbook.md +14 -15
- package/docs-release/guides/parent-control-repository-pattern.en-US.md +7 -5
- package/docs-release/guides/parent-control-repository-pattern.md +7 -5
- package/docs-release/guides/preset-development.md +214 -0
- package/docs-release/guides/repository-operating-models.en-US.md +5 -4
- package/docs-release/guides/repository-operating-models.md +5 -4
- package/docs-release/guides/task-state-machine.en-US.md +207 -0
- package/docs-release/guides/task-state-machine.md +214 -0
- package/docs-release/intl/en-US.md +1 -1
- package/docs-release/intl/zh-CN.md +1 -1
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
- package/package.json +8 -3
- package/presets/legacy-migration/checks/preset-check.mjs +3 -0
- package/presets/legacy-migration/preset.yaml +134 -0
- package/presets/legacy-migration/scripts/plan-work-queue.mjs +4 -0
- package/presets/legacy-migration/scripts/scaffold-task-contracts.mjs +4 -0
- package/presets/legacy-migration/templates/execution_strategy.append.md +18 -0
- package/presets/legacy-migration/templates/findings.seed.md +17 -0
- package/presets/legacy-migration/templates/review.seed.md +12 -0
- package/presets/legacy-migration/templates/task_plan.append.md +9 -0
- package/presets/legacy-migration/templates/visual_map.append.md +12 -0
- package/presets/legacy-migration/workbench/dashboard-panels.yaml +2 -0
- package/presets/legacy-migration/workbench/migration-queue.schema.json +23 -0
- package/presets/lesson-sedimentation/preset.yaml +23 -0
- package/presets/lesson-sedimentation/templates/prompt.md +23 -0
- package/presets/module/preset.yaml +25 -0
- package/presets/module/templates/execution_strategy.append.md +8 -0
- package/presets/module/templates/task_plan.append.md +17 -0
- package/presets/standard-task/preset.yaml +31 -0
- package/presets/standard-task/templates/task_plan.append.md +7 -0
- package/references/adversarial-review-standard.md +2 -2
- package/references/agents-md-pattern.md +2 -2
- package/references/delivery-operating-model-standard.md +3 -3
- package/references/docs-directory-standard.md +6 -7
- package/references/harness-ledger.md +53 -96
- package/references/lessons-governance.md +88 -93
- package/references/module-parallel-standard.md +14 -14
- package/references/planning-loop.md +12 -6
- package/references/pull-request-standard.md +118 -0
- package/references/repo-governance-standard.md +11 -2
- package/references/review-routing-standard.md +7 -1
- package/references/ssot-governance.md +67 -59
- package/references/taskr-gap-analysis.md +600 -0
- package/references/walkthrough-closeout.md +7 -7
- package/scripts/check-harness.mjs +40 -301
- package/scripts/commands/dashboard-command.mjs +67 -0
- package/scripts/commands/migration-command.mjs +96 -0
- package/scripts/commands/preset-command.mjs +73 -0
- package/scripts/commands/task-command.mjs +327 -0
- package/scripts/harness.mjs +55 -260
- package/scripts/lib/capability-registry.mjs +66 -8
- package/scripts/lib/check-module-parallel.mjs +237 -0
- package/scripts/lib/check-profiles.mjs +61 -153
- package/scripts/lib/check-task-contracts.mjs +47 -0
- package/scripts/lib/core-shared.mjs +10 -0
- package/scripts/lib/dashboard-data.mjs +29 -6
- package/scripts/lib/dashboard-workbench.mjs +52 -12
- package/scripts/lib/dashboard-writer.mjs +14 -2
- package/scripts/lib/git-status-summary.mjs +46 -0
- package/scripts/lib/governance-index-generator.mjs +174 -0
- package/scripts/lib/governance-sync.mjs +514 -0
- package/scripts/lib/governance-table-boundary.mjs +175 -0
- package/scripts/lib/harness-core.mjs +5 -0
- package/scripts/lib/lesson-maintenance.mjs +36 -29
- package/scripts/lib/migration-support.mjs +1 -1
- package/scripts/lib/preset-audit-contracts.mjs +37 -0
- package/scripts/lib/preset-engine.mjs +497 -0
- package/scripts/lib/preset-registry.mjs +627 -0
- package/scripts/lib/preset-resource-contracts.mjs +83 -0
- package/scripts/lib/review-confirm-git-gate.mjs +248 -0
- package/scripts/lib/status-dashboard-renderer.mjs +102 -0
- package/scripts/lib/subagent-authorization-audit.mjs +196 -0
- package/scripts/lib/task-completion-consistency.mjs +16 -0
- package/scripts/lib/task-index.mjs +93 -0
- package/scripts/lib/task-lesson-candidates.mjs +242 -0
- package/scripts/lib/task-lesson-sedimentation.mjs +326 -0
- package/scripts/lib/task-lifecycle/review-confirm.mjs +101 -0
- package/scripts/lib/task-lifecycle/review-gates.mjs +70 -0
- package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
- package/scripts/lib/task-lifecycle.mjs +297 -403
- package/scripts/lib/task-review-model.mjs +469 -0
- package/scripts/lib/task-scanner.mjs +130 -236
- package/scripts/lib/task-tombstone-commands.mjs +140 -0
- package/scripts/postinstall.mjs +14 -0
- package/skills/preset-creator/SKILL.md +179 -0
- package/skills/preset-creator/references/complex-task-skeleton/README.md +31 -0
- package/skills/preset-creator/references/complex-task-skeleton/artifacts/INDEX.md +12 -0
- package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -0
- package/skills/preset-creator/references/complex-task-skeleton/execution_strategy.md +71 -0
- package/skills/preset-creator/references/complex-task-skeleton/findings.md +24 -0
- package/skills/preset-creator/references/complex-task-skeleton/lesson_candidates.md +70 -0
- package/skills/preset-creator/references/complex-task-skeleton/long-running-task-contract.md +76 -0
- package/skills/preset-creator/references/complex-task-skeleton/progress.md +33 -0
- package/skills/preset-creator/references/complex-task-skeleton/references/INDEX.md +13 -0
- package/skills/preset-creator/references/complex-task-skeleton/review.md +107 -0
- package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +111 -0
- package/skills/preset-creator/references/complex-task-skeleton/visual_map.md +50 -0
- package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
- package/templates/AGENTS.md.template +19 -15
- package/templates/dashboard/assets/app-src/00-state.js +1 -0
- package/templates/dashboard/assets/app-src/10-router.js +2 -1
- package/templates/dashboard/assets/app-src/20-overview.js +11 -5
- package/templates/dashboard/assets/app-src/30-tasks.js +92 -246
- package/templates/dashboard/assets/app-src/35-task-detail.js +246 -0
- package/templates/dashboard/assets/app-src/45-review.js +241 -22
- package/templates/dashboard/assets/app-src/50-migration.js +24 -10
- package/templates/dashboard/assets/app-src/90-bindings.js +171 -29
- package/templates/dashboard/assets/app.css +698 -156
- package/templates/dashboard/assets/app.css.manifest.json +9 -0
- package/templates/dashboard/assets/app.js +662 -91
- package/templates/dashboard/assets/app.manifest.json +1 -0
- package/templates/dashboard/assets/css-src/00-foundation.css +342 -0
- package/templates/dashboard/assets/css-src/10-panels-flow.css +236 -0
- package/templates/dashboard/assets/css-src/20-briefs-controls.css +398 -0
- package/templates/dashboard/assets/css-src/30-task-index.css +739 -0
- package/templates/dashboard/assets/css-src/35-review-workspace.css +507 -0
- package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +427 -0
- package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
- package/templates/dashboard/assets/i18n.js +123 -21
- package/templates/ledger/Harness-Ledger.md +13 -25
- package/templates/lessons/lesson-arch-process-change.md +1 -1
- package/templates/lessons/lesson-new-doc.md +1 -1
- package/templates/lessons/lesson-ref-change.md +1 -1
- package/templates/planning/execution_strategy.md +31 -0
- package/templates/planning/lesson_candidates.md +18 -6
- package/templates/planning/optional/artifacts/INDEX.md +3 -3
- package/templates/planning/optional/references/INDEX.md +3 -3
- package/templates/planning/review.md +59 -0
- package/templates/planning/task_plan.md +36 -13
- package/templates/reference/execution-workflow-standard.md +4 -3
- package/templates/reference/pull-request-standard.md +80 -0
- package/templates/reference/repo-governance-standard.md +7 -6
- package/templates/reference/review-routing-standard.md +6 -0
- package/templates/reference/walkthrough-standard.md +2 -1
- package/templates/verifier/verifier-output.md +1 -1
- package/templates-zh-CN/AGENTS.md.template +20 -16
- package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
- package/templates-zh-CN/planning/execution_strategy.md +30 -0
- package/templates-zh-CN/planning/lesson_candidates.md +18 -6
- package/templates-zh-CN/planning/review.md +59 -1
- package/templates-zh-CN/planning/task_plan.md +30 -10
- package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
- package/templates-zh-CN/reference/docs-library-standard.md +1 -1
- package/templates-zh-CN/reference/execution-workflow-standard.md +4 -3
- package/templates-zh-CN/reference/harness-ledger-standard.md +2 -2
- package/templates-zh-CN/reference/pull-request-standard.md +106 -0
- package/templates-zh-CN/reference/repo-governance-standard.md +4 -3
- package/templates-zh-CN/reference/review-routing-standard.md +8 -1
- package/templates-zh-CN/reference/walkthrough-standard.md +3 -2
- package/templates-zh-CN/walkthrough/Closeout-SSoT.md +1 -1
- package/docs-release/assets/dashboard-overview-en.png +0 -0
- package/scripts/smoke-dashboard.mjs +0 -92
- package/scripts/test-harness.mjs +0 -1395
- package/templates/ssot/Feature-SSoT.md +0 -43
- package/templates/ssot/Lessons-SSoT.md +0 -44
- package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
- package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
lessonCandidatesFile,
|
|
5
|
+
longRunningTaskContractFile,
|
|
6
|
+
toPosix,
|
|
7
|
+
visualMapFile,
|
|
8
|
+
} from "./core-shared.mjs";
|
|
9
|
+
import {
|
|
10
|
+
firstColumn,
|
|
11
|
+
splitList,
|
|
12
|
+
splitMarkdownRow,
|
|
13
|
+
tableAfterHeading,
|
|
14
|
+
} from "./markdown-utils.mjs";
|
|
15
|
+
import { validateReviewConfirmationGitAudit } from "./review-confirm-git-gate.mjs";
|
|
16
|
+
import { isLessonCandidateDecisionComplete } from "./task-lesson-candidates.mjs";
|
|
17
|
+
|
|
18
|
+
export const taskScannerVersion = "task-scanner/2026-05-23-lifecycle-queues";
|
|
19
|
+
export const reviewFindingColumns = {
|
|
20
|
+
severity: ["Severity", "严重级别", "优先级"],
|
|
21
|
+
finding: ["Finding", "发现"],
|
|
22
|
+
open: ["Open", "是否开放"],
|
|
23
|
+
blocksRelease: ["Blocks Release", "是否阻塞发布", "阻塞发布", "阻塞确认"],
|
|
24
|
+
disposition: ["Disposition", "处置", "处理结论"],
|
|
25
|
+
waiverBy: ["Waiver By", "豁免人"],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function normalizeReviewBoolean(value) {
|
|
29
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
30
|
+
if (/^(yes|true|open|是|开放)$/.test(raw)) return "yes";
|
|
31
|
+
if (/^(no|false|closed|fixed|done|否|关闭|已关闭|已修复)$/.test(raw)) return "no";
|
|
32
|
+
return raw;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseMetadataLine(content, labels) {
|
|
36
|
+
const escaped = labels.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
37
|
+
const match = String(content || "").match(new RegExp(`^(?:${escaped})\\s*[::]\\s*([^\\n]+)`, "im"));
|
|
38
|
+
return match ? match[1].replace(/`/g, "").trim() : "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeMetadataValue(value, fallback = "") {
|
|
42
|
+
const normalized = String(value || "")
|
|
43
|
+
.replace(/`/g, "")
|
|
44
|
+
.trim()
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.replaceAll("_", "-")
|
|
47
|
+
.replace(/\s+/g, "-");
|
|
48
|
+
return normalized || fallback;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function parseTaskIdentity(taskPlanContent, fallbackTaskId) {
|
|
52
|
+
const taskKey =
|
|
53
|
+
parseMetadataLine(taskPlanContent, ["Task Key", "任务主键"]) ||
|
|
54
|
+
parseMetadataLine(taskPlanContent, ["Task ID", "任务 ID"]) ||
|
|
55
|
+
fallbackTaskId;
|
|
56
|
+
return {
|
|
57
|
+
taskKey,
|
|
58
|
+
identitySource: taskKey && taskKey !== fallbackTaskId ? "explicit" : "path-derived-legacy",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function parseTaskTombstone(taskPlanContent) {
|
|
63
|
+
const topLevelSupersedes = splitList(parseMetadataLine(taskPlanContent, ["Supersedes", "合并自"]));
|
|
64
|
+
const match = String(taskPlanContent || "").match(/^##\s*(?:Task Tombstone|任务墓碑)\s*$([\s\S]*?)(?=^##\s+|(?![\s\S]))/im);
|
|
65
|
+
const fields = match ? fieldsFromMarkdownBlock(match[1] || "") : new Map();
|
|
66
|
+
if (fields.size === 0) {
|
|
67
|
+
return {
|
|
68
|
+
deletionState: "active",
|
|
69
|
+
supersededBy: "",
|
|
70
|
+
supersedes: topLevelSupersedes,
|
|
71
|
+
deleteReason: "",
|
|
72
|
+
hiddenByDefault: false,
|
|
73
|
+
reopenEligible: false,
|
|
74
|
+
archiveEligible: false,
|
|
75
|
+
tombstoneSourcePath: "",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const state = normalizeMetadataValue(fields.get("state") || fields.get("状态") || "soft-deleted", "soft-deleted");
|
|
79
|
+
const deletionState = ["soft-deleted", "superseded", "archived"].includes(state) ? state : "soft-deleted";
|
|
80
|
+
return {
|
|
81
|
+
deletionState,
|
|
82
|
+
supersededBy: fields.get("superseded by") || fields.get("替代任务") || "",
|
|
83
|
+
supersedes: [...new Set([...topLevelSupersedes, ...splitList(fields.get("supersedes") || fields.get("合并自") || "")])],
|
|
84
|
+
deleteReason: fields.get("reason") || fields.get("原因") || "",
|
|
85
|
+
hiddenByDefault: true,
|
|
86
|
+
reopenEligible: parseTombstoneBooleanLike(fields.get("reopen eligible") || fields.get("可重新打开")),
|
|
87
|
+
archiveEligible: parseTombstoneBooleanLike(fields.get("archive eligible") || fields.get("可归档")),
|
|
88
|
+
tombstoneSourcePath: "task_plan.md#Task Tombstone",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function parseAgentReviewSubmission(reviewContent, { taskKey = "" } = {}) {
|
|
93
|
+
const match = String(reviewContent || "").match(/^##\s*(?:Agent Review Submission|Agent 审查提交|Agent 提交审查)\s*$([\s\S]*?)(?=^##\s+|(?![\s\S]))/im);
|
|
94
|
+
if (!match) return null;
|
|
95
|
+
const fields = fieldsFromMarkdownBlock(match[1] || "");
|
|
96
|
+
const required = [
|
|
97
|
+
"Submission ID",
|
|
98
|
+
"Submitted At",
|
|
99
|
+
"Submitted By",
|
|
100
|
+
"Task Key",
|
|
101
|
+
"Evidence Summary",
|
|
102
|
+
"Open Findings Count",
|
|
103
|
+
"Scanner Version",
|
|
104
|
+
];
|
|
105
|
+
const missing = required.filter((field) => !isConcreteField(fields.get(field.toLowerCase())));
|
|
106
|
+
const submittedTaskKey = fields.get("task key") || "";
|
|
107
|
+
const taskKeyMismatch = Boolean(taskKey && isConcreteField(submittedTaskKey) && !taskKeysMatch(submittedTaskKey, taskKey));
|
|
108
|
+
return {
|
|
109
|
+
submitted: missing.length === 0 && !taskKeyMismatch,
|
|
110
|
+
missingFields: taskKeyMismatch ? [...missing, "Task Key match"] : missing,
|
|
111
|
+
submissionId: fields.get("submission id") || "",
|
|
112
|
+
submittedAt: fields.get("submitted at") || "",
|
|
113
|
+
submittedBy: fields.get("submitted by") || "",
|
|
114
|
+
taskKey: submittedTaskKey,
|
|
115
|
+
taskKeyMismatch,
|
|
116
|
+
materialsChecklistHash: fields.get("materials checklist hash") || "",
|
|
117
|
+
evidenceSummary: fields.get("evidence summary") || "",
|
|
118
|
+
openFindingsCount: Number.parseInt(fields.get("open findings count") || "0", 10) || 0,
|
|
119
|
+
scannerVersion: fields.get("scanner version") || "",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function assessMaterialsReadiness({ budget, taskDir, brief, visualMap, reviewSubmission, lessonCandidates, phases, longRunningContractPath, reviewSurfaceRequired = true }) {
|
|
124
|
+
const issues = [];
|
|
125
|
+
const addIssue = (code, message, sourcePath, extra = {}) => {
|
|
126
|
+
issues.push({
|
|
127
|
+
code,
|
|
128
|
+
severity: extra.severity || "P2",
|
|
129
|
+
queue: "missing-materials",
|
|
130
|
+
sourcePath,
|
|
131
|
+
sourceLine: 0,
|
|
132
|
+
owner: extra.owner || "agent",
|
|
133
|
+
message,
|
|
134
|
+
allowedWritePaths: extra.allowedWritePaths || [`${toPosix(path.relative(path.dirname(taskDir), taskDir)) || "."}/**`],
|
|
135
|
+
forbiddenActions: ["human-confirm", "edit-unrelated-task", "fabricate-evidence"],
|
|
136
|
+
validationCommands: ["node scripts/harness.mjs check --profile target-project <target>"],
|
|
137
|
+
confidence: extra.confidence || "exact",
|
|
138
|
+
repairable: true,
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
const requiredFiles = ["task_plan.md", "progress.md"];
|
|
142
|
+
if (budget !== "simple") requiredFiles.push("brief.md", visualMapFile, "review.md", lessonCandidatesFile);
|
|
143
|
+
if (budget === "complex" && fs.existsSync(longRunningContractPath)) requiredFiles.push(longRunningTaskContractFile);
|
|
144
|
+
for (const fileName of requiredFiles) {
|
|
145
|
+
if (!fs.existsSync(path.join(taskDir, fileName))) addIssue(`missing-file-${fileName}`, `Required task material is missing: ${fileName}`, `TARGET:${fileName}`);
|
|
146
|
+
}
|
|
147
|
+
if (budget !== "simple") {
|
|
148
|
+
if (brief.source !== "standalone") addIssue("missing-brief", "Standard and complex tasks require standalone brief.md.", "TARGET:brief.md");
|
|
149
|
+
if (visualMap.status === "missing") addIssue("missing-visual-map", "Standard and complex tasks require canonical visual_map.md.", `TARGET:${visualMapFile}`);
|
|
150
|
+
}
|
|
151
|
+
if (budget !== "simple" && reviewSurfaceRequired) {
|
|
152
|
+
if (!reviewSubmission?.submitted) {
|
|
153
|
+
const message = reviewSubmission?.taskKeyMismatch
|
|
154
|
+
? "Agent Review Submission Task Key does not match this task."
|
|
155
|
+
: "Agent has not submitted a strict Agent Review Submission packet.";
|
|
156
|
+
addIssue(reviewSubmission?.taskKeyMismatch ? "invalid-review-submission-task-key" : "missing-review-submission", message, "TARGET:review.md");
|
|
157
|
+
}
|
|
158
|
+
if (!isLessonCandidateDecisionComplete(lessonCandidates)) {
|
|
159
|
+
addIssue("missing-lesson-decision", `Lesson candidate decision is not complete: ${lessonCandidates.status}.`, `TARGET:${lessonCandidatesFile}`);
|
|
160
|
+
}
|
|
161
|
+
const actionablePhases = (phases || []).filter((phase) => phase.state !== "skipped");
|
|
162
|
+
const hasPhaseEvidence = actionablePhases.some(
|
|
163
|
+
(phase) =>
|
|
164
|
+
phase.completion > 0 ||
|
|
165
|
+
["in_progress", "review", "blocked", "done"].includes(String(phase.state || "").toLowerCase()) ||
|
|
166
|
+
["partial", "present", "waived"].includes(String(phase.evidenceStatus || "").toLowerCase()),
|
|
167
|
+
);
|
|
168
|
+
if (actionablePhases.length > 0 && !hasPhaseEvidence) {
|
|
169
|
+
addIssue("phase-incomplete", "Visual Map has no phase progress or evidence yet.", `TARGET:${visualMapFile}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { ready: issues.length === 0, issues };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function requiresReviewMaterials({ state = "unknown", lifecycleState = "unknown", closeoutStatus = "missing" } = {}) {
|
|
176
|
+
return (
|
|
177
|
+
state === "review" ||
|
|
178
|
+
state === "done" ||
|
|
179
|
+
["in_review", "review-blocked", "closing", "closed-review-pending"].includes(lifecycleState) ||
|
|
180
|
+
closeoutStatus === "closed"
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function deriveTaskQueues({ id, title, reviewStatus, reviewSubmission, reviewConfirmation, reviewQueueState, materialIssues, risks, stateConflicts, lessonCandidates, closeoutStatus, tombstone, taskDir, target }) {
|
|
185
|
+
const queueReasons = [];
|
|
186
|
+
const pushReason = (reason) => {
|
|
187
|
+
queueReasons.push({
|
|
188
|
+
severity: "P2",
|
|
189
|
+
queue: "blocked",
|
|
190
|
+
sourcePath: "",
|
|
191
|
+
sourceLine: 0,
|
|
192
|
+
owner: "agent",
|
|
193
|
+
allowedWritePaths: [`${toPosix(path.relative(target.projectRoot, taskDir))}/**`],
|
|
194
|
+
forbiddenActions: ["human-confirm", "edit-unrelated-task", "fabricate-evidence"],
|
|
195
|
+
validationCommands: ["node scripts/harness.mjs check --profile target-project <target>"],
|
|
196
|
+
confidence: "exact",
|
|
197
|
+
repairable: true,
|
|
198
|
+
...reason,
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
for (const issue of materialIssues || []) pushReason(issue);
|
|
202
|
+
for (const risk of (risks || []).filter(isBlockingReviewRisk)) {
|
|
203
|
+
pushReason({
|
|
204
|
+
code: "open-blocking-finding",
|
|
205
|
+
severity: risk.severity || "P1",
|
|
206
|
+
queue: "blocked",
|
|
207
|
+
sourcePath: "TARGET:review.md",
|
|
208
|
+
message: `Open blocking review finding ${risk.id || risk.severity}: ${risk.summary || "Review finding blocks confirmation."}`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
for (const conflict of stateConflicts || []) {
|
|
212
|
+
if (conflict.severity !== "block") continue;
|
|
213
|
+
pushReason({
|
|
214
|
+
code: conflict.code,
|
|
215
|
+
severity: "P1",
|
|
216
|
+
queue: "blocked",
|
|
217
|
+
sourcePath: "TARGET:progress.md",
|
|
218
|
+
message: conflict.message,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (reviewSubmission?.submitted && reviewSubmission.scannerVersion !== taskScannerVersion) {
|
|
222
|
+
pushReason({
|
|
223
|
+
code: "stale-review-submission-scanner",
|
|
224
|
+
severity: "P2",
|
|
225
|
+
queue: "blocked",
|
|
226
|
+
sourcePath: "TARGET:review.md",
|
|
227
|
+
message: "Agent Review Submission was generated by a stale scanner version.",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const hasLessonWork = lessonCandidates?.status === "needs-promotion" || lessonCandidates?.promotionState === "queued" || lessonCandidates?.openCount > 0;
|
|
231
|
+
const taskQueues = [];
|
|
232
|
+
if (tombstone.deletionState !== "active") {
|
|
233
|
+
taskQueues.push("soft-deleted-superseded");
|
|
234
|
+
} else {
|
|
235
|
+
if ((materialIssues || []).length > 0) taskQueues.push("missing-materials");
|
|
236
|
+
if (queueReasons.some((reason) => reason.queue === "blocked")) taskQueues.push("blocked");
|
|
237
|
+
if (reviewSubmission?.submitted && reviewQueueState === "ready-to-confirm" && !reviewConfirmation?.confirmed && !hasLessonWork && !taskQueues.includes("blocked") && !taskQueues.includes("missing-materials")) {
|
|
238
|
+
taskQueues.push("review");
|
|
239
|
+
}
|
|
240
|
+
if (hasLessonWork) taskQueues.push("lessons");
|
|
241
|
+
if (reviewStatus === "confirmed") taskQueues.push(closeoutStatus === "closed" ? "finalized" : "confirmed");
|
|
242
|
+
}
|
|
243
|
+
if (taskQueues.length === 0) taskQueues.push("active");
|
|
244
|
+
return {
|
|
245
|
+
taskQueues,
|
|
246
|
+
queueReasons,
|
|
247
|
+
repairPrompt: renderRepairPrompt({ id, title, taskDir, target, reasons: queueReasons }),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function parseReviewConfirmation(reviewContent, { taskKey = "", projectRoot = "", taskDir = "", reviewPath = "", progressPath = "" } = {}) {
|
|
252
|
+
const match = String(reviewContent || "").match(/^##\s*(?:Human Review Confirmation|人工审查确认)\s*$([\s\S]*?)(?=^##\s+|(?![\s\S]))/im);
|
|
253
|
+
if (!match) return null;
|
|
254
|
+
const fields = fieldsFromMarkdownBlock(match[1] || "");
|
|
255
|
+
const required = ["Confirmation ID", "Confirmed At", "Reviewer", "Reviewer Email", "Task Key", "Confirm Text", "Evidence Checked", "Commit SHA", "Audit Status"];
|
|
256
|
+
const missing = required.filter((field) => !isConcreteField(fields.get(field.toLowerCase())));
|
|
257
|
+
const confirmedTaskKey = fields.get("task key") || "";
|
|
258
|
+
const confirmText = fields.get("confirm text") || "";
|
|
259
|
+
const commitSha = fields.get("commit sha") || "";
|
|
260
|
+
const auditStatus = fields.get("audit status") || "";
|
|
261
|
+
const taskKeyMismatch = Boolean(taskKey && isConcreteField(confirmedTaskKey) && !taskKeysMatch(confirmedTaskKey, taskKey));
|
|
262
|
+
const confirmTextMismatch = Boolean(taskKey && isConcreteField(confirmText) && !taskKeysMatch(confirmText, taskKey));
|
|
263
|
+
const commitShaInvalid = Boolean(isConcreteField(commitSha) && !/^[0-9a-f]{7,40}$/i.test(commitSha));
|
|
264
|
+
const auditStatusInvalid = Boolean(isConcreteField(auditStatus) && auditStatus.trim().toLowerCase() !== "committed");
|
|
265
|
+
let gitAudit = null;
|
|
266
|
+
if (missing.length === 0 && !taskKeyMismatch && !confirmTextMismatch && !commitShaInvalid && !auditStatusInvalid) {
|
|
267
|
+
gitAudit = validateReviewConfirmationGitAudit({
|
|
268
|
+
projectRoot,
|
|
269
|
+
taskId: taskKey,
|
|
270
|
+
reviewPath: reviewPath || (taskDir ? path.join(taskDir, "review.md") : ""),
|
|
271
|
+
progressPath: progressPath || (taskDir ? path.join(taskDir, "progress.md") : ""),
|
|
272
|
+
commitSha,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const gitAuditInvalid = Boolean(gitAudit && !gitAudit.valid);
|
|
276
|
+
const invalidFields = [
|
|
277
|
+
...(taskKeyMismatch ? ["Task Key match"] : []),
|
|
278
|
+
...(confirmTextMismatch ? ["Confirm Text match"] : []),
|
|
279
|
+
...(commitShaInvalid ? ["Commit SHA valid"] : []),
|
|
280
|
+
...(auditStatusInvalid ? ["Audit Status committed"] : []),
|
|
281
|
+
...(gitAuditInvalid ? ["Commit SHA git audit"] : []),
|
|
282
|
+
];
|
|
283
|
+
if (fields.size > 0) {
|
|
284
|
+
return {
|
|
285
|
+
confirmed: missing.length === 0 && invalidFields.length === 0,
|
|
286
|
+
missingFields: [...missing, ...invalidFields],
|
|
287
|
+
confirmationId: fields.get("confirmation id") || "",
|
|
288
|
+
confirmedAt: fields.get("confirmed at") || "",
|
|
289
|
+
reviewer: fields.get("reviewer") || "",
|
|
290
|
+
reviewerEmail: fields.get("reviewer email") || "",
|
|
291
|
+
taskKey: confirmedTaskKey,
|
|
292
|
+
taskKeyMismatch,
|
|
293
|
+
confirmText,
|
|
294
|
+
confirmTextMismatch,
|
|
295
|
+
evidenceChecked: fields.get("evidence checked") || "",
|
|
296
|
+
commitSha,
|
|
297
|
+
commitShaInvalid,
|
|
298
|
+
auditStatus,
|
|
299
|
+
auditStatusInvalid,
|
|
300
|
+
gitAudit,
|
|
301
|
+
gitAuditInvalid,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return { confirmed: false, missingFields: required };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function taskReviewStatus({ reviewContent = "", risks = [], confirmation = null, submission = null } = {}) {
|
|
308
|
+
if (risks.some(isBlockingReviewRisk)) return "blocked-open-findings";
|
|
309
|
+
if (confirmation?.confirmed) return "confirmed";
|
|
310
|
+
if (!String(reviewContent || "").trim()) return "missing";
|
|
311
|
+
if (submission?.submitted) return "agent-reviewed";
|
|
312
|
+
if (hasAgentReviewSignal(reviewContent)) return "agent-reviewed";
|
|
313
|
+
return "required";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function hasAgentReviewSignal(reviewContent) {
|
|
317
|
+
const content = String(reviewContent || "");
|
|
318
|
+
const verdict = content.match(/^\s*[-*]?\s*Verdict\s*[::]\s*([^\n]+)/im);
|
|
319
|
+
if (verdict) {
|
|
320
|
+
const value = verdict[1].trim().toLowerCase();
|
|
321
|
+
if (/^yes(?:$|[-_\s])/i.test(value) && !/^yes\s*\/\s*no\b/i.test(value)) return true;
|
|
322
|
+
}
|
|
323
|
+
return /本轮已检查|未发现阻塞目标的重要发现/.test(content);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function isBlockingReviewRisk(risk) {
|
|
327
|
+
return /^P[0-2]$/i.test(risk?.severity || "") && (risk.open || risk.blocksRelease);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function deriveLifecycleState({ state = "unknown", reviewStatus = "missing", closeoutStatus = "missing" } = {}) {
|
|
331
|
+
if (reviewStatus === "blocked-open-findings") return "review-blocked";
|
|
332
|
+
if (closeoutStatus === "closed" && reviewStatus !== "confirmed") return "closed-review-pending";
|
|
333
|
+
if (closeoutStatus === "closed") return "closed";
|
|
334
|
+
if (state === "blocked") return "blocked";
|
|
335
|
+
if (state === "done") return "closing";
|
|
336
|
+
if (state === "review") return "in_review";
|
|
337
|
+
if (state === "in_progress") return "active";
|
|
338
|
+
if (["planned", "not_started"].includes(state)) return "ready";
|
|
339
|
+
return "unknown";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function deriveReviewQueueState({ state = "unknown", lifecycleState = "unknown", reviewStatus = "missing", closeoutStatus = "missing", budget = "standard", walkthroughPath = "", lessonCandidateDecisionComplete = false, materialsReady = true, deletionState = "active" } = {}) {
|
|
343
|
+
if (deletionState !== "active") return "not-in-queue";
|
|
344
|
+
if (reviewStatus === "blocked-open-findings") return "blocked";
|
|
345
|
+
if (["not_started", "planned", "in_progress"].includes(state)) return "not-in-queue";
|
|
346
|
+
const reviewSurface = requiresReviewMaterials({ state, lifecycleState, closeoutStatus });
|
|
347
|
+
if (!reviewSurface) return "not-in-queue";
|
|
348
|
+
if (reviewStatus === "confirmed") return closeoutStatus === "closed" ? "not-in-queue" : "confirmed";
|
|
349
|
+
if (budget === "simple" && reviewStatus === "missing") return "not-in-queue";
|
|
350
|
+
const missingWalkthrough = budget !== "simple" && !walkthroughPath;
|
|
351
|
+
const missingCandidateDecision = budget !== "simple" && !lessonCandidateDecisionComplete;
|
|
352
|
+
if (!materialsReady || missingWalkthrough || missingCandidateDecision || ["missing", "required"].includes(reviewStatus)) return "needs-material";
|
|
353
|
+
if (closeoutStatus === "closed") return "closed-debt";
|
|
354
|
+
return "ready-to-confirm";
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function collectStateConflicts({ state, reviewStatus, closeoutStatus, lifecycleState }) {
|
|
358
|
+
const conflicts = [];
|
|
359
|
+
if (state === "done" && closeoutStatus !== "closed") {
|
|
360
|
+
conflicts.push({ code: "done-without-closeout", severity: "warn", message: "Task state is done, but closeout is still missing or pending." });
|
|
361
|
+
}
|
|
362
|
+
if (closeoutStatus === "closed" && reviewStatus !== "confirmed") {
|
|
363
|
+
conflicts.push({ code: "closed-without-human-review", severity: "warn", message: "Task is closed, but human review confirmation is still missing." });
|
|
364
|
+
}
|
|
365
|
+
if (reviewStatus === "blocked-open-findings") {
|
|
366
|
+
conflicts.push({ code: "review-blocked-open-findings", severity: "block", message: "Open P0-P2 review findings block human review confirmation." });
|
|
367
|
+
}
|
|
368
|
+
if (lifecycleState === "closed" && reviewStatus === "blocked-open-findings") {
|
|
369
|
+
conflicts.push({ code: "closed-with-blocking-review", severity: "block", message: "Closeout is closed while review findings still block release." });
|
|
370
|
+
}
|
|
371
|
+
return conflicts;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function collectReviewRisks(reviewContent) {
|
|
375
|
+
const { header, rows } = tableAfterHeading(reviewContent, /^ID$/i);
|
|
376
|
+
const severityIndex = firstColumn(header, reviewFindingColumns.severity);
|
|
377
|
+
const findingIndex = firstColumn(header, reviewFindingColumns.finding);
|
|
378
|
+
const openIndex = firstColumn(header, reviewFindingColumns.open);
|
|
379
|
+
const blocksIndex = firstColumn(header, reviewFindingColumns.blocksRelease);
|
|
380
|
+
const dispositionIndex = firstColumn(header, reviewFindingColumns.disposition);
|
|
381
|
+
const waiverByIndex = firstColumn(header, reviewFindingColumns.waiverBy);
|
|
382
|
+
if (severityIndex < 0 || findingIndex < 0) return [];
|
|
383
|
+
return rows
|
|
384
|
+
.filter((row) => /^P[0-3]$/i.test(row[severityIndex] || ""))
|
|
385
|
+
.map((row) => {
|
|
386
|
+
const disposition = normalizeMetadataValue(row[dispositionIndex] || "", "");
|
|
387
|
+
const waived = ["waived", "accepted-risk"].includes(disposition) && String(row[waiverByIndex] || "").trim();
|
|
388
|
+
return {
|
|
389
|
+
id: row[0],
|
|
390
|
+
severity: row[severityIndex],
|
|
391
|
+
open: !waived && normalizeReviewBoolean(row[openIndex] || "no") === "yes",
|
|
392
|
+
blocksRelease: !waived && normalizeReviewBoolean(row[blocksIndex] || "no") === "yes",
|
|
393
|
+
disposition,
|
|
394
|
+
waiverBy: row[waiverByIndex] || "",
|
|
395
|
+
summary: row[findingIndex],
|
|
396
|
+
};
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function renderRepairPrompt({ id, title, taskDir, target, reasons }) {
|
|
401
|
+
const repairable = (reasons || []).filter((reason) => reason.repairable !== false);
|
|
402
|
+
if (repairable.length === 0) return "";
|
|
403
|
+
const relativeTaskDir = toPosix(path.relative(target.projectRoot, taskDir));
|
|
404
|
+
return [
|
|
405
|
+
`Please repair task ${id}: ${title || id}.`,
|
|
406
|
+
"",
|
|
407
|
+
`Task path: ${relativeTaskDir}`,
|
|
408
|
+
"",
|
|
409
|
+
"Detected issues:",
|
|
410
|
+
...repairable.map((reason) => `- [${reason.queue}/${reason.code}] ${reason.message}`),
|
|
411
|
+
"",
|
|
412
|
+
"Allowed writes:",
|
|
413
|
+
...[...new Set(repairable.flatMap((reason) => reason.allowedWritePaths || []))].map((item) => `- ${item}`),
|
|
414
|
+
"",
|
|
415
|
+
"Forbidden actions:",
|
|
416
|
+
"- Do not write Human Review Confirmation; only a human can confirm.",
|
|
417
|
+
"- Do not edit unrelated tasks.",
|
|
418
|
+
"- Do not fabricate evidence or mark work complete without running checks.",
|
|
419
|
+
"",
|
|
420
|
+
"Expected output:",
|
|
421
|
+
"- Fix the listed task-local materials or blockers.",
|
|
422
|
+
"- Update progress.md with evidence.",
|
|
423
|
+
"- Re-run the validation commands below.",
|
|
424
|
+
"",
|
|
425
|
+
"Validation commands:",
|
|
426
|
+
...[...new Set(repairable.flatMap((reason) => reason.validationCommands || []))].map((item) => `- ${item}`),
|
|
427
|
+
].join("\n");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function fieldsFromMarkdownBlock(block) {
|
|
431
|
+
const fields = new Map();
|
|
432
|
+
const tableLines = String(block || "").split(/\r?\n/).filter((line) => line.trim().startsWith("|"));
|
|
433
|
+
for (let index = 0; index < tableLines.length - 1; index += 1) {
|
|
434
|
+
const header = splitMarkdownRow(tableLines[index]);
|
|
435
|
+
const separator = splitMarkdownRow(tableLines[index + 1]);
|
|
436
|
+
if (!separator.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
|
|
437
|
+
const fieldIndex = firstColumn(header, ["Field", "字段"]);
|
|
438
|
+
const valueIndex = firstColumn(header, ["Value", "值"]);
|
|
439
|
+
if (fieldIndex < 0 || valueIndex < 0) continue;
|
|
440
|
+
for (const line of tableLines.slice(index + 2)) {
|
|
441
|
+
const row = splitMarkdownRow(line);
|
|
442
|
+
if (row.length !== header.length) break;
|
|
443
|
+
const field = String(row[fieldIndex] || "").trim();
|
|
444
|
+
if (field) fields.set(field.toLowerCase(), String(row[valueIndex] || "").trim());
|
|
445
|
+
}
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
for (const line of String(block || "").split(/\r?\n/)) {
|
|
449
|
+
const match = line.match(/^\s*(?:[-*]\s*)?([^::|]+?)\s*[::]\s*(.+?)\s*$/);
|
|
450
|
+
if (match) fields.set(match[1].trim().toLowerCase(), match[2].trim());
|
|
451
|
+
}
|
|
452
|
+
return fields;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function isConcreteField(value) {
|
|
456
|
+
const raw = String(value || "").trim();
|
|
457
|
+
if (!raw) return false;
|
|
458
|
+
return !/^\[.*\]$/.test(raw) && !/\{\{[^}]+\}\}/.test(raw);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function taskKeysMatch(candidate, expected) {
|
|
462
|
+
const left = String(candidate || "").replace(/`/g, "").trim();
|
|
463
|
+
const right = String(expected || "").replace(/`/g, "").trim();
|
|
464
|
+
return left === right || right.endsWith(`/${left}`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function parseTombstoneBooleanLike(value) {
|
|
468
|
+
return /^(yes|true|open|blocked|是|开放|阻塞|阻塞确认|阻塞发布)$/i.test(String(value || "").trim());
|
|
469
|
+
}
|