coding-agent-harness 1.0.4 → 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 +7 -0
- package/LICENSE +661 -21
- package/LICENSE-EXCEPTION.md +37 -0
- package/README.md +33 -1
- package/README.zh-CN.md +23 -1
- package/SKILL.md +9 -8
- package/docs-release/architecture/overview.md +1 -1
- package/docs-release/architecture/overview.zh-CN.md +1 -1
- 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/guides/agent-installation.en-US.md +8 -7
- package/docs-release/guides/agent-installation.md +9 -7
- package/docs-release/guides/preset-development.md +26 -2
- package/docs-release/guides/task-state-machine.en-US.md +30 -13
- package/docs-release/guides/task-state-machine.md +30 -13
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/INDEX.md +60 -0
- package/package.json +3 -2
- package/references/harness-ledger.md +1 -1
- package/scripts/commands/migration-command.mjs +30 -0
- package/scripts/commands/task-command.mjs +26 -25
- package/scripts/harness.mjs +7 -3
- package/scripts/lib/capability-registry.mjs +17 -21
- package/scripts/lib/check-module-parallel.mjs +9 -16
- package/scripts/lib/check-profiles.mjs +35 -81
- package/scripts/lib/check-task-contracts.mjs +13 -5
- package/scripts/lib/core-shared.mjs +55 -2
- package/scripts/lib/dashboard-data.mjs +126 -18
- package/scripts/lib/dashboard-workbench.mjs +80 -1
- package/scripts/lib/dashboard-writer.mjs +6 -2
- package/scripts/lib/git-status-summary.mjs +1 -1
- package/scripts/lib/governance-sync.mjs +180 -83
- package/scripts/lib/harness-core.mjs +1 -0
- package/scripts/lib/markdown-utils.mjs +33 -0
- package/scripts/lib/migration-planner.mjs +4 -6
- package/scripts/lib/phase-kind.mjs +50 -0
- package/scripts/lib/preset-engine.mjs +5 -8
- package/scripts/lib/preset-registry.mjs +188 -39
- package/scripts/lib/review-confirm-git-gate.mjs +1 -1
- package/scripts/lib/status-builder.mjs +88 -0
- package/scripts/lib/status-dashboard-renderer.mjs +7 -4
- 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 +11 -1
- 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 +40 -29
- package/scripts/lib/task-lifecycle/review-gates.mjs +13 -10
- 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.mjs +114 -147
- package/scripts/lib/task-metadata.mjs +118 -0
- package/scripts/lib/task-review-model.mjs +54 -68
- package/scripts/lib/task-scanner.mjs +70 -143
- package/skills/preset-creator/references/complex-task-skeleton/brief.md +11 -0
- package/templates/AGENTS.md.template +7 -5
- package/templates/dashboard/assets/app-src/00-state.js +12 -0
- package/templates/dashboard/assets/app-src/10-router.js +3 -0
- package/templates/dashboard/assets/app-src/20-overview.js +7 -3
- package/templates/dashboard/assets/app-src/35-task-detail.js +46 -6
- 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 +131 -0
- package/templates/dashboard/assets/app.css +583 -0
- package/templates/dashboard/assets/app.css.manifest.json +1 -0
- package/templates/dashboard/assets/app.js +578 -10
- package/templates/dashboard/assets/app.manifest.json +1 -0
- package/templates/dashboard/assets/css-src/00-foundation.css +4 -0
- package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +62 -0
- package/templates/dashboard/assets/css-src/45-presets.css +516 -0
- package/templates/dashboard/assets/i18n.js +140 -2
- package/templates/planning/INDEX.md +87 -0
- package/templates/planning/brief.md +1 -1
- package/templates/planning/module_session_prompt.md +1 -0
- package/templates/planning/review.md +0 -18
- package/templates/planning/task_plan.md +4 -43
- package/templates/planning/visual_map.md +13 -9
- package/templates/planning/visual_map.simple.md +52 -0
- package/templates/reference/execution-workflow-standard.md +29 -2
- package/templates-zh-CN/AGENTS.md.template +7 -5
- package/templates-zh-CN/planning/INDEX.md +87 -0
- package/templates-zh-CN/planning/brief.md +1 -1
- package/templates-zh-CN/planning/module_session_prompt.md +1 -0
- package/templates-zh-CN/planning/review.md +0 -18
- package/templates-zh-CN/planning/task_plan.md +3 -63
- 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/execution-workflow-standard.md +31 -6
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
3
5
|
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { readBundledTemplate, readFileSafe, repoRoot, todayDate, toPosix, visualMapFile } from "./core-shared.mjs";
|
|
6
|
+
import { readBundledTemplate, readFileSafe, readJsonSafe, repoRoot, todayDate, toPosix, visualMapFile } from "./core-shared.mjs";
|
|
5
7
|
import { collectTasks } from "./task-scanner.mjs";
|
|
6
|
-
import { firstColumn, splitMarkdownRow,
|
|
7
|
-
import { markdownCell } from "./task-lifecycle/text-utils.mjs";
|
|
8
|
+
import { appendMarkdownTableRow, firstColumn, fitMarkdownTableRow, splitMarkdownRow, upsertMarkdownTableRow } from "./markdown-utils.mjs";
|
|
8
9
|
|
|
9
10
|
export class GovernanceSyncError extends Error {
|
|
10
11
|
constructor(message, { code = "governance-sync-failed", details = {}, recovery = [] } = {}) {
|
|
@@ -16,38 +17,14 @@ export class GovernanceSyncError extends Error {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
export function beginGovernanceSync(target, { operation = "governance-sync", dryRun = false } = {}) {
|
|
20
|
+
export function beginGovernanceSync(target, { operation = "governance-sync", dryRun = false, allowDirtyWorktree = false, allowedRelativePaths = [] } = {}) {
|
|
20
21
|
if (dryRun) return { target, dryRun, operation, git: inspectGit(target.projectRoot), lockPath: "", active: false };
|
|
21
22
|
const lockPath = path.join(target.projectRoot, ".harness/locks/governance-sync.lock");
|
|
22
23
|
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
fd = fs.openSync(lockPath, "wx");
|
|
26
|
-
fs.writeFileSync(
|
|
27
|
-
fd,
|
|
28
|
-
`${JSON.stringify({
|
|
29
|
-
operation,
|
|
30
|
-
pid: process.pid,
|
|
31
|
-
host: process.env.HOSTNAME || "",
|
|
32
|
-
branch: currentBranch(target.projectRoot),
|
|
33
|
-
targetRoot: target.projectRoot,
|
|
34
|
-
startedAt: new Date().toISOString(),
|
|
35
|
-
}, null, 2)}\n`,
|
|
36
|
-
);
|
|
37
|
-
} catch (error) {
|
|
38
|
-
if (fd !== null) fs.closeSync(fd);
|
|
39
|
-
throw new GovernanceSyncError("Governance sync lock already exists; refusing concurrent registry writes.", {
|
|
40
|
-
code: "governance-lock-exists",
|
|
41
|
-
details: { lockPath, error: error.message },
|
|
42
|
-
recovery: [
|
|
43
|
-
`Inspect ${lockPath}.`,
|
|
44
|
-
"If no process owns the lock, remove it manually and retry.",
|
|
45
|
-
],
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
if (fd !== null) fs.closeSync(fd);
|
|
24
|
+
acquireGovernanceSyncLock(lockPath, target, { operation });
|
|
49
25
|
|
|
50
26
|
const gitState = inspectGit(target.projectRoot);
|
|
27
|
+
const allowed = [...new Set((allowedRelativePaths || []).filter(Boolean).map(toPosix))].sort();
|
|
51
28
|
if (gitState.inGit) {
|
|
52
29
|
if (real(gitState.gitRoot) !== real(target.projectRoot)) {
|
|
53
30
|
releaseGovernanceSync({ lockPath, active: true });
|
|
@@ -57,7 +34,7 @@ export function beginGovernanceSync(target, { operation = "governance-sync", dry
|
|
|
57
34
|
recovery: ["Run the harness command against the target repository root."],
|
|
58
35
|
});
|
|
59
36
|
}
|
|
60
|
-
if (gitState.entries.length > 0) {
|
|
37
|
+
if (gitState.entries.length > 0 && !allowDirtyWorktree) {
|
|
61
38
|
releaseGovernanceSync({ lockPath, active: true });
|
|
62
39
|
throw new GovernanceSyncError("Governance sync requires a clean Git working tree before CLI-owned writes.", {
|
|
63
40
|
code: "governance-git-dirty",
|
|
@@ -65,9 +42,79 @@ export function beginGovernanceSync(target, { operation = "governance-sync", dry
|
|
|
65
42
|
recovery: ["Commit or otherwise resolve unrelated changes before running this lifecycle command."],
|
|
66
43
|
});
|
|
67
44
|
}
|
|
45
|
+
if (gitState.entries.length > 0 && allowDirtyWorktree) {
|
|
46
|
+
try {
|
|
47
|
+
assertDirtyCompatibleWithWriteScope(gitState.entries, allowed);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
releaseGovernanceSync({ lockPath, active: true });
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
68
53
|
assertCommitIdentity(target.projectRoot);
|
|
69
54
|
}
|
|
70
|
-
|
|
55
|
+
const initialDirtyEntries = gitState.inGit ? gitState.entries.map((entry) => ({
|
|
56
|
+
...entry,
|
|
57
|
+
fingerprint: fingerprintEntry(target.projectRoot, entry),
|
|
58
|
+
})) : [];
|
|
59
|
+
return { target, dryRun, operation, git: gitState, initialDirtyEntries, lockPath, active: true };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function acquireGovernanceSyncLock(lockPath, target, { operation }) {
|
|
63
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
64
|
+
let fd = null;
|
|
65
|
+
try {
|
|
66
|
+
fd = fs.openSync(lockPath, "wx");
|
|
67
|
+
fs.writeFileSync(
|
|
68
|
+
fd,
|
|
69
|
+
`${JSON.stringify({
|
|
70
|
+
operation,
|
|
71
|
+
pid: process.pid,
|
|
72
|
+
host: governanceLockHost(),
|
|
73
|
+
branch: currentBranch(target.projectRoot),
|
|
74
|
+
targetRoot: target.projectRoot,
|
|
75
|
+
startedAt: new Date().toISOString(),
|
|
76
|
+
}, null, 2)}\n`,
|
|
77
|
+
);
|
|
78
|
+
fs.closeSync(fd);
|
|
79
|
+
return;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (fd !== null) fs.closeSync(fd);
|
|
82
|
+
if (error?.code === "EEXIST" && attempt === 0 && removeStaleGovernanceSyncLock(lockPath)) continue;
|
|
83
|
+
throw governanceLockExistsError(lockPath, error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function removeStaleGovernanceSyncLock(lockPath) {
|
|
89
|
+
const lockContent = readFileSafe(lockPath);
|
|
90
|
+
const lock = readJsonSafe(lockPath, null);
|
|
91
|
+
if (!lock) return false;
|
|
92
|
+
if (lock.host !== governanceLockHost()) return false;
|
|
93
|
+
if (!Number.isInteger(lock?.pid) || lock.pid <= 0) return false;
|
|
94
|
+
try {
|
|
95
|
+
process.kill(lock.pid, 0);
|
|
96
|
+
return false;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error?.code !== "ESRCH") return false;
|
|
99
|
+
}
|
|
100
|
+
if (readFileSafe(lockPath) !== lockContent) return false;
|
|
101
|
+
fs.rmSync(lockPath);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function governanceLockHost() {
|
|
106
|
+
return process.env.HOSTNAME || os.hostname() || "";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function governanceLockExistsError(lockPath, error) {
|
|
110
|
+
return new GovernanceSyncError("Governance sync lock already exists; refusing concurrent registry writes.", {
|
|
111
|
+
code: "governance-lock-exists",
|
|
112
|
+
details: { lockPath, error: error?.message || String(error) },
|
|
113
|
+
recovery: [
|
|
114
|
+
`Inspect ${lockPath}.`,
|
|
115
|
+
"If no process owns the lock, remove it manually and retry.",
|
|
116
|
+
],
|
|
117
|
+
});
|
|
71
118
|
}
|
|
72
119
|
|
|
73
120
|
export function releaseGovernanceSync(context) {
|
|
@@ -82,24 +129,32 @@ export function releaseGovernanceSync(context) {
|
|
|
82
129
|
export function commitGovernanceSync(context, allowedRelativePaths, { message = "chore(harness): sync governance state" } = {}) {
|
|
83
130
|
const allowed = [...new Set((allowedRelativePaths || []).filter(Boolean).map(toPosix))].sort();
|
|
84
131
|
if (context?.dryRun || !context?.git?.inGit) return { committed: false, reason: context?.git?.inGit ? "dry-run" : "not-git", allowedPaths: allowed };
|
|
85
|
-
assertOnlyAllowedChanged(context.target.projectRoot, allowed);
|
|
86
132
|
if (allowed.length === 0) return { committed: false, reason: "no-allowed-paths", allowedPaths: allowed };
|
|
133
|
+
assertNoUnexpectedOutsideChanges(context.target.projectRoot, allowed, context.initialDirtyEntries || []);
|
|
87
134
|
git(context.target.projectRoot, ["add", "--", ...allowed]);
|
|
88
135
|
assertOnlyAllowedStaged(context.target.projectRoot, allowed);
|
|
89
136
|
const staged = git(context.target.projectRoot, ["diff", "--cached", "--name-only", "-z"]).stdout.split("\0").filter(Boolean);
|
|
90
137
|
if (staged.length === 0) return { committed: false, reason: "no-changes", allowedPaths: allowed };
|
|
91
|
-
const commitResult = git(context.target.projectRoot, ["commit", "-m", message], { allowFailure: true });
|
|
138
|
+
const commitResult = git(context.target.projectRoot, ["-c", "core.hooksPath=/dev/null", "commit", "--no-verify", "-m", message], { allowFailure: true });
|
|
92
139
|
if (commitResult.status !== 0) {
|
|
140
|
+
let outsideChanges = null;
|
|
141
|
+
try {
|
|
142
|
+
assertNoUnexpectedOutsideChanges(context.target.projectRoot, allowed, context.initialDirtyEntries || []);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
outsideChanges = error.details || null;
|
|
145
|
+
}
|
|
93
146
|
throw new GovernanceSyncError("Governance sync wrote files but Git commit failed.", {
|
|
94
147
|
code: "governance-git-commit-failed",
|
|
95
|
-
details: { stdout: commitResult.stdout.trim(), stderr: commitResult.stderr.trim(), allowedPaths: allowed },
|
|
148
|
+
details: { stdout: commitResult.stdout.trim(), stderr: commitResult.stderr.trim(), allowedPaths: allowed, outsideChanges },
|
|
96
149
|
recovery: [
|
|
97
150
|
`Inspect files: ${allowed.join(", ")}`,
|
|
98
|
-
`Then run: git add -- ${allowed.join(" ")} && git commit -m ${JSON.stringify(message)}`,
|
|
151
|
+
`Then run: git add -- ${allowed.join(" ")} && git -c core.hooksPath=/dev/null commit --no-verify -m ${JSON.stringify(message)}`,
|
|
99
152
|
],
|
|
100
153
|
});
|
|
101
154
|
}
|
|
102
|
-
|
|
155
|
+
assertLastCommitOnlyAllowed(context.target.projectRoot, allowed);
|
|
156
|
+
assertNoUnexpectedOutsideChanges(context.target.projectRoot, allowed, context.initialDirtyEntries || []);
|
|
157
|
+
assertWriteScopeClean(context.target.projectRoot, allowed);
|
|
103
158
|
return { committed: true, commitSha: git(context.target.projectRoot, ["rev-parse", "HEAD"]).stdout.trim(), allowedPaths: allowed };
|
|
104
159
|
}
|
|
105
160
|
|
|
@@ -138,7 +193,7 @@ export function syncModuleStepGovernance(target, { moduleKey, stepId, state, dry
|
|
|
138
193
|
"module-step",
|
|
139
194
|
todayDate(),
|
|
140
195
|
];
|
|
141
|
-
fs.writeFileSync(ledgerPath,
|
|
196
|
+
fs.writeFileSync(ledgerPath, appendMarkdownTableRow(content, /^ID$/i, row));
|
|
142
197
|
}
|
|
143
198
|
changes.push({ destination: ledgerRelative, action: dryRun ? "would-sync-governance" : "sync-governance", surface: "harness-ledger" });
|
|
144
199
|
return { changes };
|
|
@@ -168,7 +223,7 @@ function syncLedgerRow(target, task, { event, state, message, planPath, reviewPa
|
|
|
168
223
|
message || "none",
|
|
169
224
|
todayDate(),
|
|
170
225
|
];
|
|
171
|
-
fs.writeFileSync(ledgerPath,
|
|
226
|
+
fs.writeFileSync(ledgerPath, upsertMarkdownTableRow(content, /^ID$/i, (header, existing) => rowMatchesPlan(header, existing, planPath), row));
|
|
172
227
|
}
|
|
173
228
|
return { destination: relative, action: dryRun ? "would-sync-governance" : "sync-governance", surface: "harness-ledger" };
|
|
174
229
|
}
|
|
@@ -195,7 +250,7 @@ function syncModuleRegistryRow(target, task, { state, planPath, dryRun }) {
|
|
|
195
250
|
"none",
|
|
196
251
|
todayDate(),
|
|
197
252
|
];
|
|
198
|
-
fs.writeFileSync(registryPath,
|
|
253
|
+
fs.writeFileSync(registryPath, upsertMarkdownTableRow(content, /^ID$/i, (header, existing) => rowMatchesModule(header, existing, moduleKey, modulePlan), row));
|
|
199
254
|
}
|
|
200
255
|
return { destination: relative, action: dryRun ? "would-sync-governance" : "sync-governance", surface: "module-registry" };
|
|
201
256
|
}
|
|
@@ -285,7 +340,7 @@ function renderModuleVisualMap(moduleKey, tasks) {
|
|
|
285
340
|
});
|
|
286
341
|
const graphLines = tasks.map((task, index) => {
|
|
287
342
|
const stepId = moduleStepId(task);
|
|
288
|
-
const label =
|
|
343
|
+
const label = fitMarkdownTableRow([task.title || task.shortId || task.id], 1)[0].replace(/"/g, "'");
|
|
289
344
|
if (index === 0) return ` ${stepId}["${label}"]`;
|
|
290
345
|
const previous = moduleStepId(tasks[index - 1]);
|
|
291
346
|
return ` ${previous} --> ${stepId}["${label}"]`;
|
|
@@ -313,7 +368,7 @@ ${graphLines.length ? graphLines.join("\n") : " EMPTY[\"No module tasks\"]"}
|
|
|
313
368
|
|
|
314
369
|
| Phase ID | Depends On | State | Completion | Output | Required Evidence | Evidence Status | Blocking Risk | Owner / Handoff |
|
|
315
370
|
| --- | --- | --- | ---: | --- | --- | --- | --- | --- |
|
|
316
|
-
${rows.map((row) => `| ${
|
|
371
|
+
${rows.map((row) => `| ${fitMarkdownTableRow(row, 9).join(" | ")} |`).join("\n")}
|
|
317
372
|
|
|
318
373
|
Allowed Evidence Status: missing, partial, present, waived.
|
|
319
374
|
`;
|
|
@@ -329,10 +384,10 @@ function replaceTableRows(content, headerPattern, rows) {
|
|
|
329
384
|
if (!separator.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
|
|
330
385
|
let end = index + 2;
|
|
331
386
|
while (end < lines.length && lines[end].trim().startsWith("|")) end += 1;
|
|
332
|
-
lines.splice(index + 2, end - index - 2, ...rows.map((row) => `| ${
|
|
387
|
+
lines.splice(index + 2, end - index - 2, ...rows.map((row) => `| ${fitMarkdownTableRow(row, header.length).join(" | ")} |`));
|
|
333
388
|
return `${lines.join("\n").trimEnd()}\n`;
|
|
334
389
|
}
|
|
335
|
-
return `${String(content || "").trimEnd()}\n\n${rows.map((row) => `| ${
|
|
390
|
+
return `${String(content || "").trimEnd()}\n\n${rows.map((row) => `| ${fitMarkdownTableRow(row, row.length).join(" | ")} |`).join("\n")}\n`;
|
|
336
391
|
}
|
|
337
392
|
|
|
338
393
|
function existingOrTemplate(filePath, templateSource) {
|
|
@@ -361,32 +416,6 @@ function ensureFileFromTemplate(destinationPath, templateSource, { dryRun = fals
|
|
|
361
416
|
fs.writeFileSync(destinationPath, readBundledTemplate(templateSource));
|
|
362
417
|
}
|
|
363
418
|
|
|
364
|
-
function upsertRow(content, headerPattern, matcher, row) {
|
|
365
|
-
const updated = updateMarkdownTableRow(content, headerPattern, (header, existing) => (matcher(header, existing) ? fitRow(row, header.length) : null));
|
|
366
|
-
if (updated.matched) return updated.content;
|
|
367
|
-
return appendRow(content, headerPattern, row);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function appendRow(content, headerPattern, row) {
|
|
371
|
-
const lines = String(content || "").split(/\r?\n/);
|
|
372
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
373
|
-
if (!lines[index].trim().startsWith("|")) continue;
|
|
374
|
-
const header = splitMarkdownRow(lines[index]);
|
|
375
|
-
if (!header.some((cell) => headerPattern.test(cell))) continue;
|
|
376
|
-
let insertAt = index + 2;
|
|
377
|
-
while (insertAt < lines.length && lines[insertAt].trim().startsWith("|")) insertAt += 1;
|
|
378
|
-
lines.splice(insertAt, 0, `| ${fitRow(row, header.length).join(" | ")} |`);
|
|
379
|
-
return lines.join("\n");
|
|
380
|
-
}
|
|
381
|
-
return `${String(content || "").trimEnd()}\n\n| ${row.join(" | ")} |\n`;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function fitRow(row, length) {
|
|
385
|
-
const next = row.map((cell) => markdownCell(cell));
|
|
386
|
-
while (next.length < length) next.push("");
|
|
387
|
-
return next.slice(0, length);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
419
|
function rowMatchesPlan(header, row, planPath) {
|
|
391
420
|
const planIndex = firstColumn(header, ["Task Plan", "Plan", "当前产物"]);
|
|
392
421
|
return planIndex >= 0 && String(row[planIndex] || "").includes(planPath);
|
|
@@ -446,19 +475,28 @@ function assertCommitIdentity(root) {
|
|
|
446
475
|
}
|
|
447
476
|
}
|
|
448
477
|
|
|
449
|
-
function
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
478
|
+
function assertDirtyCompatibleWithWriteScope(entries, allowedPaths) {
|
|
479
|
+
const allowed = new Set(allowedPaths);
|
|
480
|
+
const overlapping = entries.filter((entry) => allowed.has(entry.path));
|
|
481
|
+
if (overlapping.length > 0) {
|
|
482
|
+
throw new GovernanceSyncError("Governance sync write scope overlaps existing dirty files; refusing to overwrite user-owned changes.", {
|
|
483
|
+
code: "governance-write-scope-dirty",
|
|
484
|
+
details: { overlapping, allowedPaths },
|
|
485
|
+
recovery: ["Commit, move, or remove the overlapping files before retrying this lifecycle command."],
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
const outsideStaged = entries.filter((entry) => entry.index !== " " && entry.index !== "?" && !allowed.has(entry.path));
|
|
489
|
+
if (outsideStaged.length > 0) {
|
|
490
|
+
throw new GovernanceSyncError("Git index contains staged files outside the governance sync write scope.", {
|
|
491
|
+
code: "governance-index-outside-write-scope",
|
|
492
|
+
details: { disallowed: outsideStaged, allowedPaths },
|
|
493
|
+
recovery: ["Unstage unrelated files before retrying the lifecycle command."],
|
|
456
494
|
});
|
|
457
495
|
}
|
|
458
496
|
}
|
|
459
497
|
|
|
460
498
|
function assertOnlyAllowedStaged(root, allowedPaths) {
|
|
461
|
-
const outside = statusEntries(root).filter((entry) => entry.index !== " " && !allowedPaths.includes(entry.path));
|
|
499
|
+
const outside = statusEntries(root).filter((entry) => entry.index !== " " && entry.index !== "?" && !allowedPaths.includes(entry.path));
|
|
462
500
|
if (outside.length > 0) {
|
|
463
501
|
throw new GovernanceSyncError("Git index contains staged files outside the governance sync allowlist.", {
|
|
464
502
|
code: "governance-index-allowlist-violation",
|
|
@@ -468,17 +506,76 @@ function assertOnlyAllowedStaged(root, allowedPaths) {
|
|
|
468
506
|
}
|
|
469
507
|
}
|
|
470
508
|
|
|
471
|
-
function
|
|
509
|
+
function assertNoUnexpectedOutsideChanges(root, allowedPaths, initialDirtyEntries) {
|
|
510
|
+
const allowed = new Set(allowedPaths);
|
|
511
|
+
const initialByPath = new Map(
|
|
512
|
+
(initialDirtyEntries || [])
|
|
513
|
+
.filter((entry) => !allowed.has(entry.path))
|
|
514
|
+
.map((entry) => [entry.path, entry]),
|
|
515
|
+
);
|
|
516
|
+
const unexpected = [];
|
|
517
|
+
const changed = [];
|
|
518
|
+
for (const entry of statusEntries(root)) {
|
|
519
|
+
if (allowed.has(entry.path)) continue;
|
|
520
|
+
const current = { ...entry, fingerprint: fingerprintEntry(root, entry) };
|
|
521
|
+
const initial = initialByPath.get(entry.path);
|
|
522
|
+
if (!initial) {
|
|
523
|
+
unexpected.push(current);
|
|
524
|
+
} else if (initial.raw !== current.raw || initial.fingerprint !== current.fingerprint) {
|
|
525
|
+
changed.push({ before: initial, after: current });
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (unexpected.length > 0 || changed.length > 0) {
|
|
529
|
+
throw new GovernanceSyncError("Governance sync produced changes outside its write scope.", {
|
|
530
|
+
code: "governance-allowlist-violation",
|
|
531
|
+
details: { unexpected, changed, allowedPaths },
|
|
532
|
+
recovery: ["Inspect the extra paths; the CLI will not stage or commit unrelated files."],
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function assertLastCommitOnlyAllowed(root, allowedPaths) {
|
|
538
|
+
const committed = git(root, ["diff-tree", "--no-commit-id", "--name-only", "-r", "-z", "HEAD"]).stdout
|
|
539
|
+
.split("\0")
|
|
540
|
+
.filter(Boolean)
|
|
541
|
+
.map(toPosix);
|
|
542
|
+
const outside = committed.filter((file) => !allowedPaths.includes(file));
|
|
543
|
+
if (outside.length > 0) {
|
|
544
|
+
throw new GovernanceSyncError("Governance sync commit contains files outside its write scope.", {
|
|
545
|
+
code: "governance-commit-allowlist-violation",
|
|
546
|
+
details: { disallowed: outside, committed, allowedPaths },
|
|
547
|
+
recovery: ["Inspect the last commit and remove any files that are not owned by the lifecycle command."],
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function assertWriteScopeClean(root, allowedPaths) {
|
|
472
553
|
const entries = statusEntries(root);
|
|
473
|
-
|
|
474
|
-
|
|
554
|
+
const remaining = entries.filter((entry) => allowedPaths.includes(entry.path));
|
|
555
|
+
if (remaining.length > 0) {
|
|
556
|
+
throw new GovernanceSyncError("Governance sync commit completed but write scope is not clean.", {
|
|
475
557
|
code: "governance-post-commit-dirty",
|
|
476
|
-
details: { entries },
|
|
477
|
-
recovery: ["Inspect remaining files before continuing."],
|
|
558
|
+
details: { entries: remaining, allowedPaths },
|
|
559
|
+
recovery: ["Inspect remaining write-scope files before continuing."],
|
|
478
560
|
});
|
|
479
561
|
}
|
|
480
562
|
}
|
|
481
563
|
|
|
564
|
+
function fingerprintEntry(root, entry) {
|
|
565
|
+
const absolute = path.join(root, entry.path);
|
|
566
|
+
try {
|
|
567
|
+
const stat = fs.lstatSync(absolute);
|
|
568
|
+
if (stat.isSymbolicLink()) return `symlink:${fs.readlinkSync(absolute)}`;
|
|
569
|
+
if (stat.isFile()) {
|
|
570
|
+
return `file:${stat.size}:${crypto.createHash("sha256").update(fs.readFileSync(absolute)).digest("hex")}`;
|
|
571
|
+
}
|
|
572
|
+
if (stat.isDirectory()) return "directory";
|
|
573
|
+
return `${stat.mode}:${stat.size}`;
|
|
574
|
+
} catch {
|
|
575
|
+
return "missing";
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
482
579
|
function statusEntries(root) {
|
|
483
580
|
return git(root, ["status", "--porcelain=v1", "--untracked-files=all"]).stdout
|
|
484
581
|
.split(/\r?\n/)
|
|
@@ -2,6 +2,7 @@ export * from "./core-shared.mjs";
|
|
|
2
2
|
export * from "./markdown-utils.mjs";
|
|
3
3
|
export * from "./capability-registry.mjs";
|
|
4
4
|
export * from "./task-scanner.mjs";
|
|
5
|
+
export * from "./status-builder.mjs";
|
|
5
6
|
export * from "./check-profiles.mjs";
|
|
6
7
|
export * from "./dashboard-data.mjs";
|
|
7
8
|
export * from "./dashboard-workbench.mjs";
|
|
@@ -156,3 +156,36 @@ export function updateMarkdownTableRow(content, headerPattern, updater) {
|
|
|
156
156
|
}
|
|
157
157
|
return { content, matched: false };
|
|
158
158
|
}
|
|
159
|
+
|
|
160
|
+
export function upsertMarkdownTableRow(content, headerPattern, matcher, row) {
|
|
161
|
+
const updated = updateMarkdownTableRow(content, headerPattern, (header, existing) => (matcher(header, existing) ? fitMarkdownTableRow(row, header.length) : null));
|
|
162
|
+
if (updated.matched) return updated.content;
|
|
163
|
+
return appendMarkdownTableRow(content, headerPattern, row);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function appendMarkdownTableRow(content, headerPattern, row) {
|
|
167
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
168
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
169
|
+
if (!lines[index].trim().startsWith("|")) continue;
|
|
170
|
+
const header = splitMarkdownRow(lines[index]);
|
|
171
|
+
if (!header.some((cell) => headerPattern.test(cell))) continue;
|
|
172
|
+
let insertAt = index + 2;
|
|
173
|
+
while (insertAt < lines.length && lines[insertAt].trim().startsWith("|")) insertAt += 1;
|
|
174
|
+
lines.splice(insertAt, 0, `| ${fitMarkdownTableRow(row, header.length).join(" | ")} |`);
|
|
175
|
+
return lines.join("\n");
|
|
176
|
+
}
|
|
177
|
+
return `${String(content || "").trimEnd()}\n\n| ${row.map(markdownTableCell).join(" | ")} |\n`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function fitMarkdownTableRow(row, length) {
|
|
181
|
+
const next = row.map(markdownTableCell);
|
|
182
|
+
while (next.length < length) next.push("");
|
|
183
|
+
return next.slice(0, length);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function markdownTableCell(value) {
|
|
187
|
+
return String(value || "")
|
|
188
|
+
.replace(/\r?\n/g, " ")
|
|
189
|
+
.replaceAll("|", "\\|")
|
|
190
|
+
.trim();
|
|
191
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
normalizeTarget,
|
|
7
7
|
normalizeLocale,
|
|
8
8
|
readFileSafe,
|
|
9
|
+
readJsonSafe,
|
|
9
10
|
existsInDocs,
|
|
10
11
|
walkFiles,
|
|
11
12
|
toPosix,
|
|
@@ -363,12 +364,9 @@ export function verifyMigrationSession(sessionPathInput, { fullCutover = false }
|
|
|
363
364
|
}
|
|
364
365
|
const failures = [];
|
|
365
366
|
const warnings = [];
|
|
366
|
-
let
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
} catch (error) {
|
|
370
|
-
return { operation: "migrate-verify", status: "fail", failures: [`invalid session json: ${error.message}`], warnings };
|
|
371
|
-
}
|
|
367
|
+
let readError = null;
|
|
368
|
+
const session = readJsonSafe(sessionPath, null, { onError: (error) => { readError = error; } });
|
|
369
|
+
if (!session) return { operation: "migrate-verify", status: "fail", failures: [`invalid session json: ${readError?.message || "unknown parse error"}`], warnings };
|
|
372
370
|
if (session.operation !== "migrate-run") failures.push("session operation is not migrate-run");
|
|
373
371
|
if (session.schemaVersion !== 1 && session.version !== 1) failures.push("session missing schema version");
|
|
374
372
|
if (session.planOnly) failures.push("plan-only session is not completed migration evidence; rerun migrate-run without --plan-only");
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const allowedPhaseKinds = new Set(["init", "execution", "gate"]);
|
|
2
|
+
export const allowedPhaseActors = new Set(["agent", "human", "coordinator"]);
|
|
3
|
+
|
|
4
|
+
export function normalizePhaseKind(value) {
|
|
5
|
+
const normalized = String(value || "")
|
|
6
|
+
.replace(/`/g, "")
|
|
7
|
+
.trim()
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replaceAll("_", "-");
|
|
10
|
+
if (!normalized) return "execution";
|
|
11
|
+
if (normalized === "exec" || normalized === "implementation") return "execution";
|
|
12
|
+
if (normalized === "prep" || normalized === "discussion") return "init";
|
|
13
|
+
if (normalized === "review" || normalized === "closeout") return "gate";
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizePhaseActor(value) {
|
|
18
|
+
const normalized = String(value || "")
|
|
19
|
+
.replace(/`/g, "")
|
|
20
|
+
.trim()
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replaceAll("_", "-");
|
|
23
|
+
return normalized || "agent";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isExecutionPhase(phase) {
|
|
27
|
+
return normalizePhaseKind(phase?.kind) === "execution";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function nonSkippedPhases(phases = []) {
|
|
31
|
+
return phases.filter((phase) => phase.state !== "skipped");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function implementationPhases(phases = []) {
|
|
35
|
+
return nonSkippedPhases(phases).filter(isExecutionPhase);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function phaseCompletionAverage(phases = []) {
|
|
39
|
+
const scored = implementationPhases(phases);
|
|
40
|
+
if (scored.length === 0) return 0;
|
|
41
|
+
return Math.round(scored.reduce((sum, phase) => sum + phase.completion, 0) / scored.length);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function phaseHasRecordedProgress(phase) {
|
|
45
|
+
return (
|
|
46
|
+
phase.completion > 0 ||
|
|
47
|
+
["in_progress", "review", "blocked", "done"].includes(String(phase.state || "").toLowerCase()) ||
|
|
48
|
+
["partial", "present", "waived"].includes(String(phase.evidenceStatus || "").toLowerCase())
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
-
import { repoRoot, taskContractMarker, toPosix, visualMapFile } from "./core-shared.mjs";
|
|
5
|
+
import { readJsonSafe, repoRoot, taskContractMarker, toPosix, visualMapFile } from "./core-shared.mjs";
|
|
6
6
|
import { verifyMigrationSession } from "./migration-planner.mjs";
|
|
7
7
|
import { buildPresetAudit, renderPresetTemplate } from "./preset-registry.mjs";
|
|
8
8
|
|
|
@@ -25,12 +25,9 @@ export function resolvePresetInputs(preset, { cliArgs = [], fromSession = "", ta
|
|
|
25
25
|
}
|
|
26
26
|
const filePath = path.resolve(String(rawValue));
|
|
27
27
|
if (!fs.existsSync(filePath)) throw new Error(`Preset input file not found for ${declaration.flag || name}: ${rawValue}`);
|
|
28
|
-
let
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
} catch (error) {
|
|
32
|
-
throw new Error(`Invalid preset JSON input ${declaration.flag || name}: ${error.message}`);
|
|
33
|
-
}
|
|
28
|
+
let readError = null;
|
|
29
|
+
const value = readJsonSafe(filePath, null, { onError: (error) => { readError = error; } });
|
|
30
|
+
if (value === null) throw new Error(`Invalid preset JSON input ${declaration.flag || name}: ${readError?.message || "unknown parse error"}`);
|
|
34
31
|
if (declaration.validateOperation && value.operation !== declaration.validateOperation) {
|
|
35
32
|
throw new Error(`${preset.id} preset requires ${declaration.flag || name} operation ${declaration.validateOperation}`);
|
|
36
33
|
}
|
|
@@ -463,7 +460,7 @@ function targetCommit(projectRoot) {
|
|
|
463
460
|
|
|
464
461
|
function packageVersion() {
|
|
465
462
|
try {
|
|
466
|
-
return
|
|
463
|
+
return readJsonSafe(path.join(repoRoot, "package.json"), {}).version || "unknown";
|
|
467
464
|
} catch {
|
|
468
465
|
return "unknown";
|
|
469
466
|
}
|