coding-agent-harness 1.0.1 → 1.0.2
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 +19 -0
- package/README.en-US.md +14 -0
- package/README.md +111 -86
- package/README.zh-CN.md +270 -0
- package/SKILL.md +116 -189
- package/docs-release/README.md +72 -5
- package/docs-release/architecture/overview.md +286 -28
- package/docs-release/architecture/overview.zh-CN.md +288 -0
- package/docs-release/assets/dashboard-overview-en.png +0 -0
- package/docs-release/assets/harness-architecture.svg +163 -0
- package/docs-release/assets/harness-workflow.svg +64 -0
- package/docs-release/guides/agent-installation.en-US.md +214 -0
- package/docs-release/guides/agent-installation.md +123 -26
- package/docs-release/guides/document-audience-and-surfaces.en-US.md +112 -0
- package/docs-release/guides/document-audience-and-surfaces.md +112 -0
- package/docs-release/guides/full-legacy-migration-subagent-strategy.md +334 -0
- package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +334 -0
- package/docs-release/guides/legacy-migration-agent-prompt.md +384 -0
- package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +361 -0
- package/docs-release/guides/migration-playbook.en-US.md +325 -0
- package/docs-release/guides/migration-playbook.md +329 -0
- package/docs-release/guides/parent-control-repository-pattern.en-US.md +252 -0
- package/docs-release/guides/parent-control-repository-pattern.md +252 -0
- package/docs-release/guides/repository-operating-models.en-US.md +196 -0
- package/docs-release/guides/repository-operating-models.md +196 -0
- package/docs-release/intl/README.md +15 -0
- package/docs-release/intl/de-DE.md +18 -0
- package/docs-release/intl/en-US.md +18 -0
- package/docs-release/intl/es-ES.md +18 -0
- package/docs-release/intl/fr-FR.md +18 -0
- package/docs-release/intl/ja-JP.md +18 -0
- package/docs-release/intl/ko-KR.md +18 -0
- package/docs-release/intl/zh-CN.md +18 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/brief.md +13 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/lesson_candidates.md +24 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/progress.md +1 -1
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/task_plan.md +4 -2
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/{visual_roadmap.md → visual_map.md} +9 -1
- package/package.json +3 -1
- package/references/agents-md-pattern.md +3 -3
- package/references/docs-directory-standard.md +47 -3
- package/references/external-source-intake-standard.md +75 -0
- package/references/harness-ledger.md +5 -3
- package/references/legacy-12-phase-bootstrap.md +41 -0
- package/references/lessons-governance.md +23 -6
- package/references/planning-loop.md +41 -3
- package/references/project-onboarding-audit.md +10 -0
- package/references/repo-governance-standard.md +2 -0
- package/references/testing-standard.md +50 -0
- package/references/walkthrough-closeout.md +6 -5
- package/scripts/check-harness.mjs +76 -35
- package/scripts/harness.mjs +303 -12
- package/scripts/lib/capability-registry.mjs +533 -0
- package/scripts/lib/check-profiles.mjs +510 -0
- package/scripts/lib/core-shared.mjs +186 -0
- package/scripts/lib/dashboard-data.mjs +389 -0
- package/scripts/lib/dashboard-workbench.mjs +217 -0
- package/scripts/lib/dashboard-writer.mjs +93 -2
- package/scripts/lib/harness-core.mjs +10 -1318
- package/scripts/lib/lesson-maintenance.mjs +145 -0
- package/scripts/lib/markdown-utils.mjs +158 -0
- package/scripts/lib/migration-planner.mjs +478 -0
- package/scripts/lib/migration-support.mjs +312 -0
- package/scripts/lib/task-lifecycle.mjs +755 -0
- package/scripts/lib/task-scanner.mjs +682 -0
- package/scripts/smoke-dashboard.mjs +22 -0
- package/scripts/test-harness.mjs +926 -14
- package/templates/AGENTS.md.template +41 -30
- package/templates/architecture/Architecture-SSoT.md +21 -0
- package/templates/architecture/README.md +49 -0
- package/templates/architecture/critical-flows.md +22 -0
- package/templates/architecture/local-repo-context.md +20 -0
- package/templates/architecture/service-catalog.md +17 -0
- package/templates/architecture/services/service-template.md +31 -0
- package/templates/architecture/system-map.md +22 -0
- package/templates/dashboard/assets/app-src/00-state.js +41 -0
- package/templates/dashboard/assets/app-src/10-router.js +76 -0
- package/templates/dashboard/assets/app-src/20-overview.js +235 -0
- package/templates/dashboard/assets/app-src/30-tasks.js +563 -0
- package/templates/dashboard/assets/app-src/40-modules.js +58 -0
- package/templates/dashboard/assets/app-src/45-review.js +128 -0
- package/templates/dashboard/assets/app-src/50-migration.js +169 -0
- package/templates/dashboard/assets/app-src/60-shared.js +61 -0
- package/templates/dashboard/assets/app-src/90-bindings.js +382 -0
- package/templates/dashboard/assets/app.css +2575 -310
- package/templates/dashboard/assets/app.js +1498 -307
- package/templates/dashboard/assets/app.manifest.json +11 -0
- package/templates/dashboard/assets/i18n.js +429 -44
- package/templates/dashboard/assets/mermaid-renderer.js +58 -8
- package/templates/development/README.md +52 -0
- package/templates/development/codebase-map.md +11 -0
- package/templates/development/cross-repo-debugging.md +18 -0
- package/templates/development/external-context/service-template.md +33 -0
- package/templates/development/external-source-packs/README.md +24 -0
- package/templates/development/external-source-packs/digest-template.md +28 -0
- package/templates/development/local-setup.md +16 -0
- package/templates/development/stubs-and-mocks.md +11 -0
- package/templates/integrations/README.md +40 -0
- package/templates/integrations/api-contract.md +42 -0
- package/templates/integrations/event-contract.md +46 -0
- package/templates/integrations/third-party/vendor-template.md +42 -0
- package/templates/integrations/webhook-contract.md +41 -0
- package/templates/planning/brief.md +32 -0
- package/templates/planning/lesson_candidates.md +58 -0
- package/templates/planning/long-running-task-contract.md +7 -0
- package/templates/planning/module_brief.md +25 -0
- package/templates/planning/module_session_prompt.md +6 -0
- package/templates/planning/task_plan.md +7 -5
- package/templates/planning/{visual_roadmap.md → visual_map.md} +24 -2
- package/templates/reference/docs-library-standard.md +31 -0
- package/templates/reference/execution-workflow-standard.md +4 -2
- package/templates/reference/external-source-intake-standard.md +82 -0
- package/templates/reference/harness-ledger-standard.md +1 -0
- package/templates/reference/repo-governance-standard.md +6 -4
- package/templates/reference/walkthrough-standard.md +2 -1
- package/templates/walkthrough/walkthrough-template.md +2 -2
- package/templates-zh-CN/AGENTS.md.template +69 -70
- package/templates-zh-CN/architecture/Architecture-SSoT.md +21 -0
- package/templates-zh-CN/architecture/README.md +51 -0
- package/templates-zh-CN/architecture/critical-flows.md +24 -0
- package/templates-zh-CN/architecture/local-repo-context.md +20 -0
- package/templates-zh-CN/architecture/service-catalog.md +17 -0
- package/templates-zh-CN/architecture/services/service-template.md +31 -0
- package/templates-zh-CN/architecture/system-map.md +22 -0
- package/templates-zh-CN/development/README.md +54 -0
- package/templates-zh-CN/development/codebase-map.md +11 -0
- package/templates-zh-CN/development/cross-repo-debugging.md +18 -0
- package/templates-zh-CN/development/external-context/service-template.md +33 -0
- package/templates-zh-CN/development/external-source-packs/README.md +24 -0
- package/templates-zh-CN/development/external-source-packs/digest-template.md +28 -0
- package/templates-zh-CN/development/local-setup.md +16 -0
- package/templates-zh-CN/development/stubs-and-mocks.md +11 -0
- package/templates-zh-CN/integrations/README.md +42 -0
- package/templates-zh-CN/integrations/api-contract.md +42 -0
- package/templates-zh-CN/integrations/event-contract.md +46 -0
- package/templates-zh-CN/integrations/third-party/vendor-template.md +42 -0
- package/templates-zh-CN/integrations/webhook-contract.md +41 -0
- package/templates-zh-CN/planning/brief.md +32 -0
- package/templates-zh-CN/planning/lesson_candidates.md +58 -0
- package/templates-zh-CN/planning/long-running-task-contract.md +1 -1
- package/templates-zh-CN/planning/module_brief.md +25 -0
- package/templates-zh-CN/planning/module_plan.md +2 -2
- package/templates-zh-CN/planning/module_session_prompt.md +4 -3
- package/templates-zh-CN/planning/task_plan.md +10 -4
- package/templates-zh-CN/planning/{visual_roadmap.md → visual_map.md} +21 -2
- package/templates-zh-CN/reference/docs-library-standard.md +35 -0
- package/templates-zh-CN/reference/execution-workflow-standard.md +9 -2
- package/templates-zh-CN/reference/external-source-intake-standard.md +82 -0
- package/templates-zh-CN/reference/harness-ledger-standard.md +5 -2
- package/templates-zh-CN/reference/repo-governance-standard.md +2 -0
- package/templates-zh-CN/reference/walkthrough-standard.md +4 -4
- package/templates-zh-CN/walkthrough/Closeout-SSoT.md +2 -2
- package/templates-zh-CN/walkthrough/walkthrough-template.md +2 -2
- package/templates-zh-CN/dashboard/assets/app.css +0 -399
- package/templates-zh-CN/dashboard/assets/app.js +0 -435
- package/templates-zh-CN/dashboard/assets/i18n.js +0 -47
- package/templates-zh-CN/dashboard/assets/markdown-reader.js +0 -116
- package/templates-zh-CN/dashboard/assets/mermaid-renderer.js +0 -59
- package/templates-zh-CN/dashboard/index.html +0 -18
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
allowedTaskStates,
|
|
5
|
+
allowedTaskBudgets,
|
|
6
|
+
visualMapFile,
|
|
7
|
+
legacyVisualRoadmapFile,
|
|
8
|
+
lessonCandidatesFile,
|
|
9
|
+
longRunningTaskContractFile,
|
|
10
|
+
taskContractMarker,
|
|
11
|
+
toPosix,
|
|
12
|
+
readFileSafe,
|
|
13
|
+
walkFiles,
|
|
14
|
+
titleFromMarkdown,
|
|
15
|
+
} from "./core-shared.mjs";
|
|
16
|
+
import {
|
|
17
|
+
tableAfterHeading,
|
|
18
|
+
firstColumn,
|
|
19
|
+
splitList,
|
|
20
|
+
splitDependencies,
|
|
21
|
+
getColumn,
|
|
22
|
+
} from "./markdown-utils.mjs";
|
|
23
|
+
|
|
24
|
+
export const allowedLessonCandidateTaskStatuses = new Set([
|
|
25
|
+
"missing",
|
|
26
|
+
"pending-review",
|
|
27
|
+
"no-candidate-accepted",
|
|
28
|
+
"needs-promotion",
|
|
29
|
+
"promoted",
|
|
30
|
+
"rejected",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export const allowedLessonCandidateRowStatuses = new Set([
|
|
34
|
+
"ready-for-review",
|
|
35
|
+
"needs-promotion",
|
|
36
|
+
"promoted",
|
|
37
|
+
"rejected",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
export const reviewCompleteLessonCandidateStatuses = new Set([
|
|
41
|
+
"no-candidate-accepted",
|
|
42
|
+
"needs-promotion",
|
|
43
|
+
"promoted",
|
|
44
|
+
"rejected",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
export function parseTaskState(progressContent) {
|
|
48
|
+
return parseTaskStateInfo(progressContent).state;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function parseTaskBudget(taskPlanContent) {
|
|
52
|
+
const match =
|
|
53
|
+
String(taskPlanContent || "").match(/^Selected budget\s*[::]\s*([^\n]+)/im) ||
|
|
54
|
+
String(taskPlanContent || "").match(/^选择预算\s*[::]\s*([^\n]+)/im);
|
|
55
|
+
if (!match) return "standard";
|
|
56
|
+
const raw = match[1].replace(/`/g, "").trim().toLowerCase();
|
|
57
|
+
const normalized = raw.replaceAll("_", "-").replace(/\s+/g, "-");
|
|
58
|
+
if (allowedTaskBudgets.has(normalized)) return normalized;
|
|
59
|
+
if (["long-running", "longrunning", "module-parallel"].includes(normalized)) return "complex";
|
|
60
|
+
return "standard";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseMetadataLine(content, labels) {
|
|
64
|
+
const escaped = labels.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
65
|
+
const match = String(content || "").match(new RegExp(`^(?:${escaped})\\s*[::]\\s*([^\\n]+)`, "im"));
|
|
66
|
+
return match ? match[1].replace(/`/g, "").trim() : "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeMetadataValue(value, fallback = "") {
|
|
70
|
+
const normalized = String(value || "")
|
|
71
|
+
.replace(/`/g, "")
|
|
72
|
+
.trim()
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replaceAll("_", "-")
|
|
75
|
+
.replace(/\s+/g, "-");
|
|
76
|
+
return normalized || fallback;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function parseTaskMetadata(taskPlanContent) {
|
|
80
|
+
const content = String(taskPlanContent || "");
|
|
81
|
+
const kind = normalizeMetadataValue(parseMetadataLine(content, ["Task Kind", "任务类型"]), "general");
|
|
82
|
+
const preset = normalizeMetadataValue(parseMetadataLine(content, ["Task Preset", "Preset", "任务预设"]), "none");
|
|
83
|
+
const presetVersion = parseMetadataLine(content, ["Preset Version", "预设版本"]);
|
|
84
|
+
const migrationTargetLevel = normalizeMetadataValue(
|
|
85
|
+
parseMetadataLine(content, ["Migration Target Level", "Target Level", "迁移目标等级", "目标等级"]),
|
|
86
|
+
"",
|
|
87
|
+
);
|
|
88
|
+
const migrationAchievedLevel = normalizeMetadataValue(
|
|
89
|
+
parseMetadataLine(content, ["Migration Achieved Level", "Achieved Level", "迁移实际完成等级", "实际完成等级"]),
|
|
90
|
+
"",
|
|
91
|
+
);
|
|
92
|
+
const evidenceBundle = parseMetadataLine(content, ["Evidence Bundle", "证据包"]);
|
|
93
|
+
return {
|
|
94
|
+
kind,
|
|
95
|
+
preset,
|
|
96
|
+
presetVersion,
|
|
97
|
+
migrationTargetLevel,
|
|
98
|
+
migrationAchievedLevel,
|
|
99
|
+
evidenceBundle,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function parseTaskContractInfo(taskPlanContent) {
|
|
104
|
+
const content = String(taskPlanContent || "");
|
|
105
|
+
const explicit =
|
|
106
|
+
content.match(/^Task Contract\s*[::]\s*`?([^`\n]+)`?\s*$/im) ||
|
|
107
|
+
content.match(/^任务合同\s*[::]\s*`?([^`\n]+)`?\s*$/im);
|
|
108
|
+
const version = explicit ? explicit[1].trim() : "";
|
|
109
|
+
return {
|
|
110
|
+
version,
|
|
111
|
+
generated: version === "harness-task/v1" || content.includes(taskContractMarker),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function parseTaskStateInfo(progressContent) {
|
|
116
|
+
const match = progressContent.match(/^##\s*(?:Current Status|Status|状态)\s*[::]?\s*(?:\n\s*)?([^\n]+)/im);
|
|
117
|
+
if (!match) return inferLegacyTaskState(progressContent);
|
|
118
|
+
const raw = match[1].replace(/`/g, "").trim();
|
|
119
|
+
if (!raw || raw.includes("|") || /^[-*]\s+/.test(raw)) return inferLegacyTaskState(progressContent);
|
|
120
|
+
const aliases = new Map([
|
|
121
|
+
["进行中", "in_progress"],
|
|
122
|
+
["已完成", "done"],
|
|
123
|
+
["未开始", "not_started"],
|
|
124
|
+
["计划中", "planned"],
|
|
125
|
+
["审查中", "review"],
|
|
126
|
+
["已阻塞", "blocked"],
|
|
127
|
+
["pending", "planned"],
|
|
128
|
+
]);
|
|
129
|
+
const normalized = aliases.get(raw) || raw.toLowerCase().replaceAll("-", "_").replaceAll(" ", "_");
|
|
130
|
+
return allowedTaskStates.has(normalized)
|
|
131
|
+
? { state: normalized, source: "explicit", raw }
|
|
132
|
+
: { state: "unknown", source: "invalid", raw };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function inferLegacyTaskState(progressContent) {
|
|
136
|
+
const { header, rows } = tableAfterHeading(progressContent, /^(Status|状态)$/i);
|
|
137
|
+
const statusIndex = firstColumn(header, ["Status", "状态"]);
|
|
138
|
+
if (statusIndex < 0 || rows.length === 0) return { state: "unknown", source: "missing", raw: "" };
|
|
139
|
+
const states = rows.map((row) => normalizeLegacyState(row[statusIndex])).filter(Boolean);
|
|
140
|
+
if (states.includes("blocked")) return { state: "blocked", source: "legacy-table", raw: "blocked" };
|
|
141
|
+
if (states.includes("in_progress")) return { state: "in_progress", source: "legacy-table", raw: "in_progress" };
|
|
142
|
+
if (states.includes("review")) return { state: "review", source: "legacy-table", raw: "review" };
|
|
143
|
+
if (states.length > 0 && states.every((state) => state === "done")) return { state: "done", source: "legacy-table", raw: "done" };
|
|
144
|
+
if (states.some((state) => ["planned", "not_started"].includes(state))) return { state: "planned", source: "legacy-table", raw: "planned" };
|
|
145
|
+
return { state: "unknown", source: "missing", raw: "" };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeLegacyState(value) {
|
|
149
|
+
const raw = String(value || "").replace(/`/g, "").trim().toLowerCase();
|
|
150
|
+
if (!raw || /^(none|n\/a|na|-|—|–|无)$/.test(raw)) return "";
|
|
151
|
+
if (/block|阻塞|blocked/.test(raw)) return "blocked";
|
|
152
|
+
if (/in[-_\s]?progress|doing|active|进行中|当前|working/.test(raw)) return "in_progress";
|
|
153
|
+
if (/review|审查|审核|验证中/.test(raw)) return "review";
|
|
154
|
+
if (/done|complete|completed|merged|closed|完成|已完成/.test(raw)) return "done";
|
|
155
|
+
if (/pending|planned|todo|not[-_\s]?started|未开始|计划/.test(raw)) return "planned";
|
|
156
|
+
return "";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function parsePhases(taskPlanContent) {
|
|
160
|
+
const { header, rows } = tableAfterHeading(taskPlanContent, /^Phase ID$/i);
|
|
161
|
+
if (rows.length === 0) return [];
|
|
162
|
+
const indexes = {
|
|
163
|
+
id: firstColumn(header, ["Phase ID", "阶段 ID"]),
|
|
164
|
+
dependsOn: firstColumn(header, ["Depends On", "依赖"]),
|
|
165
|
+
state: firstColumn(header, ["State", "状态"]),
|
|
166
|
+
completion: firstColumn(header, ["Completion", "完成度"]),
|
|
167
|
+
output: firstColumn(header, ["Output", "产出"]),
|
|
168
|
+
requiredEvidence: firstColumn(header, ["Required Evidence", "必要证据"]),
|
|
169
|
+
evidenceStatus: firstColumn(header, ["Evidence Status", "证据状态"]),
|
|
170
|
+
blockingRisk: firstColumn(header, ["Blocking Risk", "阻塞风险"]),
|
|
171
|
+
owner: firstColumn(header, ["Owner / Handoff", "负责人 / 交接"]),
|
|
172
|
+
};
|
|
173
|
+
return rows.map((row) => ({
|
|
174
|
+
id: row[indexes.id] || "",
|
|
175
|
+
dependsOn: splitDependencies(row[indexes.dependsOn] || ""),
|
|
176
|
+
state: row[indexes.state] || "planned",
|
|
177
|
+
completion: Number.parseInt(String(row[indexes.completion] || "0").replace("%", ""), 10) || 0,
|
|
178
|
+
output: row[indexes.output] || "",
|
|
179
|
+
requiredEvidence: splitList(row[indexes.requiredEvidence] || ""),
|
|
180
|
+
evidenceStatus: row[indexes.evidenceStatus] || "missing",
|
|
181
|
+
blockingRisk: row[indexes.blockingRisk] || "",
|
|
182
|
+
owner: row[indexes.owner] || "",
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function readTaskContractFile(taskDir, fileName, legacyContent = "") {
|
|
187
|
+
const filePath = path.join(taskDir, fileName);
|
|
188
|
+
const content = readFileSafe(filePath);
|
|
189
|
+
if (content.trim()) return { path: filePath, content, source: "standalone" };
|
|
190
|
+
return { path: filePath, content: legacyContent, source: legacyContent.trim() ? "legacy" : "missing" };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function readVisualMapContractFile(taskDir, legacyContent = "") {
|
|
194
|
+
const canonicalPath = path.join(taskDir, visualMapFile);
|
|
195
|
+
const canonical = readFileSafe(canonicalPath);
|
|
196
|
+
if (canonical.trim()) return { path: canonicalPath, content: canonical, source: "canonical", status: "present" };
|
|
197
|
+
const legacyPath = path.join(taskDir, legacyVisualRoadmapFile);
|
|
198
|
+
const legacy = readFileSafe(legacyPath);
|
|
199
|
+
if (legacy.trim()) return { path: legacyPath, content: legacy, source: "legacy", status: "legacy-only" };
|
|
200
|
+
return {
|
|
201
|
+
path: canonicalPath,
|
|
202
|
+
content: legacyContent,
|
|
203
|
+
source: legacyContent.trim() ? "legacy" : "missing",
|
|
204
|
+
status: legacyContent.trim() ? "legacy-only" : "missing",
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function isActiveTaskState(state) {
|
|
209
|
+
return ["active", "planned", "not_started", "in_progress", "review", "blocked", "reopened", "current-evidence"].includes(state);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function listTaskPlanPaths(target) {
|
|
213
|
+
const taskRoots = [
|
|
214
|
+
path.join(target.docsRoot, "09-PLANNING/TASKS"),
|
|
215
|
+
path.join(target.docsRoot, "09-PLANNING/MODULES"),
|
|
216
|
+
];
|
|
217
|
+
return taskRoots
|
|
218
|
+
.flatMap(walkFiles)
|
|
219
|
+
.filter((file) => file.endsWith("task_plan.md"))
|
|
220
|
+
.filter((file) => !file.includes(`${path.sep}_task-template${path.sep}`))
|
|
221
|
+
.filter((file) => !file.includes(`${path.sep}_optional-structures${path.sep}`))
|
|
222
|
+
.filter((file) => !file.includes(`${path.sep}_archive${path.sep}`));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function taskIdForDirectory(target, taskDir) {
|
|
226
|
+
return toPosix(path.relative(path.join(target.docsRoot, "09-PLANNING"), taskDir));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function inferTaskClassification({ id, title, relative, explicitModule, legacyCandidate = false }) {
|
|
230
|
+
if (explicitModule) {
|
|
231
|
+
return {
|
|
232
|
+
module: explicitModule,
|
|
233
|
+
source: "explicit",
|
|
234
|
+
bucket: "module",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const text = `${id} ${title} ${relative}`.toLowerCase();
|
|
238
|
+
const rules = [
|
|
239
|
+
["dashboard", /dashboard|visibility|cockpit|console|ui|frontend|view|页面|看板|驾驶舱/],
|
|
240
|
+
["migration", /migration|migrate|adoption|legacy|safe-adoption|迁移|历史|兼容/],
|
|
241
|
+
["task-lifecycle", /task|phase|lifecycle|planning|计划|任务|阶段/],
|
|
242
|
+
["review-quality", /review|finding|evidence|qa|test|regression|审查|证据|回归|测试/],
|
|
243
|
+
["release-docs", /docs-release|readme|guide|install|playbook|文档|安装|指南/],
|
|
244
|
+
["repo-governance", /git|ci|source-package|private|boundary|repo|branch|pr|仓库|边界/],
|
|
245
|
+
["automation-cli", /cli|command|script|harness\.mjs|自动化|命令/],
|
|
246
|
+
];
|
|
247
|
+
const match = rules.find(([, pattern]) => pattern.test(text));
|
|
248
|
+
return {
|
|
249
|
+
module: match ? match[0] : legacyCandidate ? "legacy-unclassified" : "unclassified",
|
|
250
|
+
source: match ? "inferred" : "fallback",
|
|
251
|
+
bucket: legacyCandidate ? "legacy" : "current",
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function assessBriefQuality(content, { source = "missing" } = {}) {
|
|
256
|
+
const text = String(content || "").trim();
|
|
257
|
+
const issues = [];
|
|
258
|
+
if (source !== "standalone") issues.push("missing-standalone-brief");
|
|
259
|
+
if (text.length < 120) issues.push("too-short");
|
|
260
|
+
if (!/^##\s+/m.test(text)) issues.push("missing-sections");
|
|
261
|
+
if (/\[(?:outcome|scope|risk|evidence|next|目标|范围|风险|证据|下一步)[^\]]*\]/i.test(text)) issues.push("unfilled-placeholder");
|
|
262
|
+
return { status: issues.length ? "fail" : "pass", issues };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function explicitVisualMapStatus(briefContent) {
|
|
266
|
+
const match = String(briefContent || "").match(/^Visual Map Status:\s*(present|not-needed|missing|legacy-only)\s*$/im);
|
|
267
|
+
return match ? match[1] : "";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function taskMigrationClassification(state, visualMapStatus) {
|
|
271
|
+
if (state === "unknown") return "unknown-needs-human";
|
|
272
|
+
if (isActiveTaskState(state)) return "active";
|
|
273
|
+
if (visualMapStatus === "present" || visualMapStatus === "legacy-only") return "historical-with-diagram";
|
|
274
|
+
return "historical-no-map-needed";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function requiresCanonicalVisualMap(task) {
|
|
278
|
+
return ["active", "reopened", "current-evidence", "historical-with-diagram"].includes(task.migrationClassification);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function taskCutoverCounters(tasks) {
|
|
282
|
+
const legacyVisualOnlyCount = tasks.filter((task) => task.visualMapStatus === "legacy-only").length;
|
|
283
|
+
const unknownClassificationCount = tasks.filter((task) => task.migrationClassification === "unknown-needs-human").length;
|
|
284
|
+
const weakBriefCount = tasks.filter((task) => task.briefQuality?.status !== "pass").length;
|
|
285
|
+
const visualMapRequiredCount = tasks.filter(requiresCanonicalVisualMap).length;
|
|
286
|
+
const missingCanonicalVisualMapCount = tasks.filter((task) => requiresCanonicalVisualMap(task) && task.visualMapSource !== "canonical").length;
|
|
287
|
+
return {
|
|
288
|
+
legacyVisualOnlyCount,
|
|
289
|
+
unknownClassificationCount,
|
|
290
|
+
weakBriefCount,
|
|
291
|
+
visualMapRequiredCount,
|
|
292
|
+
missingCanonicalVisualMapCount,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function collectTasks(target) {
|
|
297
|
+
return listTaskPlanPaths(target).map((taskPlanPath) => {
|
|
298
|
+
const taskDir = path.dirname(taskPlanPath);
|
|
299
|
+
const taskPlan = readFileSafe(taskPlanPath);
|
|
300
|
+
const brief = readTaskContractFile(taskDir, "brief.md", "");
|
|
301
|
+
const executionStrategyPath = path.join(taskDir, "execution_strategy.md");
|
|
302
|
+
const progressPath = path.join(taskDir, "progress.md");
|
|
303
|
+
const reviewPath = path.join(taskDir, "review.md");
|
|
304
|
+
const findingsPath = path.join(taskDir, "findings.md");
|
|
305
|
+
const lessonCandidatesPath = path.join(taskDir, lessonCandidatesFile);
|
|
306
|
+
const longRunningContractPath = path.join(taskDir, longRunningTaskContractFile);
|
|
307
|
+
const visualMap = readVisualMapContractFile(taskDir, taskPlan);
|
|
308
|
+
const progress = readFileSafe(progressPath);
|
|
309
|
+
const review = readFileSafe(reviewPath);
|
|
310
|
+
const lessonCandidates = parseLessonCandidateStatus(readFileSafe(lessonCandidatesPath));
|
|
311
|
+
const phases = parsePhases(visualMap.content);
|
|
312
|
+
const completion =
|
|
313
|
+
phases.length > 0
|
|
314
|
+
? Math.round(
|
|
315
|
+
phases.filter((phase) => phase.state !== "skipped").reduce((sum, phase) => sum + phase.completion, 0) /
|
|
316
|
+
Math.max(1, phases.filter((phase) => phase.state !== "skipped").length),
|
|
317
|
+
)
|
|
318
|
+
: 0;
|
|
319
|
+
const relative = toPosix(path.relative(target.projectRoot, taskDir));
|
|
320
|
+
const id = taskIdForDirectory(target, taskDir);
|
|
321
|
+
const title = titleFromMarkdown(brief.content || taskPlan, path.basename(taskDir));
|
|
322
|
+
const stateInfo = parseTaskStateInfo(progress);
|
|
323
|
+
const budget = parseTaskBudget(taskPlan);
|
|
324
|
+
const metadata = parseTaskMetadata(taskPlan);
|
|
325
|
+
const taskContract = parseTaskContractInfo(taskPlan);
|
|
326
|
+
const explicitModule = id.startsWith("MODULES/") ? id.split("/")[1] : null;
|
|
327
|
+
const legacyCandidate = brief.source !== "standalone" || visualMap.status === "legacy-only" || !fs.existsSync(executionStrategyPath);
|
|
328
|
+
const classification = inferTaskClassification({ id, title, relative, explicitModule, legacyCandidate });
|
|
329
|
+
const briefVisualStatus = explicitVisualMapStatus(brief.content);
|
|
330
|
+
const visualMapStatus = briefVisualStatus === "not-needed" && visualMap.status === "missing" ? "not-needed" : visualMap.status;
|
|
331
|
+
const risks = collectReviewRisks(review);
|
|
332
|
+
const reviewConfirmation = parseReviewConfirmation(review);
|
|
333
|
+
const reviewStatus = taskReviewStatus({ reviewContent: review, risks, confirmation: reviewConfirmation });
|
|
334
|
+
const closeoutInfo = taskCloseoutInfo(target, taskPlanPath);
|
|
335
|
+
const lifecycleState = deriveLifecycleState({ state: stateInfo.state, reviewStatus, closeoutStatus: closeoutInfo.status });
|
|
336
|
+
const stateConflicts = collectStateConflicts({ state: stateInfo.state, reviewStatus, closeoutStatus: closeoutInfo.status, lifecycleState });
|
|
337
|
+
return {
|
|
338
|
+
id,
|
|
339
|
+
shortId: path.basename(taskDir),
|
|
340
|
+
title,
|
|
341
|
+
path: `TARGET:${relative}`,
|
|
342
|
+
taskPlanPath: `TARGET:${toPosix(path.relative(target.projectRoot, taskPlanPath))}`,
|
|
343
|
+
executionStrategyPath: `TARGET:${toPosix(path.relative(target.projectRoot, executionStrategyPath))}`,
|
|
344
|
+
progressPath: `TARGET:${toPosix(path.relative(target.projectRoot, progressPath))}`,
|
|
345
|
+
reviewPath: `TARGET:${toPosix(path.relative(target.projectRoot, reviewPath))}`,
|
|
346
|
+
findingsPath: `TARGET:${toPosix(path.relative(target.projectRoot, findingsPath))}`,
|
|
347
|
+
module: explicitModule,
|
|
348
|
+
inferredModule: classification.module,
|
|
349
|
+
classificationSource: classification.source,
|
|
350
|
+
classificationBucket: classification.bucket,
|
|
351
|
+
briefSource: brief.source,
|
|
352
|
+
briefPath: `TARGET:${toPosix(path.relative(target.projectRoot, brief.path))}`,
|
|
353
|
+
visualMapSource: visualMap.source,
|
|
354
|
+
visualMapStatus,
|
|
355
|
+
visualMapPath: `TARGET:${toPosix(path.relative(target.projectRoot, visualMap.path))}`,
|
|
356
|
+
legacyVisualRoadmapPresent: fs.existsSync(path.join(taskDir, legacyVisualRoadmapFile)),
|
|
357
|
+
briefQuality: assessBriefQuality(brief.content, { source: brief.source }),
|
|
358
|
+
migrationClassification: taskMigrationClassification(stateInfo.state, visualMapStatus),
|
|
359
|
+
roadmapSource: visualMap.source,
|
|
360
|
+
state: stateInfo.state,
|
|
361
|
+
budget,
|
|
362
|
+
taskContractVersion: taskContract.version,
|
|
363
|
+
taskContractGenerated: taskContract.generated,
|
|
364
|
+
stateSource: stateInfo.source,
|
|
365
|
+
stateRaw: stateInfo.raw,
|
|
366
|
+
taskKind: metadata.kind,
|
|
367
|
+
taskPreset: metadata.preset,
|
|
368
|
+
presetVersion: metadata.presetVersion,
|
|
369
|
+
migrationTargetLevel: metadata.migrationTargetLevel,
|
|
370
|
+
migrationAchievedLevel: metadata.migrationAchievedLevel,
|
|
371
|
+
evidenceBundle: metadata.evidenceBundle,
|
|
372
|
+
migrationSnapshot: collectMigrationSnapshot(target, metadata),
|
|
373
|
+
lifecycleState,
|
|
374
|
+
reviewStatus,
|
|
375
|
+
reviewConfirmation,
|
|
376
|
+
closeoutStatus: closeoutInfo.status,
|
|
377
|
+
walkthroughPath: closeoutInfo.walkthroughPath ? `TARGET:${closeoutInfo.walkthroughPath}` : "",
|
|
378
|
+
lessonCandidatePath: fs.existsSync(lessonCandidatesPath)
|
|
379
|
+
? `TARGET:${toPosix(path.relative(target.projectRoot, lessonCandidatesPath))}`
|
|
380
|
+
: "",
|
|
381
|
+
lessonCandidateStatus: lessonCandidates.status,
|
|
382
|
+
lessonCandidateReviewDecision: lessonCandidates.reviewDecision,
|
|
383
|
+
lessonCandidatePromotionState: lessonCandidates.promotionState,
|
|
384
|
+
lessonCandidateCloseoutToken: lessonCandidates.closeoutToken,
|
|
385
|
+
lessonCandidateRowCount: lessonCandidates.rows.length,
|
|
386
|
+
lessonCandidateOpenCount: lessonCandidates.openCount,
|
|
387
|
+
lessonCandidateIssues: lessonCandidates.issues,
|
|
388
|
+
lessonCandidateDecisionComplete: isLessonCandidateDecisionComplete(lessonCandidates),
|
|
389
|
+
longRunningContractPath: fs.existsSync(longRunningContractPath)
|
|
390
|
+
? `TARGET:${toPosix(path.relative(target.projectRoot, longRunningContractPath))}`
|
|
391
|
+
: "",
|
|
392
|
+
longRunningContractStatus: fs.existsSync(longRunningContractPath) ? "present" : "missing",
|
|
393
|
+
stateConflicts,
|
|
394
|
+
completion,
|
|
395
|
+
phases,
|
|
396
|
+
risks,
|
|
397
|
+
evidence: collectEvidence(progress),
|
|
398
|
+
handoffs: collectHandoffs(progress, title),
|
|
399
|
+
dependencies: [],
|
|
400
|
+
};
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function collectMigrationSnapshot(target, metadata) {
|
|
405
|
+
if (metadata.preset !== "legacy-migration") return null;
|
|
406
|
+
const evidenceBundle = String(metadata.evidenceBundle || "").replace(/^TARGET:/, "").replace(/^\/+/, "");
|
|
407
|
+
const bundlePath = evidenceBundle ? path.join(target.projectRoot, evidenceBundle) : "";
|
|
408
|
+
const sessionPath = bundlePath ? path.join(bundlePath, "session.json") : "";
|
|
409
|
+
let session = null;
|
|
410
|
+
try {
|
|
411
|
+
session = sessionPath && fs.existsSync(sessionPath) ? JSON.parse(fs.readFileSync(sessionPath, "utf8")) : null;
|
|
412
|
+
} catch {
|
|
413
|
+
session = null;
|
|
414
|
+
}
|
|
415
|
+
const summary = session?.plan?.summary || {};
|
|
416
|
+
return {
|
|
417
|
+
targetLevel: metadata.migrationTargetLevel || "",
|
|
418
|
+
achievedLevel: metadata.migrationAchievedLevel || "",
|
|
419
|
+
evidenceBundle: evidenceBundle ? `TARGET:${evidenceBundle}` : "",
|
|
420
|
+
evidencePresent: Boolean(bundlePath && fs.existsSync(bundlePath)),
|
|
421
|
+
sessionPresent: Boolean(session),
|
|
422
|
+
sessionResult: session?.result || "",
|
|
423
|
+
normalStatus: session?.checks?.normal?.status || "",
|
|
424
|
+
strictStatus: session?.checks?.strict?.status || "",
|
|
425
|
+
strictDeferred: Boolean(session?.strictDeferred),
|
|
426
|
+
warnings: Number(summary.warnings || 0),
|
|
427
|
+
taskActions: Number(summary.taskActions || 0),
|
|
428
|
+
reviewSchemaGaps: Number(summary.reviewSchemaGaps || 0),
|
|
429
|
+
legacyReferenceGaps: Number(summary.legacyReferenceGaps || 0),
|
|
430
|
+
legacyResiduals: Number(summary.legacyResiduals || 0),
|
|
431
|
+
fullCutoverEligible: summary.fullCutoverEligible === true,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function parseLessonCandidateStatus(content) {
|
|
436
|
+
const text = String(content || "");
|
|
437
|
+
if (!text.trim()) {
|
|
438
|
+
return emptyLessonCandidateStatus("missing", ["missing-candidate-file"]);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const fields = lessonCandidateFields(text);
|
|
442
|
+
const declaredStatus = normalizeLessonCandidateStatus(fields.get("task-level status") || "pending-review");
|
|
443
|
+
const reviewDecision = normalizeCandidateField(fields.get("review decision") || "pending-human-review");
|
|
444
|
+
const promotionState = normalizeCandidateField(fields.get("promotion state") || "not-promoted");
|
|
445
|
+
const closeoutToken = String(fields.get("closeout token") || "pending").trim();
|
|
446
|
+
const rows = lessonCandidateRows(text);
|
|
447
|
+
const issues = [];
|
|
448
|
+
|
|
449
|
+
if (!allowedLessonCandidateTaskStatuses.has(declaredStatus)) {
|
|
450
|
+
issues.push(`invalid-task-status:${declaredStatus}`);
|
|
451
|
+
}
|
|
452
|
+
for (const row of rows) {
|
|
453
|
+
if (!allowedLessonCandidateRowStatuses.has(row.status)) issues.push(`invalid-row-status:${row.id || "missing-id"}:${row.status}`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const aggregateStatus = aggregateLessonCandidateStatus(rows, declaredStatus);
|
|
457
|
+
if (declaredStatus !== aggregateStatus && declaredStatus !== "missing") {
|
|
458
|
+
issues.push(`status-aggregate-mismatch:${declaredStatus}->${aggregateStatus}`);
|
|
459
|
+
}
|
|
460
|
+
if (aggregateStatus === "no-candidate-accepted" && !noCandidateReason(text)) {
|
|
461
|
+
issues.push("missing-no-candidate-reason");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
status: aggregateStatus,
|
|
466
|
+
declaredStatus,
|
|
467
|
+
schemaVersion: fields.get("schema version") || "",
|
|
468
|
+
reviewDecision,
|
|
469
|
+
promotionState,
|
|
470
|
+
closeoutToken,
|
|
471
|
+
rows,
|
|
472
|
+
openCount: rows.filter((row) => ["ready-for-review", "needs-promotion"].includes(row.status)).length,
|
|
473
|
+
issues,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export function isLessonCandidateDecisionComplete(candidateStatus) {
|
|
478
|
+
if (!candidateStatus || candidateStatus.issues?.length) return false;
|
|
479
|
+
return reviewCompleteLessonCandidateStatuses.has(candidateStatus.status);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function emptyLessonCandidateStatus(status, issues = []) {
|
|
483
|
+
return {
|
|
484
|
+
status,
|
|
485
|
+
declaredStatus: status,
|
|
486
|
+
schemaVersion: "",
|
|
487
|
+
reviewDecision: "",
|
|
488
|
+
promotionState: "",
|
|
489
|
+
closeoutToken: "",
|
|
490
|
+
rows: [],
|
|
491
|
+
openCount: 0,
|
|
492
|
+
issues,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function lessonCandidateFields(content) {
|
|
497
|
+
const { header, rows } = tableAfterHeading(content, /^Field$/i);
|
|
498
|
+
const fieldIndex = firstColumn(header, ["Field", "字段"]);
|
|
499
|
+
const valueIndex = firstColumn(header, ["Value", "值"]);
|
|
500
|
+
const fields = new Map();
|
|
501
|
+
if (fieldIndex < 0 || valueIndex < 0) return fields;
|
|
502
|
+
for (const row of rows) {
|
|
503
|
+
const key = String(row[fieldIndex] || "").trim().toLowerCase();
|
|
504
|
+
if (key) fields.set(key, String(row[valueIndex] || "").trim());
|
|
505
|
+
}
|
|
506
|
+
return fields;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function lessonCandidateRows(content) {
|
|
510
|
+
const { header, rows } = tableAfterHeading(content, /^ID$/i);
|
|
511
|
+
const idIndex = firstColumn(header, ["ID", "候选 ID"]);
|
|
512
|
+
const statusIndex = firstColumn(header, ["Row Status", "行状态", "Status", "状态"]);
|
|
513
|
+
const titleIndex = firstColumn(header, ["Title", "标题"]);
|
|
514
|
+
const decisionIndex = firstColumn(header, ["Review Decision", "审查决定"]);
|
|
515
|
+
const targetIndex = firstColumn(header, ["Promotion Target", "沉淀目标"]);
|
|
516
|
+
if (idIndex < 0 || statusIndex < 0) return [];
|
|
517
|
+
return rows
|
|
518
|
+
.filter((row) => /^LC-[A-Za-z0-9-]+$/i.test(row[idIndex] || ""))
|
|
519
|
+
.map((row) => ({
|
|
520
|
+
id: row[idIndex] || "",
|
|
521
|
+
status: normalizeLessonCandidateStatus(row[statusIndex] || ""),
|
|
522
|
+
title: row[titleIndex] || "",
|
|
523
|
+
reviewDecision: row[decisionIndex] || "",
|
|
524
|
+
promotionTarget: row[targetIndex] || "",
|
|
525
|
+
}));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function normalizeLessonCandidateStatus(value) {
|
|
529
|
+
return String(value || "")
|
|
530
|
+
.replace(/`/g, "")
|
|
531
|
+
.trim()
|
|
532
|
+
.toLowerCase()
|
|
533
|
+
.replaceAll("_", "-")
|
|
534
|
+
.replace(/\s+/g, "-");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function normalizeCandidateField(value) {
|
|
538
|
+
return String(value || "").replace(/`/g, "").trim().toLowerCase().replaceAll("_", "-").replace(/\s+/g, "-");
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function aggregateLessonCandidateStatus(rows, declaredStatus) {
|
|
542
|
+
if (rows.length === 0) return declaredStatus === "no-candidate-accepted" ? "no-candidate-accepted" : declaredStatus;
|
|
543
|
+
const statuses = rows.map((row) => row.status);
|
|
544
|
+
if (statuses.includes("ready-for-review")) return "pending-review";
|
|
545
|
+
if (statuses.includes("needs-promotion")) return "needs-promotion";
|
|
546
|
+
if (statuses.every((status) => status === "promoted")) return "promoted";
|
|
547
|
+
if (statuses.every((status) => status === "rejected")) return "rejected";
|
|
548
|
+
if (statuses.every((status) => ["promoted", "rejected"].includes(status))) return "promoted";
|
|
549
|
+
return declaredStatus;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function noCandidateReason(content) {
|
|
553
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
554
|
+
const start = lines.findIndex((line) => /^##\s*No-Candidate Reason\s*$/i.test(line.trim()));
|
|
555
|
+
if (start < 0) return "";
|
|
556
|
+
const body = [];
|
|
557
|
+
for (const line of lines.slice(start + 1)) {
|
|
558
|
+
if (/^##\s+/.test(line)) break;
|
|
559
|
+
body.push(line);
|
|
560
|
+
}
|
|
561
|
+
return body.join("\n").replace(/`/g, "").trim();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
function taskCloseoutInfo(target, taskPlanPath) {
|
|
566
|
+
const closeout = readFileSafe(path.join(target.docsRoot, "10-WALKTHROUGH/Closeout-SSoT.md"));
|
|
567
|
+
if (!closeout.trim()) return { status: "missing", walkthroughPath: "" };
|
|
568
|
+
const docsRelative = `docs/${toPosix(path.relative(target.docsRoot, taskPlanPath))}`;
|
|
569
|
+
const projectRelative = toPosix(path.relative(target.projectRoot, taskPlanPath));
|
|
570
|
+
const line = closeout
|
|
571
|
+
.split(/\r?\n/)
|
|
572
|
+
.find((entry) => entry.includes(docsRelative) || entry.includes(projectRelative));
|
|
573
|
+
if (!line) return { status: "missing", walkthroughPath: "" };
|
|
574
|
+
const walkthroughPath = extractWalkthroughPath(target, line);
|
|
575
|
+
const status = /\b(closed|complete|completed|done|skipped-with-reason|skipped|已关闭|已完成|跳过)\b/i.test(line) ? "closed" : "pending";
|
|
576
|
+
return { status, walkthroughPath };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function extractWalkthroughPath(target, closeoutLine) {
|
|
580
|
+
const matches = [...String(closeoutLine || "").matchAll(/`?((?:docs\/)?10-WALKTHROUGH\/[^`|\s]+\.md)`?/g)];
|
|
581
|
+
const match = matches.find((entry) => !entry[1].endsWith("Closeout-SSoT.md") && !entry[1].includes("/_"));
|
|
582
|
+
if (!match) return "";
|
|
583
|
+
const projectRelative = match[1].startsWith("docs/") ? match[1] : `docs/${match[1]}`;
|
|
584
|
+
if (!fs.existsSync(path.join(target.projectRoot, projectRelative))) return "";
|
|
585
|
+
return projectRelative;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function parseReviewConfirmation(reviewContent) {
|
|
589
|
+
const match = String(reviewContent || "").match(/^##\s*(?:Human Review Confirmation|人工审查确认)\s*$([\s\S]*?)(?=^##\s+|\s*$)/im);
|
|
590
|
+
if (!match) return null;
|
|
591
|
+
const block = match[1] || "";
|
|
592
|
+
const timeMatch = block.match(/\|\s*(\d{4}-\d{2}-\d{2}[^|]*)\|/);
|
|
593
|
+
const reviewerMatch = block.match(/Reviewer\s*[::]\s*([^\n]+)/i) || block.match(/审查人\s*[::]\s*([^\n]+)/);
|
|
594
|
+
return {
|
|
595
|
+
confirmed: true,
|
|
596
|
+
confirmedAt: timeMatch ? timeMatch[1].trim() : "",
|
|
597
|
+
reviewer: reviewerMatch ? reviewerMatch[1].trim() : "",
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export function taskReviewStatus({ reviewContent = "", risks = [], confirmation = null } = {}) {
|
|
602
|
+
if (risks.some(isBlockingReviewRisk)) return "blocked-open-findings";
|
|
603
|
+
if (confirmation?.confirmed) return "confirmed";
|
|
604
|
+
if (!String(reviewContent || "").trim()) return "missing";
|
|
605
|
+
if (/Verdict\s*[::]\s*yes/i.test(reviewContent) || /本轮已检查|未发现阻塞目标的重要发现/.test(reviewContent)) return "reviewed-unconfirmed";
|
|
606
|
+
return "required";
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function isBlockingReviewRisk(risk) {
|
|
610
|
+
return /^P[0-2]$/i.test(risk?.severity || "") && (risk.open || risk.blocksRelease);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export function deriveLifecycleState({ state = "unknown", reviewStatus = "missing", closeoutStatus = "missing" } = {}) {
|
|
614
|
+
if (closeoutStatus === "closed") return "closed";
|
|
615
|
+
if (state === "blocked") return "blocked";
|
|
616
|
+
if (reviewStatus === "blocked-open-findings") return "review-blocked";
|
|
617
|
+
if (state === "done") return "closing";
|
|
618
|
+
if (state === "review") return "in_review";
|
|
619
|
+
if (state === "in_progress") return "active";
|
|
620
|
+
if (["planned", "not_started"].includes(state)) return "ready";
|
|
621
|
+
return "unknown";
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function collectStateConflicts({ state, reviewStatus, closeoutStatus, lifecycleState }) {
|
|
625
|
+
const conflicts = [];
|
|
626
|
+
if (state === "done" && closeoutStatus !== "closed") {
|
|
627
|
+
conflicts.push({
|
|
628
|
+
code: "done-without-closeout",
|
|
629
|
+
severity: "warn",
|
|
630
|
+
message: "Task state is done, but closeout is still missing or pending.",
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
if (reviewStatus === "blocked-open-findings") {
|
|
634
|
+
conflicts.push({
|
|
635
|
+
code: "review-blocked-open-findings",
|
|
636
|
+
severity: "block",
|
|
637
|
+
message: "Open P0-P2 review findings block human review confirmation.",
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
if (lifecycleState === "closed" && reviewStatus === "blocked-open-findings") {
|
|
641
|
+
conflicts.push({
|
|
642
|
+
code: "closed-with-blocking-review",
|
|
643
|
+
severity: "block",
|
|
644
|
+
message: "Closeout is closed while review findings still block release.",
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
return conflicts;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function collectHandoffs(progressContent, taskId) {
|
|
651
|
+
if (!/Coordinator Handoff/i.test(progressContent) || !/pending-coordinator-pass/i.test(progressContent)) return [];
|
|
652
|
+
return [{ id: `H-${taskId}`, from: "worker", to: "coordinator", state: "pending", summary: "Coordinator handoff pending" }];
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export function collectReviewRisks(reviewContent) {
|
|
656
|
+
const { header, rows } = tableAfterHeading(reviewContent, /^ID$/i);
|
|
657
|
+
const severityIndex = getColumn(header, "Severity");
|
|
658
|
+
const findingIndex = getColumn(header, "Finding");
|
|
659
|
+
const openIndex = getColumn(header, "Open");
|
|
660
|
+
const blocksIndex = getColumn(header, "Blocks Release");
|
|
661
|
+
if (severityIndex < 0 || findingIndex < 0) return [];
|
|
662
|
+
return rows
|
|
663
|
+
.filter((row) => /^P[0-3]$/i.test(row[severityIndex] || ""))
|
|
664
|
+
.map((row) => ({
|
|
665
|
+
id: row[0],
|
|
666
|
+
severity: row[severityIndex],
|
|
667
|
+
open: /^yes$/i.test(row[openIndex] || "no"),
|
|
668
|
+
blocksRelease: /^yes$/i.test(row[blocksIndex] || "no"),
|
|
669
|
+
summary: row[findingIndex],
|
|
670
|
+
}));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function collectEvidence(progressContent) {
|
|
674
|
+
const matches = [...progressContent.matchAll(/\b(command|diff|fixture|screenshot|review|report):((?:PUBLIC|PRIVATE|TARGET|EXTERNAL|URL):[^:\s|]+):([^\n|]+)/g)];
|
|
675
|
+
return matches.map((match, index) => ({
|
|
676
|
+
id: `E-${String(index + 1).padStart(3, "0")}`,
|
|
677
|
+
type: match[1],
|
|
678
|
+
path: match[2],
|
|
679
|
+
status: "present",
|
|
680
|
+
summary: match[3].trim(),
|
|
681
|
+
}));
|
|
682
|
+
}
|