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
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
4
|
import {
|
|
6
|
-
repoRoot,
|
|
7
5
|
visualMapFile,
|
|
8
6
|
legacyVisualRoadmapFile,
|
|
9
7
|
lessonCandidatesFile,
|
|
@@ -25,26 +23,44 @@ import {
|
|
|
25
23
|
normalizeTaskId,
|
|
26
24
|
renderTaskTemplate,
|
|
27
25
|
} from "./core-shared.mjs";
|
|
28
|
-
import { verifyMigrationSession } from "./migration-planner.mjs";
|
|
29
26
|
import { readCapabilityRegistry } from "./capability-registry.mjs";
|
|
27
|
+
import { readPresetPackage } from "./preset-registry.mjs";
|
|
28
|
+
import {
|
|
29
|
+
assertPresetWriteScope,
|
|
30
|
+
buildPresetContext,
|
|
31
|
+
evaluateTemplateValues,
|
|
32
|
+
resolvePresetInputs,
|
|
33
|
+
renderPresetResourceIndex,
|
|
34
|
+
renderPresetTaskTemplate,
|
|
35
|
+
} from "./preset-engine.mjs";
|
|
30
36
|
import {
|
|
31
37
|
collectTasks,
|
|
32
38
|
collectReviewRisks,
|
|
33
39
|
isBlockingReviewRisk,
|
|
34
40
|
listTaskPlanPaths,
|
|
35
|
-
parsePhases,
|
|
36
41
|
parseTaskBudget,
|
|
37
|
-
parseLessonCandidateStatus,
|
|
38
|
-
isLessonCandidateDecisionComplete,
|
|
39
|
-
parseReviewConfirmation,
|
|
40
|
-
readVisualMapContractFile,
|
|
41
42
|
taskIdForDirectory,
|
|
43
|
+
taskScannerVersion,
|
|
42
44
|
} from "./task-scanner.mjs";
|
|
43
45
|
import {
|
|
44
46
|
getColumn,
|
|
45
47
|
firstColumn,
|
|
46
48
|
updateMarkdownTableRow,
|
|
47
49
|
} from "./markdown-utils.mjs";
|
|
50
|
+
import {
|
|
51
|
+
validateLifecycleTransition,
|
|
52
|
+
validateReviewEntryGate,
|
|
53
|
+
} from "./task-lifecycle/review-gates.mjs";
|
|
54
|
+
import { confirmTaskReview as confirmTaskReviewWithContext } from "./task-lifecycle/review-confirm.mjs";
|
|
55
|
+
import { appendProgressLog, markdownCell } from "./task-lifecycle/text-utils.mjs";
|
|
56
|
+
import {
|
|
57
|
+
beginGovernanceSync,
|
|
58
|
+
commitGovernanceSync,
|
|
59
|
+
governanceRelativePaths,
|
|
60
|
+
releaseGovernanceSync,
|
|
61
|
+
syncModuleStepGovernance,
|
|
62
|
+
syncTaskGovernance,
|
|
63
|
+
} from "./governance-sync.mjs";
|
|
48
64
|
|
|
49
65
|
function taskTemplateFiles({ locale = "en-US" } = {}) {
|
|
50
66
|
return [
|
|
@@ -91,7 +107,7 @@ function taskRoot(target, taskId, { moduleKey = "" } = {}) {
|
|
|
91
107
|
return path.join(target.docsRoot, "09-PLANNING/TASKS", normalizedTaskId);
|
|
92
108
|
}
|
|
93
109
|
|
|
94
|
-
function resolveTaskDirectory(target, taskRef) {
|
|
110
|
+
export function resolveTaskDirectory(target, taskRef) {
|
|
95
111
|
const raw = String(taskRef || "").replace(/^docs\/09-PLANNING\//, "").replace(/^\/+/, "");
|
|
96
112
|
if (!raw) throw new Error("Missing task id");
|
|
97
113
|
const direct = raw.startsWith("TASKS/") || raw.startsWith("MODULES/") ? path.join(target.docsRoot, "09-PLANNING", raw) : "";
|
|
@@ -153,11 +169,10 @@ function normalizeTaskBudgetInput(budget) {
|
|
|
153
169
|
throw new Error(`Invalid task budget: ${budget}. Expected one of: simple, standard, complex`);
|
|
154
170
|
}
|
|
155
171
|
|
|
156
|
-
function normalizeTaskPresetInput(preset) {
|
|
172
|
+
function normalizeTaskPresetInput(preset, { targetInput = "" } = {}) {
|
|
157
173
|
const normalized = String(preset || "none").trim().toLowerCase().replaceAll("_", "-");
|
|
158
174
|
if (!normalized || normalized === "none") return "none";
|
|
159
|
-
|
|
160
|
-
throw new Error(`Invalid task preset: ${preset}. Expected one of: legacy-migration`);
|
|
175
|
+
return readPresetPackage(normalized, { targetInput }).id;
|
|
161
176
|
}
|
|
162
177
|
|
|
163
178
|
function taskFilesForBudget({ budget, locale }) {
|
|
@@ -171,60 +186,6 @@ function appendLongRunningContractFile(files, { locale, longRunning }) {
|
|
|
171
186
|
return [...files, [longRunningTaskContractFile, localizedTemplateSource("templates/planning/long-running-task-contract.md", locale)]];
|
|
172
187
|
}
|
|
173
188
|
|
|
174
|
-
function validateLifecycleTransition({ event, currentState, budget, reviewContent = "" }) {
|
|
175
|
-
if (event === "task-review" && currentState !== "in_progress") {
|
|
176
|
-
throw new Error(`task-review requires current state in_progress; current state is ${currentState || "unknown"}`);
|
|
177
|
-
}
|
|
178
|
-
if (event === "task-complete" && budget !== "simple" && currentState !== "review") {
|
|
179
|
-
throw new Error(`task-complete for ${budget} tasks requires current state review. Run task-review first.`);
|
|
180
|
-
}
|
|
181
|
-
if (event === "task-complete" && budget !== "simple") {
|
|
182
|
-
const blockingRisks = collectReviewRisks(reviewContent).filter(isBlockingReviewRisk);
|
|
183
|
-
if (blockingRisks.length > 0) {
|
|
184
|
-
const ids = blockingRisks.map((risk) => risk.id || risk.severity).join(", ");
|
|
185
|
-
throw new Error(`Open blocking review findings must be closed before task-complete: ${ids}`);
|
|
186
|
-
}
|
|
187
|
-
if (!parseReviewConfirmation(reviewContent)?.confirmed) {
|
|
188
|
-
throw new Error("Human review must be confirmed before task-complete. Run review-confirm first.");
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function validateReviewEntryGate(taskDir, budget) {
|
|
194
|
-
if (budget === "simple") return;
|
|
195
|
-
const candidatePath = path.join(taskDir, lessonCandidatesFile);
|
|
196
|
-
if (!fs.existsSync(candidatePath)) {
|
|
197
|
-
throw new Error(`task-review requires ${lessonCandidatesFile} before entering human review.`);
|
|
198
|
-
}
|
|
199
|
-
const phases = parsePhases(readVisualMapContractFile(taskDir).content);
|
|
200
|
-
const actionablePhases = phases.filter((phase) => phase.state !== "skipped");
|
|
201
|
-
const hasRecordedPhaseProgress = actionablePhases.some(
|
|
202
|
-
(phase) =>
|
|
203
|
-
phase.completion > 0 ||
|
|
204
|
-
["in_progress", "review", "blocked", "done"].includes(phase.state) ||
|
|
205
|
-
["partial", "present", "waived"].includes(phase.evidenceStatus),
|
|
206
|
-
);
|
|
207
|
-
if (actionablePhases.length > 0 && !hasRecordedPhaseProgress) {
|
|
208
|
-
throw new Error("task-review requires at least one Visual Map phase progress update. Run task-phase before entering human review.");
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function validateHumanReviewConfirmation({ task, budget }) {
|
|
213
|
-
if (budget === "simple") return;
|
|
214
|
-
const state = task?.state || "unknown";
|
|
215
|
-
const lifecycle = task?.lifecycleState || "";
|
|
216
|
-
if (state !== "review" && !["in_review", "review-blocked"].includes(lifecycle)) {
|
|
217
|
-
throw new Error(`Human review confirmation requires current state review; current state is ${state}. Run task-review first.`);
|
|
218
|
-
}
|
|
219
|
-
if (!task?.walkthroughPath) {
|
|
220
|
-
throw new Error("Human review confirmation requires a walkthrough linked from Closeout SSoT before review-confirm.");
|
|
221
|
-
}
|
|
222
|
-
if (!task?.lessonCandidateDecisionComplete) {
|
|
223
|
-
const status = task?.lessonCandidateStatus || "missing";
|
|
224
|
-
throw new Error(`Human review confirmation requires lesson candidate decision complete; current status is ${status}.`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
189
|
function updateProgressState(content, state, locale) {
|
|
229
190
|
const label = stateLabel(state, locale);
|
|
230
191
|
if (/^##\s*状态[::][^\n]*/im.test(content)) {
|
|
@@ -236,22 +197,6 @@ function updateProgressState(content, state, locale) {
|
|
|
236
197
|
return `${content.trimEnd()}\n\n## Status\n\n${label}\n`;
|
|
237
198
|
}
|
|
238
199
|
|
|
239
|
-
function appendProgressLog(content, { event, message, evidence, actor = "coordinator" }) {
|
|
240
|
-
const timestamp = nowTimestamp();
|
|
241
|
-
const safeMessage = String(message || event).replace(/\r?\n/g, " ").trim();
|
|
242
|
-
const safeEvidence = String(evidence || "n/a").replace(/\r?\n/g, " ").trim();
|
|
243
|
-
if (/^##\s*Log\s*$/im.test(content)) {
|
|
244
|
-
return content.replace(
|
|
245
|
-
/(^##\s*Log\s*$[\s\S]*?\| --- \| --- \| --- \| --- \| --- \|\n)/im,
|
|
246
|
-
`$1| ${timestamp} | ${actor} | ${event}: ${safeMessage} | ${safeEvidence} | ${event === "task-complete" ? "done" : "continue"} |\n`,
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
if (/^##\s*进度记录\s*$/im.test(content)) {
|
|
250
|
-
return `${content.trimEnd()}\n\n### [${timestamp}] - ${event}\n\n- 做了什么:${safeMessage}\n- 验证结果:已记录\n- 下一步:${event === "task-complete" ? "完成" : "继续执行"}\n- 证据:${safeEvidence}\n`;
|
|
251
|
-
}
|
|
252
|
-
return `${content.trimEnd()}\n\n## Log\n\n| Time | Actor | Action | Evidence | Next |\n| --- | --- | --- | --- | --- |\n| ${timestamp} | ${actor} | ${event}: ${safeMessage} | ${safeEvidence} | ${event === "task-complete" ? "done" : "continue"} |\n`;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
200
|
function ensureDatePrefix(slug) {
|
|
256
201
|
if (datePrefix.test(slug)) return slug;
|
|
257
202
|
return `${localDate()}-${slug}`;
|
|
@@ -262,42 +207,53 @@ function bareSlug(datedId) {
|
|
|
262
207
|
return datedId;
|
|
263
208
|
}
|
|
264
209
|
|
|
265
|
-
export function createTask(targetInput, taskId, { title = "", locale = "en-US", dryRun = false, moduleKey = "", budget = "standard", longRunning = false, preset = "", fromSession = "" } = {}) {
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
210
|
+
export function createTask(targetInput, taskId, { title = "", locale = "en-US", dryRun = false, moduleKey = "", budget = "standard", longRunning = false, preset = "", fromSession = "", presetArgs = [] } = {}) {
|
|
211
|
+
const requestedPreset = preset || (moduleKey ? "module" : "");
|
|
212
|
+
const normalizedPreset = normalizeTaskPresetInput(requestedPreset, { targetInput });
|
|
213
|
+
const presetPackage = normalizedPreset === "none" ? null : readPresetPackage(normalizedPreset, { targetInput });
|
|
214
|
+
const presetInputs = presetPackage ? resolvePresetInputs(presetPackage, { cliArgs: presetArgs, fromSession, targetInput }) : null;
|
|
215
|
+
const target = normalizeTarget(presetInputs?.targetInput || targetInput);
|
|
216
|
+
if (presetInputs?.targetInput && targetInput && targetInput !== "." && path.resolve(targetInput) !== path.resolve(presetInputs.targetInput)) {
|
|
217
|
+
throw new Error(`--from-session target mismatch: session target is ${presetInputs.targetInput}`);
|
|
271
218
|
}
|
|
272
|
-
|
|
273
|
-
if (
|
|
274
|
-
if (
|
|
275
|
-
if (
|
|
276
|
-
const rawNormalized = normalizeTaskId(taskId || (
|
|
219
|
+
const normalizedBudget = normalizeTaskBudgetInput(budget);
|
|
220
|
+
if (presetPackage && !presetPackage.compatibleBudgets.includes(normalizedBudget)) throw new Error(`${normalizedPreset} preset requires --budget ${presetPackage.compatibleBudgets.join("|")}`);
|
|
221
|
+
if (presetPackage?.task?.projectLevelOnly === true && moduleKey) throw new Error(`${normalizedPreset} preset is project-level and cannot be combined with --module`);
|
|
222
|
+
if (presetPackage?.task?.requiresFromSession === true && !fromSession) throw new Error(`${normalizedPreset} preset requires --from-session`);
|
|
223
|
+
const rawNormalized = normalizeTaskId(taskId || (presetPackage?.task?.defaultTaskId || ""));
|
|
277
224
|
const normalizedTaskId = ensureDatePrefix(rawNormalized);
|
|
278
225
|
if (!normalizedTaskId) throw new Error("Missing task id");
|
|
279
226
|
const semanticSlug = bareSlug(normalizedTaskId);
|
|
280
227
|
const normalizedModuleKey = moduleKey ? normalizeTaskId(moduleKey) : "";
|
|
281
228
|
const normalizedLocale = normalizeLocale(locale || readCapabilityRegistry(target).locale);
|
|
282
|
-
const normalizedBudget = normalizeTaskBudgetInput(budget);
|
|
283
229
|
const taskTitle = title || (normalizedPreset === "legacy-migration" ? "Harness v1 legacy migration" : semanticSlug);
|
|
284
230
|
const directory = taskRoot(target, normalizedTaskId, { moduleKey: normalizedModuleKey });
|
|
285
231
|
if (fs.existsSync(directory)) throw new Error(`Task already exists: ${normalizedTaskId}`);
|
|
286
|
-
const
|
|
287
|
-
|
|
232
|
+
const evaluatedPresetValues = presetPackage ? evaluateTemplateValues(presetPackage, presetInputs.inputs, { taskId: normalizedTaskId, taskTitle, moduleKey: normalizedModuleKey }) : null;
|
|
233
|
+
const presetContext = presetPackage
|
|
234
|
+
? buildPresetContext({ ...presetPackage, task: { ...(presetPackage.task || {}), kind: presetPackage.task?.kind || "general" } }, {
|
|
235
|
+
target,
|
|
236
|
+
taskDir: directory,
|
|
237
|
+
taskId: normalizedTaskId,
|
|
238
|
+
taskTitle,
|
|
239
|
+
resolvedInputs: presetInputs.inputs,
|
|
240
|
+
evaluatedValues: evaluatedPresetValues,
|
|
241
|
+
})
|
|
288
242
|
: null;
|
|
289
243
|
const changes = [];
|
|
244
|
+
const governanceContext = beginGovernanceSync(target, { operation: `new-task ${normalizedTaskId}`, dryRun });
|
|
245
|
+
try {
|
|
290
246
|
if (normalizedModuleKey) {
|
|
291
247
|
const moduleDirectory = path.dirname(directory);
|
|
292
248
|
for (const [destination, source] of moduleTemplateFiles({ locale: normalizedLocale })) {
|
|
293
249
|
const destinationPath = path.join(moduleDirectory, destination);
|
|
294
250
|
if (fs.existsSync(destinationPath)) continue;
|
|
295
|
-
const sourcePath = path.join(repoRoot, source);
|
|
296
251
|
changes.push({
|
|
297
252
|
destination: toPosix(path.relative(target.projectRoot, destinationPath)),
|
|
298
253
|
source,
|
|
299
254
|
action: dryRun ? "would-create" : "create",
|
|
300
255
|
});
|
|
256
|
+
if (presetPackage) assertPresetWriteScope(presetPackage, toPosix(path.relative(target.projectRoot, destinationPath)));
|
|
301
257
|
if (dryRun) continue;
|
|
302
258
|
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
303
259
|
fs.writeFileSync(
|
|
@@ -317,12 +273,12 @@ export function createTask(targetInput, taskId, { title = "", locale = "en-US",
|
|
|
317
273
|
});
|
|
318
274
|
for (const [destination, source] of files) {
|
|
319
275
|
const destinationPath = path.join(directory, destination);
|
|
320
|
-
const sourcePath = path.join(repoRoot, source);
|
|
321
276
|
changes.push({
|
|
322
277
|
destination: toPosix(path.relative(target.projectRoot, destinationPath)),
|
|
323
278
|
source,
|
|
324
279
|
action: dryRun ? "would-create" : "create",
|
|
325
280
|
});
|
|
281
|
+
if (presetPackage) assertPresetWriteScope(presetPackage, toPosix(path.relative(target.projectRoot, destinationPath)));
|
|
326
282
|
if (dryRun) continue;
|
|
327
283
|
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
328
284
|
fs.writeFileSync(
|
|
@@ -343,211 +299,93 @@ export function createTask(targetInput, taskId, { title = "", locale = "en-US",
|
|
|
343
299
|
source: evidence.source,
|
|
344
300
|
action: dryRun ? "would-create" : "create",
|
|
345
301
|
});
|
|
302
|
+
assertPresetWriteScope(presetPackage, toPosix(evidence.relativePath));
|
|
346
303
|
if (dryRun) continue;
|
|
347
304
|
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
348
305
|
fs.writeFileSync(destinationPath, evidence.content);
|
|
349
306
|
}
|
|
307
|
+
for (const resource of presetContext.resourceFiles || []) {
|
|
308
|
+
const destinationPath = path.join(target.projectRoot, resource.relativePath);
|
|
309
|
+
changes.push({
|
|
310
|
+
destination: toPosix(resource.relativePath),
|
|
311
|
+
source: resource.source,
|
|
312
|
+
action: dryRun ? "would-create" : "create",
|
|
313
|
+
});
|
|
314
|
+
assertPresetWriteScope(presetPackage, toPosix(resource.relativePath));
|
|
315
|
+
if (dryRun) continue;
|
|
316
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
317
|
+
fs.writeFileSync(destinationPath, resource.content);
|
|
318
|
+
}
|
|
319
|
+
for (const [kind, rows] of Object.entries(presetContext.resourceIndexRows || {})) {
|
|
320
|
+
if (!rows.length) continue;
|
|
321
|
+
const destination = kind === "references" ? "references/INDEX.md" : "artifacts/INDEX.md";
|
|
322
|
+
const destinationPath = path.join(directory, destination);
|
|
323
|
+
const relativePath = toPosix(path.relative(target.projectRoot, destinationPath));
|
|
324
|
+
changes.push({
|
|
325
|
+
destination: relativePath,
|
|
326
|
+
source: `preset-${kind}-index`,
|
|
327
|
+
action: dryRun ? "would-update" : "update",
|
|
328
|
+
});
|
|
329
|
+
assertPresetWriteScope(presetPackage, relativePath);
|
|
330
|
+
if (dryRun) continue;
|
|
331
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
332
|
+
const existing = fs.existsSync(destinationPath) ? fs.readFileSync(destinationPath, "utf8") : "";
|
|
333
|
+
fs.writeFileSync(destinationPath, renderPresetResourceIndex(existing, kind, rows));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const task = {
|
|
337
|
+
id: taskIdForDirectory(target, directory),
|
|
338
|
+
shortId: normalizedTaskId,
|
|
339
|
+
title: taskTitle,
|
|
340
|
+
module: normalizedModuleKey || null,
|
|
341
|
+
path: `TARGET:${toPosix(path.relative(target.projectRoot, directory))}`,
|
|
342
|
+
locale: normalizedLocale,
|
|
343
|
+
budget: normalizedBudget,
|
|
344
|
+
kind: presetContext?.kind || "general",
|
|
345
|
+
preset: normalizedPreset,
|
|
346
|
+
presetVersion: presetContext?.presetVersion || "",
|
|
347
|
+
presetAudit: presetContext?.audit || null,
|
|
348
|
+
migrationTargetLevel: presetContext?.migrationTargetLevel || "",
|
|
349
|
+
migrationAchievedLevel: presetContext?.migrationAchievedLevel || "",
|
|
350
|
+
evidenceBundle: presetContext?.evidenceBundle || "",
|
|
351
|
+
longRunning,
|
|
352
|
+
};
|
|
353
|
+
const governance = syncTaskGovernance(target, task, { event: "new-task", state: "planned", message: "task registered by CLI", dryRun });
|
|
354
|
+
changes.push(...governance.changes);
|
|
355
|
+
const commandWriteScopes = governanceRelativePaths(changes);
|
|
356
|
+
if (presetContext) {
|
|
357
|
+
refreshPresetCommandAudit(target, presetContext, { commandWriteScopes, dryRun });
|
|
358
|
+
task.presetAudit = presetContext.audit;
|
|
350
359
|
}
|
|
360
|
+
const commit = commitGovernanceSync(governanceContext, commandWriteScopes, {
|
|
361
|
+
message: `chore(harness): register task ${task.id}`,
|
|
362
|
+
});
|
|
351
363
|
return {
|
|
352
364
|
dryRun,
|
|
353
|
-
task
|
|
354
|
-
id: taskIdForDirectory(target, directory),
|
|
355
|
-
shortId: normalizedTaskId,
|
|
356
|
-
title: taskTitle,
|
|
357
|
-
module: normalizedModuleKey || null,
|
|
358
|
-
path: `TARGET:${toPosix(path.relative(target.projectRoot, directory))}`,
|
|
359
|
-
locale: normalizedLocale,
|
|
360
|
-
budget: normalizedBudget,
|
|
361
|
-
kind: presetContext?.kind || "general",
|
|
362
|
-
preset: normalizedPreset,
|
|
363
|
-
presetVersion: presetContext?.presetVersion || "",
|
|
364
|
-
migrationTargetLevel: presetContext?.migrationTargetLevel || "",
|
|
365
|
-
migrationAchievedLevel: presetContext?.migrationAchievedLevel || "",
|
|
366
|
-
evidenceBundle: presetContext?.evidenceBundle || "",
|
|
367
|
-
longRunning,
|
|
368
|
-
},
|
|
365
|
+
task,
|
|
369
366
|
changes,
|
|
367
|
+
governance: { ...governance, commit },
|
|
370
368
|
};
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function readMigrationSession(fromSession) {
|
|
374
|
-
const sessionPath = path.resolve(fromSession || "");
|
|
375
|
-
if (!sessionPath || !fs.existsSync(sessionPath)) throw new Error(`Migration session not found: ${fromSession}`);
|
|
376
|
-
let session;
|
|
377
|
-
try {
|
|
378
|
-
session = JSON.parse(fs.readFileSync(sessionPath, "utf8"));
|
|
379
|
-
} catch (error) {
|
|
380
|
-
throw new Error(`Invalid migration session JSON: ${error.message}`);
|
|
369
|
+
} finally {
|
|
370
|
+
releaseGovernanceSync(governanceContext);
|
|
381
371
|
}
|
|
382
|
-
if (session.operation !== "migrate-run") throw new Error("legacy-migration preset requires a migrate-run session");
|
|
383
|
-
if (session.planOnly) throw new Error("legacy-migration preset cannot use plan-only session evidence");
|
|
384
|
-
if (!session.target || !fs.existsSync(session.target)) throw new Error(`Migration session target missing: ${session.target || "(none)"}`);
|
|
385
|
-
return { ...session, sourcePath: sessionPath };
|
|
386
372
|
}
|
|
387
373
|
|
|
388
|
-
function
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
return {
|
|
395
|
-
kind: "project-migration",
|
|
396
|
-
preset: "legacy-migration",
|
|
397
|
-
presetVersion: "v1",
|
|
398
|
-
migrationTargetLevel: targetLevel,
|
|
399
|
-
migrationAchievedLevel: achievedLevel,
|
|
400
|
-
evidenceBundle,
|
|
401
|
-
session,
|
|
402
|
-
evidenceFiles: legacyMigrationEvidenceFiles({ target, session, evidenceBundle, verifyResult }),
|
|
374
|
+
function refreshPresetCommandAudit(target, presetContext, { commandWriteScopes = [], dryRun = false } = {}) {
|
|
375
|
+
const scopes = [...new Set(commandWriteScopes.filter(Boolean))];
|
|
376
|
+
presetContext.audit = {
|
|
377
|
+
...presetContext.audit,
|
|
378
|
+
presetWriteScopes: presetContext.audit.writeScopes || [],
|
|
379
|
+
commandWriteScopes: scopes,
|
|
403
380
|
};
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
relativePath: path.join(evidenceBundle, name),
|
|
410
|
-
source,
|
|
411
|
-
content: `${JSON.stringify(value, null, 2)}\n`,
|
|
412
|
-
});
|
|
413
|
-
const addText = (name, value, source = "generated") => files.push({
|
|
414
|
-
relativePath: path.join(evidenceBundle, name),
|
|
415
|
-
source,
|
|
416
|
-
content: `${String(value || "").trim()}\n`,
|
|
417
|
-
});
|
|
418
|
-
addJson("session.json", session, "session.json");
|
|
419
|
-
addJson("migrate-plan.json", session.plan || {}, "migrate-plan.json");
|
|
420
|
-
addJson("normal-check.json", session.checks?.normal || {}, "session.checks.normal");
|
|
421
|
-
addJson("strict-check.json", session.checks?.strict || {}, "session.checks.strict");
|
|
422
|
-
addJson("migrate-verify.json", verifyResult, "migrate-verify");
|
|
423
|
-
addText("dashboard.hash.txt", dashboardHash(session.dashboard?.indexPath || ""), "dashboard");
|
|
424
|
-
addText("target-git-status.txt", JSON.stringify(session.git?.after || {}, null, 2), "session.git.after");
|
|
425
|
-
addText("target-commit.txt", targetCommit(target.projectRoot), "git");
|
|
426
|
-
addText("harness-version.txt", packageVersion(), "package.json");
|
|
427
|
-
addText("generated-at.txt", new Date().toISOString(), "generated");
|
|
428
|
-
return files;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function dashboardHash(indexPath) {
|
|
432
|
-
if (!indexPath || !fs.existsSync(indexPath)) return "missing";
|
|
433
|
-
const hash = crypto.createHash("sha256").update(fs.readFileSync(indexPath)).digest("hex");
|
|
434
|
-
return `sha256:${hash}`;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function targetCommit(projectRoot) {
|
|
438
|
-
const result = spawnSync("git", ["-C", projectRoot, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
439
|
-
return result.status === 0 ? result.stdout.trim() : "n/a";
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function packageVersion() {
|
|
443
|
-
try {
|
|
444
|
-
return JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")).version || "unknown";
|
|
445
|
-
} catch {
|
|
446
|
-
return "unknown";
|
|
381
|
+
for (const evidence of presetContext.evidenceFiles || []) {
|
|
382
|
+
if (evidence.source !== "preset-audit") continue;
|
|
383
|
+
evidence.content = `${JSON.stringify(presetContext.audit, null, 2)}\n`;
|
|
384
|
+
if (dryRun) continue;
|
|
385
|
+
fs.writeFileSync(path.join(target.projectRoot, evidence.relativePath), evidence.content);
|
|
447
386
|
}
|
|
448
387
|
}
|
|
449
388
|
|
|
450
|
-
function renderPresetTaskTemplate(destination, content, presetContext) {
|
|
451
|
-
if (!presetContext) return content;
|
|
452
|
-
if (destination === "task_plan.md") return renderLegacyMigrationTaskPlan(content, presetContext);
|
|
453
|
-
if (destination === "execution_strategy.md") return `${content.trimEnd()}\n\n${legacyMigrationExecutionStrategy(presetContext)}\n`;
|
|
454
|
-
if (destination === "findings.md") return `${content.trimEnd()}\n\n${legacyMigrationFindings(presetContext)}\n`;
|
|
455
|
-
if (destination === "review.md") return `${content.trimEnd()}\n\n${legacyMigrationReview(presetContext)}\n`;
|
|
456
|
-
if (destination === visualMapFile) return `${content.trimEnd()}\n\n${legacyMigrationVisualMap()}\n`;
|
|
457
|
-
return content;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function renderLegacyMigrationTaskPlan(content, context) {
|
|
461
|
-
const metadata = [
|
|
462
|
-
`Selected budget: complex`,
|
|
463
|
-
`Task Kind: ${context.kind}`,
|
|
464
|
-
`Task Preset: ${context.preset}`,
|
|
465
|
-
`Preset Version: ${context.presetVersion}`,
|
|
466
|
-
`Migration Target Level: ${context.migrationTargetLevel}`,
|
|
467
|
-
`Migration Achieved Level: ${context.migrationAchievedLevel}`,
|
|
468
|
-
`Evidence Bundle: ${context.evidenceBundle}`,
|
|
469
|
-
].join("\n");
|
|
470
|
-
let next = String(content).replace(/^(Task Contract:\s*harness-task\/v1\s*)$/im, `$1\n${metadata}`);
|
|
471
|
-
next = next.replace("[State the outcome this task must deliver in one sentence.]", "Create a controlled Harness v1 migration task from the recorded migrate-run session without rewriting history automatically.");
|
|
472
|
-
next = next.replace("[用一句话说明本任务完成后应达到的状态。]", "基于已记录的 migrate-run session 创建受控的 Harness v1 迁移任务,不自动改写历史材料。");
|
|
473
|
-
return `${next.trimEnd()}\n\n## Legacy Migration Preset\n\nThis Complex Task uses the legacy-migration preset. The preset only scaffolds the task and records evidence. It does not run migration, rewrite historical task bodies, stage files, or commit changes.\n\n- Baseline session: \`${context.evidenceBundle}/session.json\`\n- Migration plan: \`${context.evidenceBundle}/migrate-plan.json\`\n- Strict deferred: ${context.session.strictDeferred ? "yes" : "no"}\n- Full-cutover claim allowed now: ${context.migrationAchievedLevel === "migration-full-cutover" ? "yes" : "no"}\n`;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function legacyMigrationExecutionStrategy(context) {
|
|
477
|
-
return `## Legacy Migration Preset Strategy
|
|
478
|
-
|
|
479
|
-
This preset keeps migration inside the Complex Task contract.
|
|
480
|
-
|
|
481
|
-
| Area | Rule |
|
|
482
|
-
| --- | --- |
|
|
483
|
-
| Write boundary | Do not rewrite historical task bodies unless the user explicitly confirms that phase. |
|
|
484
|
-
| Evidence source | Use \`${context.evidenceBundle}/\` as the handoff bundle. Absolute session paths are origin data only. |
|
|
485
|
-
| Target level | \`${context.migrationTargetLevel}\` |
|
|
486
|
-
| Achieved level | \`${context.migrationAchievedLevel}\` |
|
|
487
|
-
|
|
488
|
-
## Subagent Lane Table
|
|
489
|
-
|
|
490
|
-
Declare lanes before dispatching workers.
|
|
491
|
-
|
|
492
|
-
| Lane ID | Allowed globs | Forbidden globs | Shared file owner | Worktree / branch | Handoff path | Merge order | Verification command |
|
|
493
|
-
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
494
|
-
| coordinator | docs/09-PLANNING/TASKS/** | AGENTS.md, CLAUDE.md, docs/Harness-Ledger.md until closeout | coordinator | current | progress.md | 1 | harness check --profile target-project . |
|
|
495
|
-
`;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function legacyMigrationFindings(context) {
|
|
499
|
-
return `## Legacy Migration Action Buckets
|
|
500
|
-
|
|
501
|
-
| Bucket | Count | Owner | Status | Next Action |
|
|
502
|
-
| --- | ---: | --- | --- | --- |
|
|
503
|
-
| warnings | ${context.session.plan?.summary?.warnings || 0} | coordinator | open | Triage before increasing target level |
|
|
504
|
-
| taskActions | ${context.session.plan?.summary?.taskActions || 0} | coordinator | open | Upgrade only current/reopened/current-evidence tasks |
|
|
505
|
-
| legacyResiduals | ${context.session.plan?.summary?.legacyResiduals || 0} | coordinator | open | Assign real owner before full cutover |
|
|
506
|
-
|
|
507
|
-
## Residual Policy
|
|
508
|
-
|
|
509
|
-
Residuals require reason, owner, trigger, next action, and reviewer. Placeholder owner \`migration-owner\` is not a real owner.
|
|
510
|
-
|
|
511
|
-
## Status Conflict Table
|
|
512
|
-
|
|
513
|
-
| Item | Competing Evidence | Chosen Classification | Confidence | Human Needed |
|
|
514
|
-
| --- | --- | --- | --- | --- |
|
|
515
|
-
| pending | session / SSoT / progress / git | pending | medium | yes |
|
|
516
|
-
`;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function legacyMigrationReview(context) {
|
|
520
|
-
return `## Legacy Migration Preset Gate
|
|
521
|
-
|
|
522
|
-
\`migration-full-cutover\` can only be claimed when the final session proves all gates:
|
|
523
|
-
|
|
524
|
-
- final session result is \`complete\`
|
|
525
|
-
- strict check passes
|
|
526
|
-
- \`migrate-verify --full-cutover\` passes
|
|
527
|
-
- warnings/actions/residuals/strictDeferred are zero
|
|
528
|
-
- dashboard evidence is readable
|
|
529
|
-
- review has no open P0/P1/P2 blocker
|
|
530
|
-
|
|
531
|
-
Current achieved level: \`${context.migrationAchievedLevel}\`.
|
|
532
|
-
`;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function legacyMigrationVisualMap() {
|
|
536
|
-
return `## Legacy Migration Preset Flow
|
|
537
|
-
|
|
538
|
-
\`\`\`mermaid
|
|
539
|
-
flowchart TD
|
|
540
|
-
A["Recorded migrate-run session"] --> B["Create Complex Task preset"]
|
|
541
|
-
B --> C["Baseline usable"]
|
|
542
|
-
C --> D{"User confirms deeper cutover?"}
|
|
543
|
-
D -- no --> E["Keep residuals owned"]
|
|
544
|
-
D -- yes --> F["Current work cutover"]
|
|
545
|
-
F --> G["Historical consolidation"]
|
|
546
|
-
G --> H["Strict / full-cutover verify"]
|
|
547
|
-
\`\`\`
|
|
548
|
-
`;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
389
|
export function updateTaskLifecycle(targetInput, taskId, { event = "task-log", state = "", message = "", evidence = "" } = {}) {
|
|
552
390
|
const target = normalizeTarget(targetInput);
|
|
553
391
|
const taskDir = resolveTaskDirectory(target, taskId);
|
|
@@ -556,109 +394,114 @@ export function updateTaskLifecycle(targetInput, taskId, { event = "task-log", s
|
|
|
556
394
|
const normalizedState = state ? String(state).toLowerCase().replaceAll("-", "_") : "";
|
|
557
395
|
if (normalizedState && !allowedTaskStates.has(normalizedState)) throw new Error(`Invalid task state: ${state}`);
|
|
558
396
|
const currentTask = findTaskByDirectory(target, taskDir);
|
|
397
|
+
const canonicalTaskId = taskIdForDirectory(target, taskDir);
|
|
559
398
|
const budget = parseTaskBudget(readFileSafe(path.join(taskDir, "task_plan.md")));
|
|
560
399
|
validateLifecycleTransition({
|
|
561
400
|
event,
|
|
562
401
|
currentState: currentTask?.state || "unknown",
|
|
563
402
|
budget,
|
|
564
403
|
reviewContent: readFileSafe(path.join(taskDir, "review.md")),
|
|
404
|
+
reviewTaskKey: canonicalTaskId,
|
|
405
|
+
projectRoot: target.projectRoot,
|
|
406
|
+
taskDir,
|
|
565
407
|
});
|
|
566
408
|
if (event === "task-review") validateReviewEntryGate(taskDir, budget);
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
409
|
+
const governanceContext = beginGovernanceSync(target, { operation: `${event} ${canonicalTaskId}` });
|
|
410
|
+
try {
|
|
411
|
+
let content = readFileSafe(progressPath);
|
|
412
|
+
if (normalizedState) content = updateProgressState(content, normalizedState, registry.locale);
|
|
413
|
+
content = appendProgressLog(content, { event, message, evidence });
|
|
414
|
+
fs.writeFileSync(progressPath, content.endsWith("\n") ? content : `${content}\n`);
|
|
415
|
+
const allowedPaths = [toPosix(path.relative(target.projectRoot, progressPath))];
|
|
416
|
+
if (event === "task-review") {
|
|
417
|
+
const reviewPath = path.join(taskDir, "review.md");
|
|
418
|
+
const reviewContent = readFileSafe(reviewPath);
|
|
419
|
+
fs.writeFileSync(
|
|
420
|
+
reviewPath,
|
|
421
|
+
replaceAgentReviewSubmission(
|
|
422
|
+
reviewContent,
|
|
423
|
+
renderAgentReviewSubmission({
|
|
424
|
+
target,
|
|
425
|
+
taskDir,
|
|
426
|
+
canonicalTaskId,
|
|
427
|
+
message,
|
|
428
|
+
evidence,
|
|
429
|
+
}),
|
|
430
|
+
),
|
|
431
|
+
);
|
|
432
|
+
allowedPaths.push(toPosix(path.relative(target.projectRoot, reviewPath)));
|
|
433
|
+
}
|
|
434
|
+
const task =
|
|
435
|
+
findTaskByDirectory(target, taskDir) ||
|
|
436
|
+
{
|
|
437
|
+
id: canonicalTaskId,
|
|
438
|
+
shortId: path.basename(taskDir),
|
|
439
|
+
title: canonicalTaskId,
|
|
440
|
+
path: `TARGET:${toPosix(path.relative(target.projectRoot, taskDir))}`,
|
|
441
|
+
state: normalizedState || currentTask?.state || "unknown",
|
|
442
|
+
};
|
|
443
|
+
const governanceState = normalizedState || task.state || currentTask?.state || "planned";
|
|
444
|
+
const governance = syncTaskGovernance(target, task, { event, state: governanceState, message, dryRun: false });
|
|
445
|
+
const commit = commitGovernanceSync(governanceContext, [...allowedPaths, ...governanceRelativePaths(governance.changes)], {
|
|
446
|
+
message: `chore(harness): advance task ${canonicalTaskId} to ${governanceState}`,
|
|
447
|
+
});
|
|
448
|
+
return {
|
|
449
|
+
event,
|
|
450
|
+
task,
|
|
451
|
+
governance: { ...governance, commit },
|
|
452
|
+
};
|
|
453
|
+
} finally {
|
|
454
|
+
releaseGovernanceSync(governanceContext);
|
|
455
|
+
}
|
|
575
456
|
}
|
|
576
457
|
|
|
577
458
|
export function confirmTaskReview(targetInput, taskId, { reviewer = "Human Reviewer", message = "", confirmText = "", evidence = "" } = {}) {
|
|
578
459
|
const target = normalizeTarget(targetInput);
|
|
579
460
|
const taskDir = resolveTaskDirectory(target, taskId);
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if (confirmText && ![shortId, canonicalTaskId].includes(confirmText)) {
|
|
584
|
-
throw new Error(`Review confirmation text must match task id: ${shortId}`);
|
|
585
|
-
}
|
|
586
|
-
if (!confirmText) throw new Error(`Missing review confirmation text: ${shortId}`);
|
|
587
|
-
|
|
588
|
-
const reviewPath = path.join(taskDir, "review.md");
|
|
589
|
-
const progressPath = path.join(taskDir, "progress.md");
|
|
590
|
-
const reviewContent = readFileSafe(reviewPath);
|
|
591
|
-
const budget = parseTaskBudget(readFileSafe(path.join(taskDir, "task_plan.md")));
|
|
592
|
-
const candidateStatus = parseLessonCandidateStatus(readFileSafe(path.join(taskDir, lessonCandidatesFile)));
|
|
593
|
-
const blockingRisks = collectReviewRisks(reviewContent).filter(isBlockingReviewRisk);
|
|
594
|
-
if (blockingRisks.length > 0) {
|
|
595
|
-
const ids = blockingRisks.map((risk) => risk.id || risk.severity).join(", ");
|
|
596
|
-
throw new Error(`Open blocking review findings must be closed before confirmation: ${ids}`);
|
|
597
|
-
}
|
|
598
|
-
validateHumanReviewConfirmation({
|
|
599
|
-
task: findTaskByDirectory(target, taskDir),
|
|
600
|
-
budget,
|
|
601
|
-
});
|
|
602
|
-
if (budget !== "simple" && !isLessonCandidateDecisionComplete(candidateStatus)) {
|
|
603
|
-
throw new Error(`Human review confirmation requires lesson candidate decision complete; current status is ${candidateStatus.status}.`);
|
|
604
|
-
}
|
|
605
|
-
|
|
461
|
+
return confirmTaskReviewWithContext({ target, taskDir, findTaskByDirectory }, { reviewer, message, confirmText, evidence });
|
|
462
|
+
}
|
|
463
|
+
function renderAgentReviewSubmission({ target, taskDir, canonicalTaskId, message, evidence }) {
|
|
606
464
|
const timestamp = nowTimestamp();
|
|
607
|
-
const
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
465
|
+
const submissionId = `ARS-${timestamp.replace(/[^0-9]/g, "").slice(0, 14)}`;
|
|
466
|
+
const materialsHash = hashTaskMaterials(taskDir);
|
|
467
|
+
const reviewContent = readFileSafe(path.join(taskDir, "review.md"));
|
|
468
|
+
const openFindings = collectReviewRisks(reviewContent).filter(isBlockingReviewRisk).length;
|
|
469
|
+
const evidenceSummary = evidence || message || "Agent submitted task for human review.";
|
|
470
|
+
return [
|
|
471
|
+
"## Agent Review Submission",
|
|
614
472
|
"",
|
|
615
|
-
"|
|
|
616
|
-
"| --- | --- |
|
|
617
|
-
`|
|
|
473
|
+
"| Field | Value |",
|
|
474
|
+
"| --- | --- |",
|
|
475
|
+
`| Submission ID | ${submissionId} |`,
|
|
476
|
+
`| Submitted At | ${timestamp} |`,
|
|
477
|
+
"| Submitted By | agent |",
|
|
478
|
+
`| Task Key | ${canonicalTaskId} |`,
|
|
479
|
+
`| Materials Checklist Hash | ${materialsHash} |`,
|
|
480
|
+
`| Evidence Summary | ${markdownCell(evidenceSummary)} |`,
|
|
481
|
+
`| Open Findings Count | ${openFindings} |`,
|
|
482
|
+
`| Scanner Version | ${taskScannerVersion} |`,
|
|
483
|
+
`| Target | TARGET:${toPosix(path.relative(target.projectRoot, taskDir))} |`,
|
|
618
484
|
"",
|
|
619
485
|
].join("\n");
|
|
620
|
-
const nextReview = replaceReviewConfirmation(reviewContent, confirmationBlock);
|
|
621
|
-
fs.writeFileSync(reviewPath, nextReview.endsWith("\n") ? nextReview : `${nextReview}\n`);
|
|
622
|
-
|
|
623
|
-
let progressContent = readFileSafe(progressPath);
|
|
624
|
-
progressContent = appendProgressLog(progressContent, {
|
|
625
|
-
event: "review-confirm",
|
|
626
|
-
message: message || `Human review confirmed by ${reviewer}`,
|
|
627
|
-
evidence: evidence || `TARGET:docs/09-PLANNING/${canonicalTaskId}/review.md`,
|
|
628
|
-
actor: reviewer || "Human Reviewer",
|
|
629
|
-
});
|
|
630
|
-
fs.writeFileSync(progressPath, progressContent.endsWith("\n") ? progressContent : `${progressContent}\n`);
|
|
631
|
-
|
|
632
|
-
return {
|
|
633
|
-
event: "review-confirm",
|
|
634
|
-
task: findTaskByDirectory(target, taskDir) || { id: canonicalTaskId, reviewStatus: "confirmed" },
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
function assertTaskDirectoryInsidePlanning(target, taskDir) {
|
|
639
|
-
const realTaskDir = fs.realpathSync(taskDir);
|
|
640
|
-
const allowedRoots = [
|
|
641
|
-
path.join(target.docsRoot, "09-PLANNING/TASKS"),
|
|
642
|
-
path.join(target.docsRoot, "09-PLANNING/MODULES"),
|
|
643
|
-
].filter(fs.existsSync).map((root) => fs.realpathSync(root));
|
|
644
|
-
if (!allowedRoots.some((root) => realTaskDir === root || realTaskDir.startsWith(`${root}${path.sep}`))) {
|
|
645
|
-
throw new Error(`Task directory outside planning root: ${taskIdForDirectory(target, taskDir)}`);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
function markdownCell(value) {
|
|
650
|
-
return String(value || "")
|
|
651
|
-
.replace(/\r?\n/g, " ")
|
|
652
|
-
.replaceAll("|", "\\|")
|
|
653
|
-
.trim();
|
|
654
486
|
}
|
|
655
|
-
|
|
656
|
-
function replaceReviewConfirmation(content, block) {
|
|
487
|
+
function replaceAgentReviewSubmission(content, block) {
|
|
657
488
|
const trimmed = String(content || "").trimEnd();
|
|
658
|
-
if (/^##\s*(?:
|
|
659
|
-
return trimmed.replace(/^##\s*(?:
|
|
489
|
+
if (/^##\s*(?:Agent Review Submission|Agent 审查提交|Agent 提交审查)\s*$/im.test(trimmed)) {
|
|
490
|
+
return `${trimmed.replace(/^##\s*(?:Agent Review Submission|Agent 审查提交|Agent 提交审查)\s*$[\s\S]*?(?=^##\s+|(?![\s\S]))/im, `${block.trimEnd()}\n\n`)}\n`;
|
|
491
|
+
}
|
|
492
|
+
return `${trimmed}\n\n${block.trimEnd()}\n`;
|
|
493
|
+
}
|
|
494
|
+
function hashTaskMaterials(taskDir) {
|
|
495
|
+
const hash = crypto.createHash("sha256");
|
|
496
|
+
for (const fileName of ["brief.md", "task_plan.md", visualMapFile, lessonCandidatesFile, "progress.md", "review.md", "findings.md", longRunningTaskContractFile]) {
|
|
497
|
+
const filePath = path.join(taskDir, fileName);
|
|
498
|
+
if (!fs.existsSync(filePath)) continue;
|
|
499
|
+
hash.update(fileName);
|
|
500
|
+
hash.update("\0");
|
|
501
|
+
hash.update(readFileSafe(filePath));
|
|
502
|
+
hash.update("\0");
|
|
660
503
|
}
|
|
661
|
-
return
|
|
504
|
+
return hash.digest("hex").slice(0, 16);
|
|
662
505
|
}
|
|
663
506
|
|
|
664
507
|
export function updateTaskPhase(targetInput, taskId, phaseId, { state = "", completion = "", evidenceStatus = "" } = {}) {
|
|
@@ -692,9 +535,17 @@ export function updateTaskPhase(targetInput, taskId, phaseId, { state = "", comp
|
|
|
692
535
|
return next;
|
|
693
536
|
});
|
|
694
537
|
if (!phaseUpdate.matched) throw new Error(`Phase not found: ${phaseId}`);
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
538
|
+
const governanceContext = beginGovernanceSync(target, { operation: `task-phase ${taskId} ${phaseId}` });
|
|
539
|
+
try {
|
|
540
|
+
content = phaseUpdate.content;
|
|
541
|
+
fs.writeFileSync(visualMapPath, content);
|
|
542
|
+
const commit = commitGovernanceSync(governanceContext, [toPosix(path.relative(target.projectRoot, visualMapPath))], {
|
|
543
|
+
message: `chore(harness): update task phase ${taskId} ${phaseId}`,
|
|
544
|
+
});
|
|
545
|
+
return { event: "task-phase", task: findTaskByDirectory(target, taskDir), phaseId, governance: { commit } };
|
|
546
|
+
} finally {
|
|
547
|
+
releaseGovernanceSync(governanceContext);
|
|
548
|
+
}
|
|
698
549
|
}
|
|
699
550
|
|
|
700
551
|
export function updateModuleStep(targetInput, moduleKey, stepId, { state = "" } = {}) {
|
|
@@ -714,42 +565,85 @@ export function updateModuleStep(targetInput, moduleKey, stepId, { state = "" }
|
|
|
714
565
|
return next;
|
|
715
566
|
});
|
|
716
567
|
if (!stepUpdate.matched) throw new Error(`Module step not found: ${stepId}`);
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
568
|
+
const governanceContext = beginGovernanceSync(target, { operation: `module-step ${normalizedModuleKey} ${stepId}` });
|
|
569
|
+
try {
|
|
570
|
+
content = stepUpdate.content;
|
|
571
|
+
fs.writeFileSync(modulePlanPath, content);
|
|
572
|
+
|
|
573
|
+
const registryPath = path.join(target.docsRoot, "09-PLANNING/Module-Registry.md");
|
|
574
|
+
if (fs.existsSync(registryPath)) {
|
|
575
|
+
let registry = readFileSafe(registryPath);
|
|
576
|
+
const registryUpdate = updateMarkdownTableRow(registry, /^(ID|模块 Key)$/i, (header, row) => {
|
|
577
|
+
const moduleIndex = firstColumn(header, ["Module", "模块", "模块 Key"]);
|
|
578
|
+
const taskPlanIndex = getColumn(header, "Task Plan");
|
|
579
|
+
const matchesModule = normalizeTaskId(row[moduleIndex] || "") === normalizedModuleKey;
|
|
580
|
+
const matchesPlan = taskPlanIndex >= 0 && String(row[taskPlanIndex] || "").includes(`/MODULES/${normalizedModuleKey}/`);
|
|
581
|
+
if (!matchesModule && !matchesPlan) return null;
|
|
582
|
+
const next = [...row];
|
|
583
|
+
const statusIndex = firstColumn(header, ["Status", "状态"]);
|
|
584
|
+
const updatedIndex = firstColumn(header, ["Updated", "更新时间"]);
|
|
585
|
+
const currentStepIndex = firstColumn(header, ["Current Step", "当前步骤"]);
|
|
586
|
+
const chineseRegistry = header.some((cell) => /模块 Key|模块名称|状态|更新时间/.test(cell));
|
|
587
|
+
if (statusIndex >= 0) {
|
|
588
|
+
next[statusIndex] = normalizedState === "done"
|
|
589
|
+
? chineseRegistry ? "completed" : "merged"
|
|
590
|
+
: normalizedState === "in-progress" ? chineseRegistry ? "in-progress" : "active" : normalizedState;
|
|
591
|
+
}
|
|
592
|
+
if (currentStepIndex >= 0) next[currentStepIndex] = stepId;
|
|
593
|
+
if (updatedIndex >= 0) next[updatedIndex] = todayDate();
|
|
594
|
+
return next;
|
|
595
|
+
});
|
|
596
|
+
registry = registryUpdate.content;
|
|
597
|
+
fs.writeFileSync(registryPath, registry);
|
|
598
|
+
}
|
|
599
|
+
const governance = syncModuleStepGovernance(target, { moduleKey: normalizedModuleKey, stepId, state: normalizedState });
|
|
600
|
+
const commit = commitGovernanceSync(
|
|
601
|
+
governanceContext,
|
|
602
|
+
[
|
|
603
|
+
toPosix(path.relative(target.projectRoot, modulePlanPath)),
|
|
604
|
+
toPosix(path.relative(target.projectRoot, registryPath)),
|
|
605
|
+
...governanceRelativePaths(governance.changes),
|
|
606
|
+
],
|
|
607
|
+
{ message: `chore(harness): update module ${normalizedModuleKey} step ${stepId}` },
|
|
608
|
+
);
|
|
609
|
+
return { event: "module-step", moduleKey: normalizedModuleKey, stepId, state: normalizedState, governance: { ...governance, commit } };
|
|
610
|
+
} finally {
|
|
611
|
+
releaseGovernanceSync(governanceContext);
|
|
745
612
|
}
|
|
746
|
-
return { event: "module-step", moduleKey: normalizedModuleKey, stepId, state: normalizedState };
|
|
747
613
|
}
|
|
748
614
|
|
|
749
|
-
export function listLifecycleTasks(targetInput, { state = "", moduleKey = "" } = {}) {
|
|
615
|
+
export function listLifecycleTasks(targetInput, { state = "", moduleKey = "", queue = "", preset = "", review = "", lesson = "", search = "", missingMaterials = false } = {}) {
|
|
750
616
|
const target = normalizeTarget(targetInput);
|
|
751
617
|
let tasks = collectTasks(target);
|
|
752
618
|
if (state) tasks = tasks.filter((task) => task.state === String(state).toLowerCase().replaceAll("-", "_"));
|
|
753
619
|
if (moduleKey) tasks = tasks.filter((task) => task.module === normalizeTaskId(moduleKey));
|
|
620
|
+
if (queue) {
|
|
621
|
+
const normalizedQueue = queryToken(queue);
|
|
622
|
+
tasks = tasks.filter((task) => (task.taskQueues || []).map(queryToken).includes(normalizedQueue));
|
|
623
|
+
}
|
|
624
|
+
if (preset) tasks = tasks.filter((task) => queryToken(task.taskPreset || "none") === queryToken(preset));
|
|
625
|
+
if (review) tasks = tasks.filter((task) => queryToken(task.reviewStatus || "") === queryToken(review));
|
|
626
|
+
if (lesson) {
|
|
627
|
+
const needle = queryToken(lesson);
|
|
628
|
+
tasks = tasks.filter((task) => [task.lessonCandidateStatus, task.lessonCandidateReviewDecision, task.lessonCandidatePromotionState].some((value) => queryToken(value) === needle));
|
|
629
|
+
}
|
|
630
|
+
if (missingMaterials) tasks = tasks.filter((task) => !task.materialsReady);
|
|
631
|
+
if (search) {
|
|
632
|
+
const needle = String(search).toLowerCase();
|
|
633
|
+
tasks = tasks.filter((task) => [
|
|
634
|
+
task.id,
|
|
635
|
+
task.taskKey,
|
|
636
|
+
task.shortId,
|
|
637
|
+
task.title,
|
|
638
|
+
task.currentPath,
|
|
639
|
+
task.taskPlanPath,
|
|
640
|
+
task.module,
|
|
641
|
+
task.inferredModule,
|
|
642
|
+
].some((value) => String(value || "").toLowerCase().includes(needle)));
|
|
643
|
+
}
|
|
754
644
|
return { tasks };
|
|
755
645
|
}
|
|
646
|
+
|
|
647
|
+
function queryToken(value) {
|
|
648
|
+
return String(value || "").trim().toLowerCase().replaceAll("_", "-");
|
|
649
|
+
}
|