coding-agent-harness 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.en-US.md +14 -0
- package/README.md +111 -86
- package/README.zh-CN.md +270 -0
- package/SKILL.md +116 -189
- package/docs-release/README.md +72 -5
- package/docs-release/architecture/overview.md +286 -28
- package/docs-release/architecture/overview.zh-CN.md +288 -0
- package/docs-release/assets/dashboard-overview-en.png +0 -0
- package/docs-release/assets/harness-architecture.svg +163 -0
- package/docs-release/assets/harness-workflow.svg +64 -0
- package/docs-release/guides/agent-installation.en-US.md +214 -0
- package/docs-release/guides/agent-installation.md +123 -26
- package/docs-release/guides/document-audience-and-surfaces.en-US.md +112 -0
- package/docs-release/guides/document-audience-and-surfaces.md +112 -0
- package/docs-release/guides/full-legacy-migration-subagent-strategy.md +334 -0
- package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +334 -0
- package/docs-release/guides/legacy-migration-agent-prompt.md +384 -0
- package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +361 -0
- package/docs-release/guides/migration-playbook.en-US.md +325 -0
- package/docs-release/guides/migration-playbook.md +329 -0
- package/docs-release/guides/parent-control-repository-pattern.en-US.md +252 -0
- package/docs-release/guides/parent-control-repository-pattern.md +252 -0
- package/docs-release/guides/repository-operating-models.en-US.md +196 -0
- package/docs-release/guides/repository-operating-models.md +196 -0
- package/docs-release/intl/README.md +15 -0
- package/docs-release/intl/de-DE.md +18 -0
- package/docs-release/intl/en-US.md +18 -0
- package/docs-release/intl/es-ES.md +18 -0
- package/docs-release/intl/fr-FR.md +18 -0
- package/docs-release/intl/ja-JP.md +18 -0
- package/docs-release/intl/ko-KR.md +18 -0
- package/docs-release/intl/zh-CN.md +18 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/brief.md +13 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/lesson_candidates.md +24 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/progress.md +1 -1
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/task_plan.md +4 -2
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/{visual_roadmap.md → visual_map.md} +9 -1
- package/package.json +3 -1
- package/references/agents-md-pattern.md +3 -3
- package/references/docs-directory-standard.md +47 -3
- package/references/external-source-intake-standard.md +75 -0
- package/references/harness-ledger.md +5 -3
- package/references/legacy-12-phase-bootstrap.md +41 -0
- package/references/lessons-governance.md +23 -6
- package/references/planning-loop.md +41 -3
- package/references/project-onboarding-audit.md +10 -0
- package/references/repo-governance-standard.md +2 -0
- package/references/testing-standard.md +50 -0
- package/references/walkthrough-closeout.md +6 -5
- package/scripts/check-harness.mjs +76 -35
- package/scripts/harness.mjs +303 -12
- package/scripts/lib/capability-registry.mjs +533 -0
- package/scripts/lib/check-profiles.mjs +510 -0
- package/scripts/lib/core-shared.mjs +186 -0
- package/scripts/lib/dashboard-data.mjs +389 -0
- package/scripts/lib/dashboard-workbench.mjs +217 -0
- package/scripts/lib/dashboard-writer.mjs +93 -2
- package/scripts/lib/harness-core.mjs +10 -1318
- package/scripts/lib/lesson-maintenance.mjs +145 -0
- package/scripts/lib/markdown-utils.mjs +158 -0
- package/scripts/lib/migration-planner.mjs +478 -0
- package/scripts/lib/migration-support.mjs +312 -0
- package/scripts/lib/task-lifecycle.mjs +755 -0
- package/scripts/lib/task-scanner.mjs +682 -0
- package/scripts/smoke-dashboard.mjs +22 -0
- package/scripts/test-harness.mjs +926 -14
- package/templates/AGENTS.md.template +41 -30
- package/templates/architecture/Architecture-SSoT.md +21 -0
- package/templates/architecture/README.md +49 -0
- package/templates/architecture/critical-flows.md +22 -0
- package/templates/architecture/local-repo-context.md +20 -0
- package/templates/architecture/service-catalog.md +17 -0
- package/templates/architecture/services/service-template.md +31 -0
- package/templates/architecture/system-map.md +22 -0
- package/templates/dashboard/assets/app-src/00-state.js +41 -0
- package/templates/dashboard/assets/app-src/10-router.js +76 -0
- package/templates/dashboard/assets/app-src/20-overview.js +235 -0
- package/templates/dashboard/assets/app-src/30-tasks.js +563 -0
- package/templates/dashboard/assets/app-src/40-modules.js +58 -0
- package/templates/dashboard/assets/app-src/45-review.js +128 -0
- package/templates/dashboard/assets/app-src/50-migration.js +169 -0
- package/templates/dashboard/assets/app-src/60-shared.js +61 -0
- package/templates/dashboard/assets/app-src/90-bindings.js +382 -0
- package/templates/dashboard/assets/app.css +2575 -310
- package/templates/dashboard/assets/app.js +1498 -307
- package/templates/dashboard/assets/app.manifest.json +11 -0
- package/templates/dashboard/assets/i18n.js +429 -44
- package/templates/dashboard/assets/mermaid-renderer.js +58 -8
- package/templates/development/README.md +52 -0
- package/templates/development/codebase-map.md +11 -0
- package/templates/development/cross-repo-debugging.md +18 -0
- package/templates/development/external-context/service-template.md +33 -0
- package/templates/development/external-source-packs/README.md +24 -0
- package/templates/development/external-source-packs/digest-template.md +28 -0
- package/templates/development/local-setup.md +16 -0
- package/templates/development/stubs-and-mocks.md +11 -0
- package/templates/integrations/README.md +40 -0
- package/templates/integrations/api-contract.md +42 -0
- package/templates/integrations/event-contract.md +46 -0
- package/templates/integrations/third-party/vendor-template.md +42 -0
- package/templates/integrations/webhook-contract.md +41 -0
- package/templates/planning/brief.md +32 -0
- package/templates/planning/lesson_candidates.md +58 -0
- package/templates/planning/long-running-task-contract.md +7 -0
- package/templates/planning/module_brief.md +25 -0
- package/templates/planning/module_session_prompt.md +6 -0
- package/templates/planning/task_plan.md +7 -5
- package/templates/planning/{visual_roadmap.md → visual_map.md} +24 -2
- package/templates/reference/docs-library-standard.md +31 -0
- package/templates/reference/execution-workflow-standard.md +4 -2
- package/templates/reference/external-source-intake-standard.md +82 -0
- package/templates/reference/harness-ledger-standard.md +1 -0
- package/templates/reference/repo-governance-standard.md +6 -4
- package/templates/reference/walkthrough-standard.md +2 -1
- package/templates/walkthrough/walkthrough-template.md +2 -2
- package/templates-zh-CN/AGENTS.md.template +69 -70
- package/templates-zh-CN/architecture/Architecture-SSoT.md +21 -0
- package/templates-zh-CN/architecture/README.md +51 -0
- package/templates-zh-CN/architecture/critical-flows.md +24 -0
- package/templates-zh-CN/architecture/local-repo-context.md +20 -0
- package/templates-zh-CN/architecture/service-catalog.md +17 -0
- package/templates-zh-CN/architecture/services/service-template.md +31 -0
- package/templates-zh-CN/architecture/system-map.md +22 -0
- package/templates-zh-CN/development/README.md +54 -0
- package/templates-zh-CN/development/codebase-map.md +11 -0
- package/templates-zh-CN/development/cross-repo-debugging.md +18 -0
- package/templates-zh-CN/development/external-context/service-template.md +33 -0
- package/templates-zh-CN/development/external-source-packs/README.md +24 -0
- package/templates-zh-CN/development/external-source-packs/digest-template.md +28 -0
- package/templates-zh-CN/development/local-setup.md +16 -0
- package/templates-zh-CN/development/stubs-and-mocks.md +11 -0
- package/templates-zh-CN/integrations/README.md +42 -0
- package/templates-zh-CN/integrations/api-contract.md +42 -0
- package/templates-zh-CN/integrations/event-contract.md +46 -0
- package/templates-zh-CN/integrations/third-party/vendor-template.md +42 -0
- package/templates-zh-CN/integrations/webhook-contract.md +41 -0
- package/templates-zh-CN/planning/brief.md +32 -0
- package/templates-zh-CN/planning/lesson_candidates.md +58 -0
- package/templates-zh-CN/planning/long-running-task-contract.md +1 -1
- package/templates-zh-CN/planning/module_brief.md +25 -0
- package/templates-zh-CN/planning/module_plan.md +2 -2
- package/templates-zh-CN/planning/module_session_prompt.md +4 -3
- package/templates-zh-CN/planning/task_plan.md +10 -4
- package/templates-zh-CN/planning/{visual_roadmap.md → visual_map.md} +21 -2
- package/templates-zh-CN/reference/docs-library-standard.md +35 -0
- package/templates-zh-CN/reference/execution-workflow-standard.md +9 -2
- package/templates-zh-CN/reference/external-source-intake-standard.md +82 -0
- package/templates-zh-CN/reference/harness-ledger-standard.md +5 -2
- package/templates-zh-CN/reference/repo-governance-standard.md +2 -0
- package/templates-zh-CN/reference/walkthrough-standard.md +4 -4
- package/templates-zh-CN/walkthrough/Closeout-SSoT.md +2 -2
- package/templates-zh-CN/walkthrough/walkthrough-template.md +2 -2
- package/templates-zh-CN/dashboard/assets/app.css +0 -399
- package/templates-zh-CN/dashboard/assets/app.js +0 -435
- package/templates-zh-CN/dashboard/assets/i18n.js +0 -47
- package/templates-zh-CN/dashboard/assets/markdown-reader.js +0 -116
- package/templates-zh-CN/dashboard/assets/mermaid-renderer.js +0 -59
- package/templates-zh-CN/dashboard/index.html +0 -18
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import {
|
|
6
|
+
normalizeTarget,
|
|
7
|
+
normalizeLocale,
|
|
8
|
+
readFileSafe,
|
|
9
|
+
existsInDocs,
|
|
10
|
+
walkFiles,
|
|
11
|
+
toPosix,
|
|
12
|
+
sanitizeText,
|
|
13
|
+
slug,
|
|
14
|
+
visualMapFile,
|
|
15
|
+
legacyVisualRoadmapFile,
|
|
16
|
+
inferProjectLocale,
|
|
17
|
+
} from "./core-shared.mjs";
|
|
18
|
+
import {
|
|
19
|
+
readCapabilityRegistry,
|
|
20
|
+
detectCapabilities,
|
|
21
|
+
addCapability,
|
|
22
|
+
} from "./capability-registry.mjs";
|
|
23
|
+
import { buildStatus } from "./check-profiles.mjs";
|
|
24
|
+
import { collectAdoption, categorizeWarning, splitWarningMessage } from "./dashboard-data.mjs";
|
|
25
|
+
import {
|
|
26
|
+
listTaskPlanPaths,
|
|
27
|
+
isActiveTaskState,
|
|
28
|
+
requiresCanonicalVisualMap,
|
|
29
|
+
taskCutoverCounters,
|
|
30
|
+
} from "./task-scanner.mjs";
|
|
31
|
+
import { writeDashboardFolder } from "./dashboard-data.mjs";
|
|
32
|
+
import {
|
|
33
|
+
migrationSampleFiles,
|
|
34
|
+
probeTargetLocale,
|
|
35
|
+
inspectGitStatus,
|
|
36
|
+
ensureSessionDir,
|
|
37
|
+
statusCheckSummary,
|
|
38
|
+
strictDeferredFromStatus,
|
|
39
|
+
writeMigrationReport,
|
|
40
|
+
validateFullCutoverSession,
|
|
41
|
+
recommendedMigrationCapabilities,
|
|
42
|
+
migrationPhases,
|
|
43
|
+
} from "./migration-support.mjs";
|
|
44
|
+
|
|
45
|
+
export function buildMigrationPlan(targetInput, { limit = 20 } = {}) {
|
|
46
|
+
const target = normalizeTarget(targetInput);
|
|
47
|
+
const status = buildStatus(targetInput, { strict: false, strictLegacy: false });
|
|
48
|
+
const registry = readCapabilityRegistry(target);
|
|
49
|
+
const locale = registry.raw ? registry.locale : inferProjectLocale(target, registry.locale);
|
|
50
|
+
const adoption = collectAdoption(status);
|
|
51
|
+
const warnings = adoption.warnings.map((warning) => warning.detail).filter(Boolean);
|
|
52
|
+
const taskActionsByTask = new Map();
|
|
53
|
+
const reviewActionsByPath = new Map();
|
|
54
|
+
const legacyActions = [];
|
|
55
|
+
const legacyResiduals = [];
|
|
56
|
+
const warningGroups = new Map();
|
|
57
|
+
const tasksByShortId = new Map(status.tasks.map((task) => [task.shortId, task]));
|
|
58
|
+
|
|
59
|
+
function addTaskAction(taskId, actionPath, fileName, actionText) {
|
|
60
|
+
const existing = taskActionsByTask.get(taskId) || {
|
|
61
|
+
taskId,
|
|
62
|
+
path: actionPath,
|
|
63
|
+
files: new Set(),
|
|
64
|
+
action:
|
|
65
|
+
actionText ||
|
|
66
|
+
"Rewrite this task into the v1 task contract by adapting the localized task template and preserving evidence links.",
|
|
67
|
+
};
|
|
68
|
+
existing.files.add(fileName);
|
|
69
|
+
taskActionsByTask.set(taskId, existing);
|
|
70
|
+
return existing;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const warning of warnings) {
|
|
74
|
+
const category = categorizeWarning(warning);
|
|
75
|
+
const group = warningGroups.get(category) || { category, count: 0, examples: [] };
|
|
76
|
+
group.count += 1;
|
|
77
|
+
if (group.examples.length < 3) group.examples.push(sanitizeText(warning));
|
|
78
|
+
warningGroups.set(category, group);
|
|
79
|
+
|
|
80
|
+
const taskContract = warning.match(/(?:adoption-needed:\s*)?(docs\/09-PLANNING\/TASKS\/([^/\s]+))\s+missing\s+(execution_strategy\.md|visual_map\.md|visual_roadmap\.md)/i);
|
|
81
|
+
if (taskContract) {
|
|
82
|
+
const key = taskContract[2];
|
|
83
|
+
const task = tasksByShortId.get(key);
|
|
84
|
+
const actionFile = taskContract[3] === legacyVisualRoadmapFile ? visualMapFile : taskContract[3];
|
|
85
|
+
const visualGap = actionFile === visualMapFile;
|
|
86
|
+
if (!task || (!isActiveTaskState(task.state) && !(visualGap && requiresCanonicalVisualMap(task)))) {
|
|
87
|
+
legacyResiduals.push({
|
|
88
|
+
type: "legacy-task-contract-gap",
|
|
89
|
+
taskId: key,
|
|
90
|
+
path: `TARGET:${taskContract[1]}`,
|
|
91
|
+
missing: taskContract[3],
|
|
92
|
+
reason: "Historical or unknown-state task. Do not migrate mechanically; upgrade only if reopened or reused as current evidence.",
|
|
93
|
+
});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
addTaskAction(
|
|
97
|
+
key,
|
|
98
|
+
`TARGET:${taskContract[1]}`,
|
|
99
|
+
actionFile,
|
|
100
|
+
"For active, reopened, or full-cutover tasks, add standalone v1 task contract files by adapting the localized task template and preserving evidence links.",
|
|
101
|
+
);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const reviewGap = warning.match(/(?:adoption-needed:\s*)?(docs\/[^\s]+\.md)\s+missing\s+(.+)/i);
|
|
106
|
+
if (reviewGap && /Reviewer Identity|Confidence Challenge|Evidence Checked|Final Confidence Basis/i.test(reviewGap[2])) {
|
|
107
|
+
const key = reviewGap[1];
|
|
108
|
+
const existing = reviewActionsByPath.get(key) || {
|
|
109
|
+
path: `TARGET:${key}`,
|
|
110
|
+
missing: new Set(),
|
|
111
|
+
action: "Upgrade this review only if it is active, release-blocking, or reused as current evidence. Otherwise keep it as historical material.",
|
|
112
|
+
};
|
|
113
|
+
existing.missing.add(reviewGap[2]);
|
|
114
|
+
reviewActionsByPath.set(key, existing);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const legacyRequired = warning.match(/-\s+missing required file:\s+([^\s]+)/i);
|
|
119
|
+
if (legacyRequired) {
|
|
120
|
+
legacyActions.push({
|
|
121
|
+
type: "missing-reference",
|
|
122
|
+
path: `TARGET:${legacyRequired[1]}`,
|
|
123
|
+
action: "Create or adapt this reference only when the related capability is intentionally adopted.",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const legacyVisualOnlyTasks = [];
|
|
129
|
+
const unknownClassificationTasks = [];
|
|
130
|
+
const weakBriefTasks = [];
|
|
131
|
+
for (const task of status.tasks) {
|
|
132
|
+
if (task.visualMapStatus === "legacy-only") {
|
|
133
|
+
legacyVisualOnlyTasks.push({
|
|
134
|
+
taskId: task.shortId,
|
|
135
|
+
path: task.path,
|
|
136
|
+
classification: task.migrationClassification,
|
|
137
|
+
action: "Rewrite legacy visual_roadmap.md into canonical visual_map.md. Do not keep it as the active task map.",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (task.migrationClassification === "unknown-needs-human") {
|
|
141
|
+
unknownClassificationTasks.push({
|
|
142
|
+
taskId: task.shortId,
|
|
143
|
+
path: task.path,
|
|
144
|
+
state: task.state,
|
|
145
|
+
action: "Classify whether this is active, reopened, current evidence, historical-with-diagram, or historical-no-map-needed before full cutover.",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (task.briefQuality?.status !== "pass") {
|
|
149
|
+
weakBriefTasks.push({
|
|
150
|
+
taskId: task.shortId,
|
|
151
|
+
path: task.path,
|
|
152
|
+
issues: task.briefQuality?.issues || [],
|
|
153
|
+
action: "Rewrite brief.md so a human can understand the goal, status, evidence, risks, and next action without opening the full task archive.",
|
|
154
|
+
});
|
|
155
|
+
addTaskAction(
|
|
156
|
+
task.shortId,
|
|
157
|
+
task.path,
|
|
158
|
+
"brief.md",
|
|
159
|
+
"Rewrite the human brief and preserve links to source task evidence.",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (requiresCanonicalVisualMap(task) && task.visualMapSource !== "canonical") {
|
|
163
|
+
addTaskAction(
|
|
164
|
+
task.shortId,
|
|
165
|
+
task.path,
|
|
166
|
+
visualMapFile,
|
|
167
|
+
"Rewrite task diagrams into canonical visual_map.md. Legacy visual_roadmap.md is read-only migration input.",
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (isActiveTaskState(task.state) && task.briefSource !== "standalone") {
|
|
171
|
+
addTaskAction(
|
|
172
|
+
task.shortId,
|
|
173
|
+
task.path,
|
|
174
|
+
"brief.md",
|
|
175
|
+
"For active or reopened tasks, add standalone v1 task contract files by adapting the localized task template and preserving evidence links.",
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const taskActions = [...taskActionsByTask.values()].map((action) => ({
|
|
181
|
+
...action,
|
|
182
|
+
files: [...action.files].sort(),
|
|
183
|
+
commands: [
|
|
184
|
+
...[...action.files].sort().map((file) => `copy/adapt docs/09-PLANNING/TASKS/_task-template/${file} into ${action.path}`),
|
|
185
|
+
`node scripts/harness.mjs task-log ${action.taskId} --message "migrated active task contract" ${target.projectRoot}`,
|
|
186
|
+
],
|
|
187
|
+
}));
|
|
188
|
+
const reviewActions = [...reviewActionsByPath.values()].map((action) => ({
|
|
189
|
+
...action,
|
|
190
|
+
missing: [...action.missing].sort(),
|
|
191
|
+
}));
|
|
192
|
+
const recommendedCapabilities = recommendedMigrationCapabilities(status, target, registry);
|
|
193
|
+
const missingExecutionStrategy = taskActions.filter((action) => action.files.includes("execution_strategy.md")).length;
|
|
194
|
+
const missingVisualMap = taskActions.filter((action) => action.files.includes(visualMapFile)).length;
|
|
195
|
+
const cutoverCounters = taskCutoverCounters(status.tasks);
|
|
196
|
+
const visualMapActions = taskActions.filter((action) => action.files.includes(visualMapFile)).length;
|
|
197
|
+
const fullCutoverEligible =
|
|
198
|
+
status.checkState.status === "pass" &&
|
|
199
|
+
taskActions.length === 0 &&
|
|
200
|
+
reviewActions.length === 0 &&
|
|
201
|
+
legacyActions.length === 0 &&
|
|
202
|
+
legacyResiduals.length === 0 &&
|
|
203
|
+
recommendedCapabilities.length === 0 &&
|
|
204
|
+
cutoverCounters.legacyVisualOnlyCount === 0 &&
|
|
205
|
+
cutoverCounters.unknownClassificationCount === 0 &&
|
|
206
|
+
cutoverCounters.weakBriefCount === 0 &&
|
|
207
|
+
cutoverCounters.missingCanonicalVisualMapCount === 0;
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
operation: "migrate-plan",
|
|
211
|
+
target: target.projectRoot,
|
|
212
|
+
locale,
|
|
213
|
+
mode: status.mode,
|
|
214
|
+
compatibility: {
|
|
215
|
+
preserves: [
|
|
216
|
+
"AGENTS.md and CLAUDE.md are never overwritten by safe-adoption.",
|
|
217
|
+
"Existing Harness-Ledger, SSoT, walkthrough, progress, review, and historical task plans are preserved.",
|
|
218
|
+
"Closed historical tasks may remain in legacy format unless they become active evidence for a strict gate.",
|
|
219
|
+
],
|
|
220
|
+
strictGate: "Normal migration mode reports adoption-needed warnings; --strict remains available as the final cutover gate.",
|
|
221
|
+
},
|
|
222
|
+
summary: {
|
|
223
|
+
tasks: status.tasks.length,
|
|
224
|
+
warnings: warnings.length,
|
|
225
|
+
missingExecutionStrategy,
|
|
226
|
+
missingVisualMap,
|
|
227
|
+
missingVisualRoadmap: missingVisualMap,
|
|
228
|
+
visualMapActions,
|
|
229
|
+
legacyVisualOnly: legacyVisualOnlyTasks.length,
|
|
230
|
+
unknownClassification: unknownClassificationTasks.length,
|
|
231
|
+
weakBrief: weakBriefTasks.length,
|
|
232
|
+
missingCanonicalVisualMap: cutoverCounters.missingCanonicalVisualMapCount,
|
|
233
|
+
taskActions: taskActions.length,
|
|
234
|
+
reviewSchemaGaps: reviewActions.length,
|
|
235
|
+
legacyReferenceGaps: legacyActions.length,
|
|
236
|
+
legacyResiduals: legacyResiduals.length,
|
|
237
|
+
recommendedCapabilities: recommendedCapabilities.map((capability) => capability.name),
|
|
238
|
+
fullCutoverEligible,
|
|
239
|
+
},
|
|
240
|
+
recommendedCapabilities,
|
|
241
|
+
phases: migrationPhases({ locale, recommendedCapabilities }),
|
|
242
|
+
taskActions: taskActions.slice(0, limit),
|
|
243
|
+
visualMapActions: taskActions.filter((action) => action.files.includes(visualMapFile)).slice(0, limit),
|
|
244
|
+
legacyVisualOnlyTasks: legacyVisualOnlyTasks.slice(0, limit),
|
|
245
|
+
unknownClassificationTasks: unknownClassificationTasks.slice(0, limit),
|
|
246
|
+
weakBriefTasks: weakBriefTasks.slice(0, limit),
|
|
247
|
+
reviewActions: reviewActions.slice(0, limit),
|
|
248
|
+
legacyActions: legacyActions.slice(0, limit),
|
|
249
|
+
legacyResiduals: legacyResiduals.slice(0, limit),
|
|
250
|
+
warningGroups: [...warningGroups.values()],
|
|
251
|
+
warningQueue: adoption.warnings.slice(0, limit),
|
|
252
|
+
nextCommands: [
|
|
253
|
+
`harness migrate-run --locale ${locale} --session-dir /tmp/cah-migration-${slug(status.project.name)} --out-dir /tmp/cah-migration-${slug(status.project.name)}/dashboard ${target.projectRoot}`,
|
|
254
|
+
`harness migrate-verify /tmp/cah-migration-${slug(status.project.name)}/session.json`,
|
|
255
|
+
`harness migrate-verify --full-cutover /tmp/cah-migration-${slug(status.project.name)}/session.json`,
|
|
256
|
+
`harness check --profile target-project ${target.projectRoot}`,
|
|
257
|
+
`harness check --profile target-project --strict ${target.projectRoot}`,
|
|
258
|
+
],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function runMigration(targetInput, options = {}) {
|
|
263
|
+
const target = normalizeTarget(targetInput);
|
|
264
|
+
const targetLabel = target.projectRoot;
|
|
265
|
+
const beforeGit = inspectGitStatus(target.projectRoot);
|
|
266
|
+
if (beforeGit.error) throw new Error(`Could not inspect git status: ${beforeGit.error.trim()}`);
|
|
267
|
+
if (beforeGit.dirty && !options.allowDirty) {
|
|
268
|
+
throw new Error(`Target git worktree is dirty; rerun with --allow-dirty after reviewing changes.\n${beforeGit.entries.join("\n")}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const localeProbe = probeTargetLocale(target);
|
|
272
|
+
if (!options.locale && localeProbe.mixedLanguageDetected && !options.assumeLocale) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`Target contains mixed Chinese/English harness text. Choose explicitly with --locale zh-CN or --locale en-US.\nProbe: ${JSON.stringify(localeProbe.totals)}`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const selectedLocale = normalizeLocale(options.locale || localeProbe.suggested);
|
|
278
|
+
const baselineStatus = buildStatus(targetInput, { strict: false, strictLegacy: false });
|
|
279
|
+
const initialPlan = buildMigrationPlan(targetInput, { limit: options.limit || 50 });
|
|
280
|
+
const sessionDir = ensureSessionDir(path.basename(target.projectRoot), options.sessionDir || "");
|
|
281
|
+
const dashboardDir = options.outDir ? path.resolve(options.outDir) : path.join(sessionDir, "dashboard");
|
|
282
|
+
|
|
283
|
+
let safeAdoption = null;
|
|
284
|
+
let dashboardCapability = null;
|
|
285
|
+
const safeAdoptionDryRun = addCapability(targetInput, "safe-adoption", { dryRun: true, locale: selectedLocale });
|
|
286
|
+
const dashboardDryRun = addCapability(targetInput, "dashboard", { dryRun: true, locale: selectedLocale });
|
|
287
|
+
let dashboardIndex = "";
|
|
288
|
+
if (!options.planOnly) {
|
|
289
|
+
safeAdoption = addCapability(targetInput, "safe-adoption", { dryRun: false, locale: selectedLocale });
|
|
290
|
+
dashboardCapability = addCapability(targetInput, "dashboard", { dryRun: false, locale: selectedLocale });
|
|
291
|
+
const writtenDashboardDir = writeDashboardFolder(dashboardDir, targetInput);
|
|
292
|
+
dashboardIndex = path.join(writtenDashboardDir, "index.html");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const normalStatus = buildStatus(targetInput, { strict: false, strictLegacy: false });
|
|
296
|
+
const strictStatus = buildStatus(targetInput, { strict: true, strictLegacy: true });
|
|
297
|
+
const finalPlan = buildMigrationPlan(targetInput, { limit: options.limit || 50 });
|
|
298
|
+
const afterGit = inspectGitStatus(target.projectRoot);
|
|
299
|
+
const strictDeferred = strictDeferredFromStatus(strictStatus);
|
|
300
|
+
const result = options.planOnly
|
|
301
|
+
? "plan-only"
|
|
302
|
+
: normalStatus.checkState.status === "fail"
|
|
303
|
+
? "failed"
|
|
304
|
+
: strictStatus.checkState.status === "fail"
|
|
305
|
+
? "adopted-with-strict-deferred"
|
|
306
|
+
: "complete";
|
|
307
|
+
const session = {
|
|
308
|
+
operation: "migrate-run",
|
|
309
|
+
version: 1,
|
|
310
|
+
schemaVersion: 1,
|
|
311
|
+
generatedAt: new Date().toISOString(),
|
|
312
|
+
result,
|
|
313
|
+
target: targetLabel,
|
|
314
|
+
sessionDir,
|
|
315
|
+
planOnly: Boolean(options.planOnly),
|
|
316
|
+
localeDecision: {
|
|
317
|
+
selected: selectedLocale,
|
|
318
|
+
source: options.locale ? "explicit" : localeProbe.mixedLanguageDetected ? "assumed-from-probe" : "probe",
|
|
319
|
+
probe: localeProbe,
|
|
320
|
+
},
|
|
321
|
+
capabilities: readCapabilityRegistry(target).capabilities,
|
|
322
|
+
baseline: {
|
|
323
|
+
statusPath: path.join(sessionDir, "baseline-status.json"),
|
|
324
|
+
migratePlanPath: path.join(sessionDir, "migrate-plan.json"),
|
|
325
|
+
taskCount: baselineStatus.tasks.length,
|
|
326
|
+
warningCount: baselineStatus.checkState.warnings,
|
|
327
|
+
},
|
|
328
|
+
dryRun: {
|
|
329
|
+
safeAdoption: safeAdoptionDryRun.report,
|
|
330
|
+
dashboard: dashboardDryRun.report,
|
|
331
|
+
},
|
|
332
|
+
capabilityReports: {
|
|
333
|
+
safeAdoption: safeAdoption?.report || null,
|
|
334
|
+
dashboard: dashboardCapability?.report || null,
|
|
335
|
+
},
|
|
336
|
+
dashboard: dashboardIndex ? { dir: dashboardDir, indexPath: dashboardIndex, kind: "html-folder" } : null,
|
|
337
|
+
plan: finalPlan,
|
|
338
|
+
checks: {
|
|
339
|
+
normal: statusCheckSummary(normalStatus),
|
|
340
|
+
strict: statusCheckSummary(strictStatus),
|
|
341
|
+
},
|
|
342
|
+
strictDeferred,
|
|
343
|
+
git: {
|
|
344
|
+
before: beforeGit,
|
|
345
|
+
after: afterGit,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
const sessionPath = path.join(sessionDir, "session.json");
|
|
349
|
+
fs.writeFileSync(path.join(sessionDir, "baseline-status.json"), `${JSON.stringify(baselineStatus, null, 2)}\n`);
|
|
350
|
+
fs.writeFileSync(path.join(sessionDir, "migrate-plan.json"), `${JSON.stringify(initialPlan, null, 2)}\n`);
|
|
351
|
+
fs.writeFileSync(path.join(sessionDir, "status-normal.json"), `${JSON.stringify(normalStatus, null, 2)}\n`);
|
|
352
|
+
fs.writeFileSync(path.join(sessionDir, "status-strict.json"), `${JSON.stringify(strictStatus, null, 2)}\n`);
|
|
353
|
+
fs.writeFileSync(sessionPath, `${JSON.stringify(session, null, 2)}\n`);
|
|
354
|
+
const reportPath = path.join(sessionDir, "report.md");
|
|
355
|
+
fs.writeFileSync(reportPath, writeMigrationReport(session));
|
|
356
|
+
return { ...session, sessionPath, reportPath };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function verifyMigrationSession(sessionPathInput, { fullCutover = false } = {}) {
|
|
360
|
+
const sessionPath = path.resolve(sessionPathInput || "");
|
|
361
|
+
if (!sessionPath || !fs.existsSync(sessionPath)) {
|
|
362
|
+
return { operation: "migrate-verify", status: "fail", failures: [`session file not found: ${sessionPathInput}`], warnings: [] };
|
|
363
|
+
}
|
|
364
|
+
const failures = [];
|
|
365
|
+
const warnings = [];
|
|
366
|
+
let session;
|
|
367
|
+
try {
|
|
368
|
+
session = JSON.parse(fs.readFileSync(sessionPath, "utf8"));
|
|
369
|
+
} catch (error) {
|
|
370
|
+
return { operation: "migrate-verify", status: "fail", failures: [`invalid session json: ${error.message}`], warnings };
|
|
371
|
+
}
|
|
372
|
+
if (session.operation !== "migrate-run") failures.push("session operation is not migrate-run");
|
|
373
|
+
if (session.schemaVersion !== 1 && session.version !== 1) failures.push("session missing schema version");
|
|
374
|
+
if (session.planOnly) failures.push("plan-only session is not completed migration evidence; rerun migrate-run without --plan-only");
|
|
375
|
+
if (!session.generatedAt) failures.push("session missing generatedAt");
|
|
376
|
+
if (!session.sessionDir || !fs.existsSync(session.sessionDir)) failures.push(`sessionDir missing: ${session.sessionDir || "(none)"}`);
|
|
377
|
+
if (!session.plan?.operation) failures.push("session missing migration plan");
|
|
378
|
+
if (!session.checks?.normal || !session.checks?.strict) failures.push("session missing recorded normal/strict checks");
|
|
379
|
+
if (!session.git?.before || !session.git?.after) failures.push("session missing git audit metadata");
|
|
380
|
+
if (session.git?.before && session.git.before.inGit !== true) failures.push("migration target was not recorded as a git worktree");
|
|
381
|
+
if (session.git?.after && session.git.after.inGit !== true) failures.push("migration target after-state was not recorded as a git worktree");
|
|
382
|
+
if (!session.target || !fs.existsSync(session.target)) failures.push(`target missing: ${session.target || "(none)"}`);
|
|
383
|
+
if (!session.localeDecision?.selected) failures.push("session missing locale decision");
|
|
384
|
+
if (session.git?.after?.staged?.length) failures.push(`migration left staged files: ${session.git.after.staged.join(", ")}`);
|
|
385
|
+
|
|
386
|
+
if (session.target && fs.existsSync(session.target)) {
|
|
387
|
+
const target = normalizeTarget(session.target);
|
|
388
|
+
const currentGit = inspectGitStatus(target.projectRoot);
|
|
389
|
+
if (currentGit.error) failures.push(`could not inspect current git status: ${currentGit.error.trim()}`);
|
|
390
|
+
if (currentGit.inGit !== true) failures.push("target is not currently a git worktree");
|
|
391
|
+
if (currentGit.staged.length) failures.push(`target currently has staged files: ${currentGit.staged.join(", ")}`);
|
|
392
|
+
if (!session.planOnly) {
|
|
393
|
+
const registry = readCapabilityRegistry(target);
|
|
394
|
+
const capabilities = new Set(registry.capabilities.map((capability) => capability.name));
|
|
395
|
+
if (!registry.raw) failures.push(".harness-capabilities.json was not created");
|
|
396
|
+
for (const required of ["safe-adoption", "dashboard"]) {
|
|
397
|
+
if (!capabilities.has(required)) failures.push(`required capability missing: ${required}`);
|
|
398
|
+
}
|
|
399
|
+
if (session.localeDecision?.selected && registry.locale !== session.localeDecision.selected) {
|
|
400
|
+
failures.push(`registry locale ${registry.locale} does not match session locale ${session.localeDecision.selected}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const normal = buildStatus(target.projectRoot, { strict: false, strictLegacy: false });
|
|
404
|
+
if (normal.checkState.status === "fail") failures.push(`normal check fails with ${normal.checkState.failures} failures`);
|
|
405
|
+
const strict = buildStatus(target.projectRoot, { strict: true, strictLegacy: true });
|
|
406
|
+
if (strict.checkState.status === "fail") {
|
|
407
|
+
const deferred = session.strictDeferred;
|
|
408
|
+
if (session.result === "complete") failures.push("session claims complete while current strict check fails");
|
|
409
|
+
if (!deferred?.owner || !deferred?.trigger || !deferred?.nextAction || !deferred?.failureCount) {
|
|
410
|
+
failures.push("current strict failures need strictDeferred owner, trigger, nextAction, and failureCount");
|
|
411
|
+
} else {
|
|
412
|
+
warnings.push(`current strict cutover deferred: ${strict.checkState.failures} failures`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (!session.planOnly) {
|
|
418
|
+
const indexPath = session.dashboard?.indexPath || "";
|
|
419
|
+
const dashboardDir = session.dashboard?.dir || "";
|
|
420
|
+
if (!indexPath) failures.push("session missing dashboard index path");
|
|
421
|
+
if (indexPath && !/\.html?$/i.test(indexPath)) failures.push(`dashboard index is not HTML: ${indexPath}`);
|
|
422
|
+
if (indexPath && path.basename(indexPath) !== "index.html") failures.push(`dashboard index must be index.html: ${indexPath}`);
|
|
423
|
+
if (indexPath && !fs.existsSync(indexPath)) failures.push(`dashboard index not found: ${indexPath}`);
|
|
424
|
+
if (/\.md$/i.test(indexPath)) failures.push(`dashboard path points to Markdown: ${indexPath}`);
|
|
425
|
+
if (indexPath && dashboardDir && path.resolve(indexPath) !== path.join(path.resolve(dashboardDir), "index.html")) {
|
|
426
|
+
failures.push(`dashboard index is not inside dashboard dir: ${indexPath}`);
|
|
427
|
+
}
|
|
428
|
+
for (const required of ["assets/dashboard-data.js", "data/status.json", "data/adoption.json"]) {
|
|
429
|
+
if (dashboardDir && !fs.existsSync(path.join(dashboardDir, required))) failures.push(`dashboard folder missing ${required}`);
|
|
430
|
+
}
|
|
431
|
+
const dashboardHtml = indexPath && fs.existsSync(indexPath) ? readFileSafe(indexPath) : "";
|
|
432
|
+
if (dashboardHtml && !dashboardHtml.includes("dashboard-data.js")) failures.push("dashboard index does not load dashboard-data.js");
|
|
433
|
+
const dataScriptPath = dashboardDir ? path.join(dashboardDir, "assets/dashboard-data.js") : "";
|
|
434
|
+
const dataScript = dataScriptPath && fs.existsSync(dataScriptPath) ? readFileSafe(dataScriptPath) : "";
|
|
435
|
+
const dataMatch = dataScript.match(/window\.__HARNESS_DASHBOARD__\s*=\s*([\s\S]*);\s*$/);
|
|
436
|
+
if (!dataMatch) {
|
|
437
|
+
failures.push("dashboard-data.js does not contain a generated dashboard bundle");
|
|
438
|
+
} else {
|
|
439
|
+
try {
|
|
440
|
+
const dashboardBundle = JSON.parse(dataMatch[1]);
|
|
441
|
+
const expectedProjectName = session.target ? path.basename(session.target) : "";
|
|
442
|
+
if (dashboardBundle.status?.schemaVersion !== 2) failures.push("dashboard bundle missing status schemaVersion 2");
|
|
443
|
+
if (expectedProjectName && dashboardBundle.status?.project?.name !== expectedProjectName) {
|
|
444
|
+
failures.push(`dashboard bundle project ${dashboardBundle.status?.project?.name || "(none)"} does not match target ${expectedProjectName}`);
|
|
445
|
+
}
|
|
446
|
+
if (!dashboardBundle.status?.checkState) failures.push("dashboard bundle missing checkState");
|
|
447
|
+
if (!Array.isArray(dashboardBundle.adoption?.warnings)) failures.push("dashboard bundle missing adoption warnings array");
|
|
448
|
+
} catch (error) {
|
|
449
|
+
failures.push(`dashboard-data.js contains invalid dashboard JSON: ${error.message}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (session.checks?.normal?.status === "fail") failures.push("recorded normal check failed");
|
|
455
|
+
if (session.checks?.strict?.status === "fail") {
|
|
456
|
+
const deferred = session.strictDeferred;
|
|
457
|
+
if (!deferred?.owner || !deferred?.trigger || !deferred?.nextAction || !deferred?.failureCount) {
|
|
458
|
+
failures.push("strict failures need strictDeferred owner, trigger, nextAction, and failureCount");
|
|
459
|
+
} else {
|
|
460
|
+
warnings.push(`strict cutover deferred: ${deferred.failureCount} failures`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (fullCutover) validateFullCutoverSession(session, failures);
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
operation: "migrate-verify",
|
|
468
|
+
status: failures.length ? "fail" : "pass",
|
|
469
|
+
fullCutover: Boolean(fullCutover),
|
|
470
|
+
sessionPath,
|
|
471
|
+
target: session.target || "",
|
|
472
|
+
result: session.result || "",
|
|
473
|
+
dashboard: session.dashboard || null,
|
|
474
|
+
strictDeferred: session.strictDeferred || null,
|
|
475
|
+
failures,
|
|
476
|
+
warnings,
|
|
477
|
+
};
|
|
478
|
+
}
|