coding-agent-harness 1.0.2 → 1.0.5
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 +32 -0
- package/CONTRIBUTING.md +98 -0
- package/LICENSE +661 -21
- package/LICENSE-EXCEPTION.md +37 -0
- package/README.md +244 -87
- package/README.zh-CN.md +77 -35
- package/SKILL.md +32 -24
- 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/architecture/system-explainer/01-system-overview.md +217 -0
- package/docs-release/architecture/system-explainer/02-module-dependency.md +257 -0
- package/docs-release/architecture/system-explainer/03-task-lifecycle.md +304 -0
- package/docs-release/architecture/system-explainer/04-check-and-governance.md +239 -0
- package/docs-release/architecture/system-explainer/05-data-flow.md +276 -0
- package/docs-release/architecture/system-explainer/06-preset-and-migration.md +303 -0
- package/docs-release/architecture/system-explainer/README.md +67 -0
- package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +226 -0
- package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +263 -0
- package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +319 -0
- package/docs-release/architecture/system-explainer/en-US/04-check-and-governance.md +250 -0
- package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +290 -0
- package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +323 -0
- package/docs-release/architecture/system-explainer/en-US/README.md +70 -0
- package/docs-release/assets/dashboard-overview.png +0 -0
- package/docs-release/guides/agent-installation.en-US.md +39 -15
- package/docs-release/guides/agent-installation.md +43 -16
- 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 +238 -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 +224 -0
- package/docs-release/guides/task-state-machine.md +231 -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/INDEX.md +60 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
- package/package.json +10 -4
- 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 +126 -0
- package/scripts/commands/preset-command.mjs +73 -0
- package/scripts/commands/task-command.mjs +328 -0
- package/scripts/harness.mjs +59 -260
- package/scripts/lib/capability-registry.mjs +82 -28
- package/scripts/lib/check-module-parallel.mjs +230 -0
- package/scripts/lib/check-profiles.mjs +90 -228
- package/scripts/lib/check-task-contracts.mjs +55 -0
- package/scripts/lib/core-shared.mjs +65 -2
- package/scripts/lib/dashboard-data.mjs +155 -24
- package/scripts/lib/dashboard-workbench.mjs +131 -12
- package/scripts/lib/dashboard-writer.mjs +20 -4
- package/scripts/lib/git-status-summary.mjs +46 -0
- package/scripts/lib/governance-index-generator.mjs +174 -0
- package/scripts/lib/governance-sync.mjs +611 -0
- package/scripts/lib/governance-table-boundary.mjs +175 -0
- package/scripts/lib/harness-core.mjs +6 -0
- package/scripts/lib/lesson-maintenance.mjs +36 -29
- package/scripts/lib/markdown-utils.mjs +33 -0
- package/scripts/lib/migration-planner.mjs +4 -6
- package/scripts/lib/migration-support.mjs +1 -1
- package/scripts/lib/phase-kind.mjs +50 -0
- package/scripts/lib/preset-audit-contracts.mjs +37 -0
- package/scripts/lib/preset-engine.mjs +494 -0
- package/scripts/lib/preset-registry.mjs +776 -0
- package/scripts/lib/preset-resource-contracts.mjs +83 -0
- package/scripts/lib/review-confirm-git-gate.mjs +248 -0
- package/scripts/lib/status-builder.mjs +88 -0
- package/scripts/lib/status-dashboard-renderer.mjs +105 -0
- package/scripts/lib/subagent-authorization-audit.mjs +196 -0
- package/scripts/lib/task-audit-metadata.mjs +385 -0
- package/scripts/lib/task-audit-migration.mjs +350 -0
- package/scripts/lib/task-completion-consistency.mjs +26 -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/create-task-helpers.mjs +67 -0
- package/scripts/lib/task-lifecycle/phase-sync.mjs +88 -0
- package/scripts/lib/task-lifecycle/review-confirm.mjs +112 -0
- package/scripts/lib/task-lifecycle/review-gates.mjs +73 -0
- package/scripts/lib/task-lifecycle/review-submission.mjs +63 -0
- package/scripts/lib/task-lifecycle/scaffold-provenance.mjs +49 -0
- package/scripts/lib/task-lifecycle/template-files.mjs +53 -0
- package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
- package/scripts/lib/task-lifecycle.mjs +338 -477
- package/scripts/lib/task-metadata.mjs +118 -0
- package/scripts/lib/task-review-model.mjs +455 -0
- package/scripts/lib/task-scanner.mjs +193 -372
- 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 +43 -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 +24 -18
- package/templates/dashboard/assets/app-src/00-state.js +13 -0
- package/templates/dashboard/assets/app-src/10-router.js +5 -1
- package/templates/dashboard/assets/app-src/20-overview.js +18 -8
- package/templates/dashboard/assets/app-src/30-tasks.js +92 -246
- package/templates/dashboard/assets/app-src/35-task-detail.js +286 -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/55-presets.js +375 -0
- package/templates/dashboard/assets/app-src/60-shared.js +3 -1
- package/templates/dashboard/assets/app-src/90-bindings.js +302 -29
- package/templates/dashboard/assets/app.css +1501 -376
- package/templates/dashboard/assets/app.css.manifest.json +10 -0
- package/templates/dashboard/assets/app.js +1240 -101
- package/templates/dashboard/assets/app.manifest.json +2 -0
- package/templates/dashboard/assets/css-src/00-foundation.css +346 -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 +489 -0
- package/templates/dashboard/assets/css-src/45-presets.css +516 -0
- package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
- package/templates/dashboard/assets/i18n.js +263 -23
- 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/INDEX.md +87 -0
- package/templates/planning/brief.md +1 -1
- package/templates/planning/execution_strategy.md +31 -0
- package/templates/planning/lesson_candidates.md +18 -6
- package/templates/planning/module_session_prompt.md +1 -0
- package/templates/planning/optional/artifacts/INDEX.md +3 -3
- package/templates/planning/optional/references/INDEX.md +3 -3
- package/templates/planning/review.md +41 -0
- package/templates/planning/task_plan.md +5 -21
- package/templates/planning/visual_map.md +13 -9
- package/templates/planning/visual_map.simple.md +52 -0
- package/templates/reference/execution-workflow-standard.md +31 -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 +25 -19
- package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
- package/templates-zh-CN/planning/INDEX.md +87 -0
- package/templates-zh-CN/planning/brief.md +1 -1
- 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/module_session_prompt.md +1 -0
- package/templates-zh-CN/planning/review.md +41 -1
- package/templates-zh-CN/planning/task_plan.md +4 -44
- package/templates-zh-CN/planning/visual_map.md +14 -7
- package/templates-zh-CN/planning/visual_map.simple.md +48 -0
- 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 +33 -7
- 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,13 +1,10 @@
|
|
|
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,
|
|
10
|
-
longRunningTaskContractFile,
|
|
11
8
|
allowedTaskStates,
|
|
12
9
|
allowedTaskBudgets,
|
|
13
10
|
allowedPhaseStates,
|
|
@@ -17,73 +14,56 @@ import {
|
|
|
17
14
|
toPosix,
|
|
18
15
|
readFileSafe,
|
|
19
16
|
readBundledTemplate,
|
|
20
|
-
localizedTemplateSource,
|
|
21
17
|
todayDate,
|
|
22
18
|
localDate,
|
|
23
19
|
datePrefix,
|
|
24
|
-
nowTimestamp,
|
|
25
20
|
normalizeTaskId,
|
|
26
21
|
renderTaskTemplate,
|
|
27
22
|
} from "./core-shared.mjs";
|
|
28
|
-
import { verifyMigrationSession } from "./migration-planner.mjs";
|
|
29
23
|
import { readCapabilityRegistry } from "./capability-registry.mjs";
|
|
24
|
+
import { readPresetPackage } from "./preset-registry.mjs";
|
|
25
|
+
import {
|
|
26
|
+
assertPresetWriteScope,
|
|
27
|
+
buildPresetContext,
|
|
28
|
+
evaluateTemplateValues,
|
|
29
|
+
resolvePresetInputs,
|
|
30
|
+
renderPresetResourceIndex,
|
|
31
|
+
renderPresetTaskTemplate,
|
|
32
|
+
} from "./preset-engine.mjs";
|
|
30
33
|
import {
|
|
31
34
|
collectTasks,
|
|
32
|
-
collectReviewRisks,
|
|
33
|
-
isBlockingReviewRisk,
|
|
34
35
|
listTaskPlanPaths,
|
|
35
|
-
parsePhases,
|
|
36
36
|
parseTaskBudget,
|
|
37
|
-
parseLessonCandidateStatus,
|
|
38
|
-
isLessonCandidateDecisionComplete,
|
|
39
|
-
parseReviewConfirmation,
|
|
40
|
-
readVisualMapContractFile,
|
|
41
37
|
taskIdForDirectory,
|
|
42
38
|
} from "./task-scanner.mjs";
|
|
39
|
+
import { getColumn, firstColumn, updateMarkdownTableRow } from "./markdown-utils.mjs";
|
|
40
|
+
import { validateLifecycleTransition, validateReviewEntryGate } from "./task-lifecycle/review-gates.mjs";
|
|
41
|
+
import { advanceLifecyclePhase, autoRecordNoLessonCandidateDecision } from "./task-lifecycle/phase-sync.mjs";
|
|
42
|
+
import { confirmTaskReview as confirmTaskReviewWithContext } from "./task-lifecycle/review-confirm.mjs";
|
|
43
|
+
import { appendProgressLog } from "./task-lifecycle/text-utils.mjs";
|
|
44
|
+
import { buildScaffoldProvenance } from "./task-lifecycle/scaffold-provenance.mjs";
|
|
45
|
+
import { buildCreationTaskAudit } from "./task-audit-metadata.mjs";
|
|
43
46
|
import {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
["brief.md", "templates/planning/brief.md"],
|
|
65
|
-
["task_plan.md", "templates/planning/task_plan.md"],
|
|
66
|
-
[visualMapFile, "templates/planning/visual_map.md"],
|
|
67
|
-
["progress.md", "templates/planning/progress.md"],
|
|
68
|
-
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function optionalTaskTemplateFiles({ locale = "en-US" } = {}) {
|
|
72
|
-
return [
|
|
73
|
-
["references/INDEX.md", "templates/planning/optional/references/INDEX.md"],
|
|
74
|
-
["artifacts/INDEX.md", "templates/planning/optional/artifacts/INDEX.md"],
|
|
75
|
-
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function moduleTemplateFiles({ locale = "en-US" } = {}) {
|
|
79
|
-
return [
|
|
80
|
-
["brief.md", "templates/planning/module_brief.md"],
|
|
81
|
-
["module_plan.md", "templates/planning/module_plan.md"],
|
|
82
|
-
["execution_strategy.md", "templates/planning/execution_strategy.md"],
|
|
83
|
-
[visualMapFile, "templates/planning/visual_map.md"],
|
|
84
|
-
["session_prompt.md", "templates/planning/module_session_prompt.md"],
|
|
85
|
-
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
|
|
86
|
-
}
|
|
47
|
+
renderAgentReviewSubmission,
|
|
48
|
+
replaceAgentReviewSubmission,
|
|
49
|
+
} from "./task-lifecycle/review-submission.mjs";
|
|
50
|
+
import {
|
|
51
|
+
appendLongRunningContractFile,
|
|
52
|
+
moduleTemplateFiles,
|
|
53
|
+
taskFilesForBudget,
|
|
54
|
+
} from "./task-lifecycle/template-files.mjs";
|
|
55
|
+
import {
|
|
56
|
+
planCreateTaskChanges,
|
|
57
|
+
refreshPresetCommandAudit,
|
|
58
|
+
} from "./task-lifecycle/create-task-helpers.mjs";
|
|
59
|
+
import {
|
|
60
|
+
beginGovernanceSync,
|
|
61
|
+
commitGovernanceSync,
|
|
62
|
+
governanceRelativePaths,
|
|
63
|
+
releaseGovernanceSync,
|
|
64
|
+
syncModuleStepGovernance,
|
|
65
|
+
syncTaskGovernance,
|
|
66
|
+
} from "./governance-sync.mjs";
|
|
87
67
|
|
|
88
68
|
function taskRoot(target, taskId, { moduleKey = "" } = {}) {
|
|
89
69
|
const normalizedTaskId = normalizeTaskId(taskId);
|
|
@@ -91,7 +71,7 @@ function taskRoot(target, taskId, { moduleKey = "" } = {}) {
|
|
|
91
71
|
return path.join(target.docsRoot, "09-PLANNING/TASKS", normalizedTaskId);
|
|
92
72
|
}
|
|
93
73
|
|
|
94
|
-
function resolveTaskDirectory(target, taskRef) {
|
|
74
|
+
export function resolveTaskDirectory(target, taskRef) {
|
|
95
75
|
const raw = String(taskRef || "").replace(/^docs\/09-PLANNING\//, "").replace(/^\/+/, "");
|
|
96
76
|
if (!raw) throw new Error("Missing task id");
|
|
97
77
|
const direct = raw.startsWith("TASKS/") || raw.startsWith("MODULES/") ? path.join(target.docsRoot, "09-PLANNING", raw) : "";
|
|
@@ -153,76 +133,10 @@ function normalizeTaskBudgetInput(budget) {
|
|
|
153
133
|
throw new Error(`Invalid task budget: ${budget}. Expected one of: simple, standard, complex`);
|
|
154
134
|
}
|
|
155
135
|
|
|
156
|
-
function normalizeTaskPresetInput(preset) {
|
|
136
|
+
function normalizeTaskPresetInput(preset, { targetInput = "" } = {}) {
|
|
157
137
|
const normalized = String(preset || "none").trim().toLowerCase().replaceAll("_", "-");
|
|
158
138
|
if (!normalized || normalized === "none") return "none";
|
|
159
|
-
|
|
160
|
-
throw new Error(`Invalid task preset: ${preset}. Expected one of: legacy-migration`);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function taskFilesForBudget({ budget, locale }) {
|
|
164
|
-
if (budget === "simple") return simpleTaskTemplateFiles({ locale });
|
|
165
|
-
if (budget === "complex") return [...taskTemplateFiles({ locale }), ...optionalTaskTemplateFiles({ locale })];
|
|
166
|
-
return taskTemplateFiles({ locale });
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function appendLongRunningContractFile(files, { locale, longRunning }) {
|
|
170
|
-
if (!longRunning) return files;
|
|
171
|
-
return [...files, [longRunningTaskContractFile, localizedTemplateSource("templates/planning/long-running-task-contract.md", locale)]];
|
|
172
|
-
}
|
|
173
|
-
|
|
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
|
-
}
|
|
139
|
+
return readPresetPackage(normalized, { targetInput }).id;
|
|
226
140
|
}
|
|
227
141
|
|
|
228
142
|
function updateProgressState(content, state, locale) {
|
|
@@ -236,22 +150,6 @@ function updateProgressState(content, state, locale) {
|
|
|
236
150
|
return `${content.trimEnd()}\n\n## Status\n\n${label}\n`;
|
|
237
151
|
}
|
|
238
152
|
|
|
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
153
|
function ensureDatePrefix(slug) {
|
|
256
154
|
if (datePrefix.test(slug)) return slug;
|
|
257
155
|
return `${localDate()}-${slug}`;
|
|
@@ -262,42 +160,119 @@ function bareSlug(datedId) {
|
|
|
262
160
|
return datedId;
|
|
263
161
|
}
|
|
264
162
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
163
|
+
function automaticTaskSlug(seed) {
|
|
164
|
+
return normalizeTaskId(seed || "task").slice(0, 48).replace(/-+$/g, "") || "task";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function randomTaskSuffix() {
|
|
168
|
+
return crypto.randomBytes(4).toString("hex");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resolveTaskIdentity({ target, taskId, title, presetPackage, moduleKey, automaticTaskId }) {
|
|
172
|
+
if (!automaticTaskId) {
|
|
173
|
+
const rawNormalized = normalizeTaskId(taskId || (presetPackage?.task?.defaultTaskId || ""));
|
|
174
|
+
const normalizedTaskId = ensureDatePrefix(rawNormalized);
|
|
175
|
+
if (!normalizedTaskId) throw new Error("Missing task id");
|
|
176
|
+
return { normalizedTaskId, semanticSlug: bareSlug(normalizedTaskId) };
|
|
271
177
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
178
|
+
|
|
179
|
+
const semanticSlug = automaticTaskSlug(title || presetPackage?.task?.defaultTaskId || "task");
|
|
180
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
181
|
+
const normalizedTaskId = `${localDate()}-${semanticSlug}-${randomTaskSuffix()}`;
|
|
182
|
+
if (!fs.existsSync(taskRoot(target, normalizedTaskId, { moduleKey }))) return { normalizedTaskId, semanticSlug };
|
|
183
|
+
}
|
|
184
|
+
throw new Error(`Unable to allocate automatic task id for: ${semanticSlug}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function createTask(targetInput, taskId, { title = "", locale = "en-US", dryRun = false, moduleKey = "", budget = "standard", longRunning = false, preset = "", fromSession = "", presetArgs = [], automaticTaskId = false } = {}) {
|
|
188
|
+
const requestedPreset = preset || (moduleKey ? "module" : "");
|
|
189
|
+
const normalizedPreset = normalizeTaskPresetInput(requestedPreset, { targetInput });
|
|
190
|
+
const presetPackage = normalizedPreset === "none" ? null : readPresetPackage(normalizedPreset, { targetInput });
|
|
191
|
+
const presetInputs = presetPackage ? resolvePresetInputs(presetPackage, { cliArgs: presetArgs, fromSession, targetInput }) : null;
|
|
192
|
+
const target = normalizeTarget(presetInputs?.targetInput || targetInput);
|
|
193
|
+
if (presetInputs?.targetInput && targetInput && targetInput !== "." && path.resolve(targetInput) !== path.resolve(presetInputs.targetInput)) {
|
|
194
|
+
throw new Error(`--from-session target mismatch: session target is ${presetInputs.targetInput}`);
|
|
195
|
+
}
|
|
196
|
+
const normalizedBudget = normalizeTaskBudgetInput(budget);
|
|
197
|
+
if (presetPackage && !presetPackage.compatibleBudgets.includes(normalizedBudget)) throw new Error(`${normalizedPreset} preset requires --budget ${presetPackage.compatibleBudgets.join("|")}`);
|
|
198
|
+
if (presetPackage?.task?.projectLevelOnly === true && moduleKey) throw new Error(`${normalizedPreset} preset is project-level and cannot be combined with --module`);
|
|
199
|
+
if (presetPackage?.task?.requiresFromSession === true && !fromSession) throw new Error(`${normalizedPreset} preset requires --from-session`);
|
|
280
200
|
const normalizedModuleKey = moduleKey ? normalizeTaskId(moduleKey) : "";
|
|
201
|
+
const identity = resolveTaskIdentity({ target, taskId, title, presetPackage, moduleKey: normalizedModuleKey, automaticTaskId });
|
|
202
|
+
const normalizedTaskId = identity.normalizedTaskId;
|
|
203
|
+
const semanticSlug = identity.semanticSlug;
|
|
281
204
|
const normalizedLocale = normalizeLocale(locale || readCapabilityRegistry(target).locale);
|
|
282
|
-
const normalizedBudget = normalizeTaskBudgetInput(budget);
|
|
283
205
|
const taskTitle = title || (normalizedPreset === "legacy-migration" ? "Harness v1 legacy migration" : semanticSlug);
|
|
284
206
|
const directory = taskRoot(target, normalizedTaskId, { moduleKey: normalizedModuleKey });
|
|
285
207
|
if (fs.existsSync(directory)) throw new Error(`Task already exists: ${normalizedTaskId}`);
|
|
286
|
-
const
|
|
287
|
-
|
|
208
|
+
const scaffoldProvenance = buildScaffoldProvenance({
|
|
209
|
+
taskId,
|
|
210
|
+
normalizedTaskId,
|
|
211
|
+
title,
|
|
212
|
+
locale: normalizedLocale,
|
|
213
|
+
budget: normalizedBudget,
|
|
214
|
+
longRunning,
|
|
215
|
+
moduleKey: normalizedModuleKey,
|
|
216
|
+
preset: normalizedPreset,
|
|
217
|
+
fromSession,
|
|
218
|
+
targetInput: presetInputs?.targetInput || targetInput,
|
|
219
|
+
automaticTaskId,
|
|
220
|
+
});
|
|
221
|
+
const baseTaskAudit = buildCreationTaskAudit(scaffoldProvenance, { projectRoot: target.projectRoot });
|
|
222
|
+
const evaluatedPresetValues = presetPackage ? evaluateTemplateValues(presetPackage, presetInputs.inputs, { taskId: normalizedTaskId, taskTitle, moduleKey: normalizedModuleKey }) : null;
|
|
223
|
+
const presetContext = presetPackage
|
|
224
|
+
? buildPresetContext({ ...presetPackage, task: { ...(presetPackage.task || {}), kind: presetPackage.task?.kind || "general" } }, {
|
|
225
|
+
target,
|
|
226
|
+
taskDir: directory,
|
|
227
|
+
taskId: normalizedTaskId,
|
|
228
|
+
taskTitle,
|
|
229
|
+
resolvedInputs: presetInputs.inputs,
|
|
230
|
+
evaluatedValues: evaluatedPresetValues,
|
|
231
|
+
})
|
|
288
232
|
: null;
|
|
233
|
+
const task = {
|
|
234
|
+
id: taskIdForDirectory(target, directory),
|
|
235
|
+
shortId: normalizedTaskId,
|
|
236
|
+
title: taskTitle,
|
|
237
|
+
module: normalizedModuleKey || null,
|
|
238
|
+
path: `TARGET:${toPosix(path.relative(target.projectRoot, directory))}`,
|
|
239
|
+
locale: normalizedLocale,
|
|
240
|
+
budget: normalizedBudget,
|
|
241
|
+
kind: presetContext?.kind || "general",
|
|
242
|
+
preset: normalizedPreset,
|
|
243
|
+
presetVersion: presetContext?.presetVersion || "",
|
|
244
|
+
presetAudit: presetContext?.audit || null,
|
|
245
|
+
migrationTargetLevel: presetContext?.migrationTargetLevel || "",
|
|
246
|
+
migrationAchievedLevel: presetContext?.migrationAchievedLevel || "",
|
|
247
|
+
evidenceBundle: presetContext?.evidenceBundle || "",
|
|
248
|
+
longRunning,
|
|
249
|
+
};
|
|
250
|
+
const plannedChanges = planCreateTaskChanges({
|
|
251
|
+
target,
|
|
252
|
+
directory,
|
|
253
|
+
normalizedModuleKey,
|
|
254
|
+
normalizedLocale,
|
|
255
|
+
normalizedBudget,
|
|
256
|
+
longRunning,
|
|
257
|
+
presetContext,
|
|
258
|
+
task,
|
|
259
|
+
});
|
|
260
|
+
const plannedGovernance = syncTaskGovernance(target, task, { event: "new-task", state: "planned", message: "task registered by CLI", dryRun: true });
|
|
261
|
+
const plannedWriteScopes = governanceRelativePaths([...plannedChanges, ...plannedGovernance.changes]);
|
|
289
262
|
const changes = [];
|
|
263
|
+
const governanceContext = beginGovernanceSync(target, { operation: `new-task ${normalizedTaskId}`, dryRun, allowDirtyWorktree: true, allowedRelativePaths: plannedWriteScopes });
|
|
264
|
+
try {
|
|
290
265
|
if (normalizedModuleKey) {
|
|
291
266
|
const moduleDirectory = path.dirname(directory);
|
|
292
267
|
for (const [destination, source] of moduleTemplateFiles({ locale: normalizedLocale })) {
|
|
293
268
|
const destinationPath = path.join(moduleDirectory, destination);
|
|
294
269
|
if (fs.existsSync(destinationPath)) continue;
|
|
295
|
-
const sourcePath = path.join(repoRoot, source);
|
|
296
270
|
changes.push({
|
|
297
271
|
destination: toPosix(path.relative(target.projectRoot, destinationPath)),
|
|
298
272
|
source,
|
|
299
273
|
action: dryRun ? "would-create" : "create",
|
|
300
274
|
});
|
|
275
|
+
if (presetPackage) assertPresetWriteScope(presetPackage, toPosix(path.relative(target.projectRoot, destinationPath)));
|
|
301
276
|
if (dryRun) continue;
|
|
302
277
|
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
303
278
|
fs.writeFileSync(
|
|
@@ -307,6 +282,13 @@ export function createTask(targetInput, taskId, { title = "", locale = "en-US",
|
|
|
307
282
|
title: normalizedModuleKey,
|
|
308
283
|
locale: normalizedLocale,
|
|
309
284
|
budget: normalizedBudget,
|
|
285
|
+
moduleKey: normalizedModuleKey,
|
|
286
|
+
preset: normalizedPreset,
|
|
287
|
+
presetVersion: presetContext?.presetVersion || "",
|
|
288
|
+
evidenceBundle: presetContext?.evidenceBundle || "",
|
|
289
|
+
longRunning,
|
|
290
|
+
scaffoldProvenance,
|
|
291
|
+
taskAudit: buildCreationTaskAudit({ ...scaffoldProvenance, templateSource: source }, { projectRoot: target.projectRoot }),
|
|
310
292
|
}),
|
|
311
293
|
);
|
|
312
294
|
}
|
|
@@ -317,12 +299,12 @@ export function createTask(targetInput, taskId, { title = "", locale = "en-US",
|
|
|
317
299
|
});
|
|
318
300
|
for (const [destination, source] of files) {
|
|
319
301
|
const destinationPath = path.join(directory, destination);
|
|
320
|
-
const sourcePath = path.join(repoRoot, source);
|
|
321
302
|
changes.push({
|
|
322
303
|
destination: toPosix(path.relative(target.projectRoot, destinationPath)),
|
|
323
304
|
source,
|
|
324
305
|
action: dryRun ? "would-create" : "create",
|
|
325
306
|
});
|
|
307
|
+
if (presetPackage) assertPresetWriteScope(presetPackage, toPosix(path.relative(target.projectRoot, destinationPath)));
|
|
326
308
|
if (dryRun) continue;
|
|
327
309
|
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
328
310
|
fs.writeFileSync(
|
|
@@ -332,6 +314,18 @@ export function createTask(targetInput, taskId, { title = "", locale = "en-US",
|
|
|
332
314
|
title: taskTitle,
|
|
333
315
|
locale: normalizedLocale,
|
|
334
316
|
budget: normalizedBudget,
|
|
317
|
+
moduleKey: normalizedModuleKey,
|
|
318
|
+
preset: normalizedPreset,
|
|
319
|
+
presetVersion: presetContext?.presetVersion || "",
|
|
320
|
+
evidenceBundle: presetContext?.evidenceBundle || "",
|
|
321
|
+
longRunning,
|
|
322
|
+
scaffoldProvenance: {
|
|
323
|
+
...scaffoldProvenance,
|
|
324
|
+
templateSource: source,
|
|
325
|
+
},
|
|
326
|
+
taskAudit: destination === "INDEX.md"
|
|
327
|
+
? buildCreationTaskAudit({ ...scaffoldProvenance, templateSource: source }, { projectRoot: target.projectRoot })
|
|
328
|
+
: baseTaskAudit,
|
|
335
329
|
}), presetContext),
|
|
336
330
|
);
|
|
337
331
|
}
|
|
@@ -343,211 +337,61 @@ export function createTask(targetInput, taskId, { title = "", locale = "en-US",
|
|
|
343
337
|
source: evidence.source,
|
|
344
338
|
action: dryRun ? "would-create" : "create",
|
|
345
339
|
});
|
|
340
|
+
assertPresetWriteScope(presetPackage, toPosix(evidence.relativePath));
|
|
346
341
|
if (dryRun) continue;
|
|
347
342
|
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
348
343
|
fs.writeFileSync(destinationPath, evidence.content);
|
|
349
344
|
}
|
|
345
|
+
for (const resource of presetContext.resourceFiles || []) {
|
|
346
|
+
const destinationPath = path.join(target.projectRoot, resource.relativePath);
|
|
347
|
+
changes.push({
|
|
348
|
+
destination: toPosix(resource.relativePath),
|
|
349
|
+
source: resource.source,
|
|
350
|
+
action: dryRun ? "would-create" : "create",
|
|
351
|
+
});
|
|
352
|
+
assertPresetWriteScope(presetPackage, toPosix(resource.relativePath));
|
|
353
|
+
if (dryRun) continue;
|
|
354
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
355
|
+
fs.writeFileSync(destinationPath, resource.content);
|
|
356
|
+
}
|
|
357
|
+
for (const [kind, rows] of Object.entries(presetContext.resourceIndexRows || {})) {
|
|
358
|
+
if (!rows.length) continue;
|
|
359
|
+
const destination = kind === "references" ? "references/INDEX.md" : "artifacts/INDEX.md";
|
|
360
|
+
const destinationPath = path.join(directory, destination);
|
|
361
|
+
const relativePath = toPosix(path.relative(target.projectRoot, destinationPath));
|
|
362
|
+
changes.push({
|
|
363
|
+
destination: relativePath,
|
|
364
|
+
source: `preset-${kind}-index`,
|
|
365
|
+
action: dryRun ? "would-update" : "update",
|
|
366
|
+
});
|
|
367
|
+
assertPresetWriteScope(presetPackage, relativePath);
|
|
368
|
+
if (dryRun) continue;
|
|
369
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
370
|
+
const existing = fs.existsSync(destinationPath) ? fs.readFileSync(destinationPath, "utf8") : "";
|
|
371
|
+
fs.writeFileSync(destinationPath, renderPresetResourceIndex(existing, kind, rows));
|
|
372
|
+
}
|
|
350
373
|
}
|
|
374
|
+
const governance = syncTaskGovernance(target, task, { event: "new-task", state: "planned", message: "task registered by CLI", dryRun });
|
|
375
|
+
changes.push(...governance.changes);
|
|
376
|
+
const commandWriteScopes = governanceRelativePaths(changes);
|
|
377
|
+
if (presetContext) {
|
|
378
|
+
refreshPresetCommandAudit(target, presetContext, { commandWriteScopes, dryRun });
|
|
379
|
+
task.presetAudit = presetContext.audit;
|
|
380
|
+
}
|
|
381
|
+
const commit = commitGovernanceSync(governanceContext, commandWriteScopes, {
|
|
382
|
+
message: `chore(harness): register task ${task.id}`,
|
|
383
|
+
});
|
|
351
384
|
return {
|
|
352
385
|
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
|
-
},
|
|
386
|
+
task,
|
|
369
387
|
changes,
|
|
388
|
+
governance: { ...governance, commit },
|
|
370
389
|
};
|
|
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}`);
|
|
381
|
-
}
|
|
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
|
-
}
|
|
387
|
-
|
|
388
|
-
function legacyMigrationPresetContext({ target, taskDir, taskId, session }) {
|
|
389
|
-
const stamp = String(session.generatedAt || new Date().toISOString()).replace(/[^0-9A-Za-z-]+/g, "-").replace(/-+$/g, "");
|
|
390
|
-
const evidenceBundle = toPosix(path.relative(target.projectRoot, path.join(taskDir, "evidence", stamp || "session")));
|
|
391
|
-
const targetLevel = "migration-baseline";
|
|
392
|
-
const achievedLevel = session.strictDeferred ? "migration-deferred" : session.result === "complete" ? "migration-full-cutover" : "migration-baseline";
|
|
393
|
-
const verifyResult = verifyMigrationSession(session.sourcePath, { fullCutover: false });
|
|
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 }),
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function legacyMigrationEvidenceFiles({ target, session, evidenceBundle, verifyResult }) {
|
|
407
|
-
const files = [];
|
|
408
|
-
const addJson = (name, value, source = "session") => files.push({
|
|
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";
|
|
390
|
+
} finally {
|
|
391
|
+
releaseGovernanceSync(governanceContext);
|
|
447
392
|
}
|
|
448
393
|
}
|
|
449
394
|
|
|
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
395
|
export function updateTaskLifecycle(targetInput, taskId, { event = "task-log", state = "", message = "", evidence = "" } = {}) {
|
|
552
396
|
const target = normalizeTarget(targetInput);
|
|
553
397
|
const taskDir = resolveTaskDirectory(target, taskId);
|
|
@@ -556,111 +400,77 @@ export function updateTaskLifecycle(targetInput, taskId, { event = "task-log", s
|
|
|
556
400
|
const normalizedState = state ? String(state).toLowerCase().replaceAll("-", "_") : "";
|
|
557
401
|
if (normalizedState && !allowedTaskStates.has(normalizedState)) throw new Error(`Invalid task state: ${state}`);
|
|
558
402
|
const currentTask = findTaskByDirectory(target, taskDir);
|
|
403
|
+
const canonicalTaskId = taskIdForDirectory(target, taskDir);
|
|
559
404
|
const budget = parseTaskBudget(readFileSafe(path.join(taskDir, "task_plan.md")));
|
|
560
405
|
validateLifecycleTransition({
|
|
561
406
|
event,
|
|
562
407
|
currentState: currentTask?.state || "unknown",
|
|
563
408
|
budget,
|
|
564
409
|
reviewContent: readFileSafe(path.join(taskDir, "review.md")),
|
|
410
|
+
indexContent: readFileSafe(path.join(taskDir, "INDEX.md")),
|
|
411
|
+
reviewTaskKey: canonicalTaskId,
|
|
412
|
+
projectRoot: target.projectRoot,
|
|
413
|
+
taskDir,
|
|
565
414
|
});
|
|
566
415
|
if (event === "task-review") validateReviewEntryGate(taskDir, budget);
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
416
|
+
const governanceContext = beginGovernanceSync(target, { operation: `${event} ${canonicalTaskId}` });
|
|
417
|
+
try {
|
|
418
|
+
let content = readFileSafe(progressPath);
|
|
419
|
+
if (normalizedState) content = updateProgressState(content, normalizedState, registry.locale);
|
|
420
|
+
content = appendProgressLog(content, { event, message, evidence });
|
|
421
|
+
fs.writeFileSync(progressPath, content.endsWith("\n") ? content : `${content}\n`);
|
|
422
|
+
const allowedPaths = [toPosix(path.relative(target.projectRoot, progressPath))];
|
|
423
|
+
const advancedPhasePath = advanceLifecyclePhase(target, taskDir, event);
|
|
424
|
+
if (advancedPhasePath) allowedPaths.push(advancedPhasePath);
|
|
425
|
+
if (event === "task-review") {
|
|
426
|
+
const reviewPath = path.join(taskDir, "review.md");
|
|
427
|
+
const reviewContent = readFileSafe(reviewPath);
|
|
428
|
+
fs.writeFileSync(
|
|
429
|
+
reviewPath,
|
|
430
|
+
replaceAgentReviewSubmission(
|
|
431
|
+
reviewContent,
|
|
432
|
+
renderAgentReviewSubmission({
|
|
433
|
+
target,
|
|
434
|
+
taskDir,
|
|
435
|
+
canonicalTaskId,
|
|
436
|
+
message,
|
|
437
|
+
evidence,
|
|
438
|
+
}),
|
|
439
|
+
),
|
|
440
|
+
);
|
|
441
|
+
allowedPaths.push(toPosix(path.relative(target.projectRoot, reviewPath)));
|
|
442
|
+
const lessonDecisionPath = autoRecordNoLessonCandidateDecision(target, taskDir);
|
|
443
|
+
if (lessonDecisionPath) allowedPaths.push(lessonDecisionPath);
|
|
444
|
+
}
|
|
445
|
+
const task =
|
|
446
|
+
findTaskByDirectory(target, taskDir) ||
|
|
447
|
+
{
|
|
448
|
+
id: canonicalTaskId,
|
|
449
|
+
shortId: path.basename(taskDir),
|
|
450
|
+
title: canonicalTaskId,
|
|
451
|
+
path: `TARGET:${toPosix(path.relative(target.projectRoot, taskDir))}`,
|
|
452
|
+
state: normalizedState || currentTask?.state || "unknown",
|
|
453
|
+
};
|
|
454
|
+
const governanceState = normalizedState || task.state || currentTask?.state || "planned";
|
|
455
|
+
const governance = syncTaskGovernance(target, task, { event, state: governanceState, message, dryRun: false });
|
|
456
|
+
const commit = commitGovernanceSync(governanceContext, [...allowedPaths, ...governanceRelativePaths(governance.changes)], {
|
|
457
|
+
message: `chore(harness): advance task ${canonicalTaskId} to ${governanceState}`,
|
|
458
|
+
});
|
|
459
|
+
return {
|
|
460
|
+
event,
|
|
461
|
+
task,
|
|
462
|
+
governance: { ...governance, commit },
|
|
463
|
+
};
|
|
464
|
+
} finally {
|
|
465
|
+
releaseGovernanceSync(governanceContext);
|
|
466
|
+
}
|
|
575
467
|
}
|
|
576
468
|
|
|
577
469
|
export function confirmTaskReview(targetInput, taskId, { reviewer = "Human Reviewer", message = "", confirmText = "", evidence = "" } = {}) {
|
|
578
470
|
const target = normalizeTarget(targetInput);
|
|
579
471
|
const taskDir = resolveTaskDirectory(target, taskId);
|
|
580
|
-
|
|
581
|
-
const canonicalTaskId = taskIdForDirectory(target, taskDir);
|
|
582
|
-
const shortId = path.basename(taskDir);
|
|
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
|
-
|
|
606
|
-
const timestamp = nowTimestamp();
|
|
607
|
-
const safeReviewer = markdownCell(reviewer || "Human Reviewer");
|
|
608
|
-
const safeMessage = markdownCell(message || "Human review confirmed");
|
|
609
|
-
const safeEvidence = markdownCell(evidence || `TARGET:docs/09-PLANNING/${canonicalTaskId}/review.md`);
|
|
610
|
-
const confirmationBlock = [
|
|
611
|
-
"## Human Review Confirmation",
|
|
612
|
-
"",
|
|
613
|
-
`Reviewer: ${safeReviewer}`,
|
|
614
|
-
"",
|
|
615
|
-
"| Confirmed At | Reviewer | Message | Evidence |",
|
|
616
|
-
"| --- | --- | --- | --- |",
|
|
617
|
-
`| ${timestamp} | ${safeReviewer} | ${safeMessage} | ${safeEvidence} |`,
|
|
618
|
-
"",
|
|
619
|
-
].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
|
-
}
|
|
472
|
+
return confirmTaskReviewWithContext({ target, taskDir, findTaskByDirectory }, { reviewer, message, confirmText, evidence });
|
|
647
473
|
}
|
|
648
|
-
|
|
649
|
-
function markdownCell(value) {
|
|
650
|
-
return String(value || "")
|
|
651
|
-
.replace(/\r?\n/g, " ")
|
|
652
|
-
.replaceAll("|", "\\|")
|
|
653
|
-
.trim();
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
function replaceReviewConfirmation(content, block) {
|
|
657
|
-
const trimmed = String(content || "").trimEnd();
|
|
658
|
-
if (/^##\s*(?:Human Review Confirmation|人工审查确认)\s*$/im.test(trimmed)) {
|
|
659
|
-
return trimmed.replace(/^##\s*(?:Human Review Confirmation|人工审查确认)\s*$[\s\S]*?(?=^##\s+|\s*$)/im, block.trimEnd());
|
|
660
|
-
}
|
|
661
|
-
return `${trimmed}\n\n${block}`;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
474
|
export function updateTaskPhase(targetInput, taskId, phaseId, { state = "", completion = "", evidenceStatus = "" } = {}) {
|
|
665
475
|
const target = normalizeTarget(targetInput);
|
|
666
476
|
const taskDir = resolveTaskDirectory(target, taskId);
|
|
@@ -692,9 +502,17 @@ export function updateTaskPhase(targetInput, taskId, phaseId, { state = "", comp
|
|
|
692
502
|
return next;
|
|
693
503
|
});
|
|
694
504
|
if (!phaseUpdate.matched) throw new Error(`Phase not found: ${phaseId}`);
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
505
|
+
const governanceContext = beginGovernanceSync(target, { operation: `task-phase ${taskId} ${phaseId}` });
|
|
506
|
+
try {
|
|
507
|
+
content = phaseUpdate.content;
|
|
508
|
+
fs.writeFileSync(visualMapPath, content);
|
|
509
|
+
const commit = commitGovernanceSync(governanceContext, [toPosix(path.relative(target.projectRoot, visualMapPath))], {
|
|
510
|
+
message: `chore(harness): update task phase ${taskId} ${phaseId}`,
|
|
511
|
+
});
|
|
512
|
+
return { event: "task-phase", task: findTaskByDirectory(target, taskDir), phaseId, governance: { commit } };
|
|
513
|
+
} finally {
|
|
514
|
+
releaseGovernanceSync(governanceContext);
|
|
515
|
+
}
|
|
698
516
|
}
|
|
699
517
|
|
|
700
518
|
export function updateModuleStep(targetInput, moduleKey, stepId, { state = "" } = {}) {
|
|
@@ -714,42 +532,85 @@ export function updateModuleStep(targetInput, moduleKey, stepId, { state = "" }
|
|
|
714
532
|
return next;
|
|
715
533
|
});
|
|
716
534
|
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
|
-
|
|
535
|
+
const governanceContext = beginGovernanceSync(target, { operation: `module-step ${normalizedModuleKey} ${stepId}` });
|
|
536
|
+
try {
|
|
537
|
+
content = stepUpdate.content;
|
|
538
|
+
fs.writeFileSync(modulePlanPath, content);
|
|
539
|
+
|
|
540
|
+
const registryPath = path.join(target.docsRoot, "09-PLANNING/Module-Registry.md");
|
|
541
|
+
if (fs.existsSync(registryPath)) {
|
|
542
|
+
let registry = readFileSafe(registryPath);
|
|
543
|
+
const registryUpdate = updateMarkdownTableRow(registry, /^(ID|模块 Key)$/i, (header, row) => {
|
|
544
|
+
const moduleIndex = firstColumn(header, ["Module", "模块", "模块 Key"]);
|
|
545
|
+
const taskPlanIndex = getColumn(header, "Task Plan");
|
|
546
|
+
const matchesModule = normalizeTaskId(row[moduleIndex] || "") === normalizedModuleKey;
|
|
547
|
+
const matchesPlan = taskPlanIndex >= 0 && String(row[taskPlanIndex] || "").includes(`/MODULES/${normalizedModuleKey}/`);
|
|
548
|
+
if (!matchesModule && !matchesPlan) return null;
|
|
549
|
+
const next = [...row];
|
|
550
|
+
const statusIndex = firstColumn(header, ["Status", "状态"]);
|
|
551
|
+
const updatedIndex = firstColumn(header, ["Updated", "更新时间"]);
|
|
552
|
+
const currentStepIndex = firstColumn(header, ["Current Step", "当前步骤"]);
|
|
553
|
+
const chineseRegistry = header.some((cell) => /模块 Key|模块名称|状态|更新时间/.test(cell));
|
|
554
|
+
if (statusIndex >= 0) {
|
|
555
|
+
next[statusIndex] = normalizedState === "done"
|
|
556
|
+
? chineseRegistry ? "completed" : "merged"
|
|
557
|
+
: normalizedState === "in-progress" ? chineseRegistry ? "in-progress" : "active" : normalizedState;
|
|
558
|
+
}
|
|
559
|
+
if (currentStepIndex >= 0) next[currentStepIndex] = stepId;
|
|
560
|
+
if (updatedIndex >= 0) next[updatedIndex] = todayDate();
|
|
561
|
+
return next;
|
|
562
|
+
});
|
|
563
|
+
registry = registryUpdate.content;
|
|
564
|
+
fs.writeFileSync(registryPath, registry);
|
|
565
|
+
}
|
|
566
|
+
const governance = syncModuleStepGovernance(target, { moduleKey: normalizedModuleKey, stepId, state: normalizedState });
|
|
567
|
+
const commit = commitGovernanceSync(
|
|
568
|
+
governanceContext,
|
|
569
|
+
[
|
|
570
|
+
toPosix(path.relative(target.projectRoot, modulePlanPath)),
|
|
571
|
+
toPosix(path.relative(target.projectRoot, registryPath)),
|
|
572
|
+
...governanceRelativePaths(governance.changes),
|
|
573
|
+
],
|
|
574
|
+
{ message: `chore(harness): update module ${normalizedModuleKey} step ${stepId}` },
|
|
575
|
+
);
|
|
576
|
+
return { event: "module-step", moduleKey: normalizedModuleKey, stepId, state: normalizedState, governance: { ...governance, commit } };
|
|
577
|
+
} finally {
|
|
578
|
+
releaseGovernanceSync(governanceContext);
|
|
745
579
|
}
|
|
746
|
-
return { event: "module-step", moduleKey: normalizedModuleKey, stepId, state: normalizedState };
|
|
747
580
|
}
|
|
748
581
|
|
|
749
|
-
export function listLifecycleTasks(targetInput, { state = "", moduleKey = "" } = {}) {
|
|
582
|
+
export function listLifecycleTasks(targetInput, { state = "", moduleKey = "", queue = "", preset = "", review = "", lesson = "", search = "", missingMaterials = false } = {}) {
|
|
750
583
|
const target = normalizeTarget(targetInput);
|
|
751
584
|
let tasks = collectTasks(target);
|
|
752
585
|
if (state) tasks = tasks.filter((task) => task.state === String(state).toLowerCase().replaceAll("-", "_"));
|
|
753
586
|
if (moduleKey) tasks = tasks.filter((task) => task.module === normalizeTaskId(moduleKey));
|
|
587
|
+
if (queue) {
|
|
588
|
+
const normalizedQueue = queryToken(queue);
|
|
589
|
+
tasks = tasks.filter((task) => (task.taskQueues || []).map(queryToken).includes(normalizedQueue));
|
|
590
|
+
}
|
|
591
|
+
if (preset) tasks = tasks.filter((task) => queryToken(task.taskPreset || "none") === queryToken(preset));
|
|
592
|
+
if (review) tasks = tasks.filter((task) => queryToken(task.reviewStatus || "") === queryToken(review));
|
|
593
|
+
if (lesson) {
|
|
594
|
+
const needle = queryToken(lesson);
|
|
595
|
+
tasks = tasks.filter((task) => [task.lessonCandidateStatus, task.lessonCandidateReviewDecision, task.lessonCandidatePromotionState].some((value) => queryToken(value) === needle));
|
|
596
|
+
}
|
|
597
|
+
if (missingMaterials) tasks = tasks.filter((task) => !task.materialsReady);
|
|
598
|
+
if (search) {
|
|
599
|
+
const needle = String(search).toLowerCase();
|
|
600
|
+
tasks = tasks.filter((task) => [
|
|
601
|
+
task.id,
|
|
602
|
+
task.taskKey,
|
|
603
|
+
task.shortId,
|
|
604
|
+
task.title,
|
|
605
|
+
task.currentPath,
|
|
606
|
+
task.taskPlanPath,
|
|
607
|
+
task.module,
|
|
608
|
+
task.inferredModule,
|
|
609
|
+
].some((value) => String(value || "").toLowerCase().includes(needle)));
|
|
610
|
+
}
|
|
754
611
|
return { tasks };
|
|
755
612
|
}
|
|
613
|
+
|
|
614
|
+
function queryToken(value) {
|
|
615
|
+
return String(value || "").trim().toLowerCase().replaceAll("_", "-");
|
|
616
|
+
}
|