coding-agent-harness 1.0.5 → 1.0.6
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/CONTRIBUTING.md +2 -2
- package/README.md +63 -3
- package/README.zh-CN.md +52 -3
- package/SKILL.md +43 -43
- package/dist/build-dist.mjs +189 -0
- package/dist/check-dist-observation.mjs +428 -0
- package/dist/check-harness.mjs +489 -0
- package/dist/check-import-graph.mjs +511 -0
- package/dist/check-runtime-emit.mjs +304 -0
- package/dist/check-type-boundaries.mjs +139 -0
- package/dist/commands/dashboard-command.mjs +80 -0
- package/dist/commands/migration-command.mjs +152 -0
- package/dist/commands/preset-command.mjs +91 -0
- package/dist/commands/task-command.mjs +324 -0
- package/dist/harness.mjs +304 -0
- package/dist/lib/capability-registry.mjs +643 -0
- package/dist/lib/check-module-parallel.mjs +227 -0
- package/dist/lib/check-profiles.mjs +414 -0
- package/dist/lib/check-task-contracts.mjs +54 -0
- package/dist/lib/core-shared.mjs +254 -0
- package/dist/lib/dashboard-data.mjs +608 -0
- package/dist/lib/dashboard-workbench.mjs +334 -0
- package/dist/lib/dashboard-writer.mjs +200 -0
- package/dist/lib/git-status-summary.mjs +45 -0
- package/dist/lib/governance-index-generator.mjs +236 -0
- package/dist/lib/governance-sync.mjs +617 -0
- package/dist/lib/governance-table-boundary.mjs +161 -0
- package/{scripts → dist}/lib/harness-core.mjs +2 -0
- package/dist/lib/harness-paths.mjs +338 -0
- package/dist/lib/lesson-maintenance.mjs +139 -0
- package/dist/lib/markdown-utils.mjs +193 -0
- package/dist/lib/migration-planner.mjs +439 -0
- package/dist/lib/migration-support.mjs +317 -0
- package/dist/lib/phase-kind.mjs +46 -0
- package/dist/lib/preset-audit-contracts.mjs +40 -0
- package/dist/lib/preset-engine.mjs +516 -0
- package/dist/lib/preset-registry.mjs +831 -0
- package/dist/lib/preset-resource-contracts.mjs +83 -0
- package/dist/lib/review-confirm-git-gate.mjs +244 -0
- package/dist/lib/status-builder.mjs +87 -0
- package/{scripts → dist}/lib/status-dashboard-renderer.mjs +44 -46
- package/dist/lib/structure-migration.mjs +404 -0
- package/dist/lib/subagent-authorization-audit.mjs +198 -0
- package/dist/lib/task-audit-metadata.mjs +376 -0
- package/dist/lib/task-audit-migration.mjs +355 -0
- package/dist/lib/task-completion-consistency.mjs +29 -0
- package/dist/lib/task-index.mjs +133 -0
- package/dist/lib/task-lesson-candidates.mjs +239 -0
- package/dist/lib/task-lesson-sedimentation.mjs +300 -0
- package/dist/lib/task-lifecycle/create-task-helpers.mjs +84 -0
- package/dist/lib/task-lifecycle/phase-sync.mjs +82 -0
- package/dist/lib/task-lifecycle/review-confirm.mjs +93 -0
- package/dist/lib/task-lifecycle/review-gates.mjs +62 -0
- package/dist/lib/task-lifecycle/review-submission.mjs +52 -0
- package/dist/lib/task-lifecycle/scaffold-provenance.mjs +54 -0
- package/dist/lib/task-lifecycle/template-files.mjs +52 -0
- package/dist/lib/task-lifecycle/text-utils.mjs +26 -0
- package/dist/lib/task-lifecycle.mjs +611 -0
- package/dist/lib/task-metadata.mjs +116 -0
- package/dist/lib/task-review-model.mjs +474 -0
- package/dist/lib/task-scanner.mjs +439 -0
- package/dist/lib/task-tombstone-commands.mjs +125 -0
- package/dist/postinstall.mjs +14 -0
- package/dist/run-built-tests.mjs +84 -0
- package/docs-release/README.md +1 -0
- package/docs-release/architecture/overview.md +12 -12
- package/docs-release/architecture/overview.zh-CN.md +12 -12
- package/docs-release/architecture/system-explainer/01-system-overview.md +15 -14
- package/docs-release/architecture/system-explainer/02-module-dependency.md +8 -8
- package/docs-release/architecture/system-explainer/03-task-lifecycle.md +3 -3
- package/docs-release/architecture/system-explainer/04-check-and-governance.md +9 -7
- package/docs-release/architecture/system-explainer/05-data-flow.md +5 -5
- package/docs-release/architecture/system-explainer/06-preset-and-migration.md +1 -4
- package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +15 -14
- package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +8 -8
- package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +3 -3
- package/docs-release/architecture/system-explainer/en-US/04-check-and-governance.md +10 -8
- package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +5 -5
- package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +1 -4
- package/docs-release/guides/agent-installation.en-US.md +14 -8
- package/docs-release/guides/agent-installation.md +14 -8
- package/docs-release/guides/contributing.md +3 -3
- package/docs-release/guides/contributing.zh-CN.md +3 -3
- package/docs-release/guides/document-audience-and-surfaces.en-US.md +10 -10
- package/docs-release/guides/document-audience-and-surfaces.md +10 -10
- package/docs-release/guides/legacy-migration-agent-prompt.md +25 -2
- package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +25 -2
- package/docs-release/guides/migration-playbook.en-US.md +63 -1
- package/docs-release/guides/migration-playbook.md +59 -1
- package/docs-release/guides/parent-control-repository-pattern.en-US.md +25 -25
- package/docs-release/guides/parent-control-repository-pattern.md +25 -25
- package/docs-release/guides/preset-development.md +2 -2
- package/docs-release/guides/repository-operating-models.en-US.md +21 -21
- package/docs-release/guides/repository-operating-models.md +21 -21
- package/docs-release/guides/task-state-machine.en-US.md +5 -5
- package/docs-release/guides/task-state-machine.md +5 -5
- package/docs-release/guides/typescript-runtime-migration-closeout.md +96 -0
- package/examples/minimal-project/AGENTS.md +2 -2
- package/examples/minimal-project/coding-agent-harness/harness.yaml +14 -0
- package/examples/minimal-project/coding-agent-harness/planning/tasks/demo-task/progress.md +11 -0
- package/examples/minimal-project/{docs/09-PLANNING/TASKS → coding-agent-harness/planning/tasks}/demo-task/review.md +1 -1
- package/package.json +20 -12
- package/presets/legacy-migration/preset.yaml +5 -5
- package/presets/legacy-migration/templates/execution_strategy.append.md +1 -1
- package/presets/lesson-sedimentation/preset.yaml +3 -3
- package/presets/module/preset.yaml +2 -2
- package/presets/module/templates/execution_strategy.append.md +1 -1
- package/presets/module/templates/task_plan.append.md +3 -3
- package/presets/standard-task/preset.yaml +2 -2
- package/references/adversarial-review-standard.md +2 -2
- package/references/agents-md-pattern.md +14 -14
- package/references/cadence-ledger.md +1 -1
- package/references/ci-cd-standard.md +1 -1
- package/references/delivery-operating-model-standard.md +4 -4
- package/references/docs-directory-standard.md +65 -159
- package/references/external-source-intake-standard.md +10 -10
- package/references/harness-ledger.md +5 -5
- package/references/legacy-12-phase-bootstrap.md +2 -2
- package/references/lessons-governance.md +15 -15
- package/references/long-running-task-standard.md +6 -6
- package/references/module-parallel-standard.md +34 -34
- package/references/planning-loop.md +6 -6
- package/references/project-onboarding-audit.md +4 -4
- package/references/regression-system.md +2 -2
- package/references/repo-governance-standard.md +4 -4
- package/references/review-routing-standard.md +1 -1
- package/references/ssot-governance.md +19 -19
- package/references/taskr-gap-analysis.md +5 -5
- package/references/walkthrough-closeout.md +14 -14
- package/references/worktree-parallel.md +3 -3
- package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +1 -1
- package/skills/preset-creator/references/preset-package-skeleton.md +5 -5
- package/templates/AGENTS.md.template +26 -26
- package/templates/architecture/README.md +4 -4
- package/templates/architecture/service-catalog.md +2 -2
- package/templates/architecture/services/service-template.md +1 -1
- package/templates/dashboard/assets/app-src/20-overview.js +11 -5
- package/templates/dashboard/assets/app-src/40-modules.js +1 -1
- package/templates/dashboard/assets/app.js +12 -6
- package/templates/dashboard/assets/i18n.js +4 -2
- package/templates/development/README.md +10 -10
- package/templates/development/cross-repo-debugging.md +3 -3
- package/templates/development/external-context/service-template.md +2 -2
- package/templates/development/external-source-packs/README.md +4 -4
- package/templates/integrations/README.md +4 -4
- package/templates/integrations/api-contract.md +2 -2
- package/templates/integrations/event-contract.md +2 -2
- package/templates/integrations/third-party/vendor-template.md +2 -2
- package/templates/integrations/webhook-contract.md +2 -2
- package/templates/ledger/Harness-Ledger.md +1 -1
- package/templates/planning/INDEX.md +1 -0
- package/templates/planning/module_session_prompt.md +1 -1
- package/templates/planning/task_plan.md +1 -1
- package/templates/planning/walkthrough.md +47 -0
- package/templates/reference/docs-library-standard.md +8 -8
- package/templates/reference/external-source-intake-standard.md +15 -15
- package/templates/reference/repo-governance-standard.md +1 -1
- package/templates/ssot/Module-Registry.md +1 -1
- package/templates/walkthrough/walkthrough-template.md +2 -2
- package/templates-zh-CN/AGENTS.md.template +26 -26
- package/templates-zh-CN/CLAUDE.md.template +1 -1
- package/templates-zh-CN/architecture/README.md +4 -4
- package/templates-zh-CN/architecture/service-catalog.md +2 -2
- package/templates-zh-CN/architecture/services/service-template.md +1 -1
- package/templates-zh-CN/development/README.md +10 -10
- package/templates-zh-CN/development/cross-repo-debugging.md +3 -3
- package/templates-zh-CN/development/external-context/service-template.md +2 -2
- package/templates-zh-CN/development/external-source-packs/README.md +4 -4
- package/templates-zh-CN/integrations/README.md +4 -4
- package/templates-zh-CN/integrations/api-contract.md +2 -2
- package/templates-zh-CN/integrations/event-contract.md +2 -2
- package/templates-zh-CN/integrations/third-party/vendor-template.md +2 -2
- package/templates-zh-CN/integrations/webhook-contract.md +2 -2
- package/templates-zh-CN/ledger/Harness-Ledger.md +1 -1
- package/templates-zh-CN/lessons/lesson-arch-process-change.md +1 -1
- package/templates-zh-CN/lessons/lesson-new-doc.md +3 -3
- package/templates-zh-CN/lessons/lesson-ref-change.md +4 -4
- package/templates-zh-CN/planning/module_session_prompt.md +11 -11
- package/templates-zh-CN/planning/walkthrough.md +47 -0
- package/templates-zh-CN/reference/adversarial-review-standard.md +2 -2
- package/templates-zh-CN/reference/delivery-operating-model-standard.md +3 -3
- package/templates-zh-CN/reference/docs-library-standard.md +28 -28
- package/templates-zh-CN/reference/execution-workflow-standard.md +1 -1
- package/templates-zh-CN/reference/external-source-intake-standard.md +16 -16
- package/templates-zh-CN/reference/harness-ledger-standard.md +6 -6
- package/templates-zh-CN/reference/regression-ssot-governance.md +2 -2
- package/templates-zh-CN/reference/repo-governance-standard.md +1 -1
- package/templates-zh-CN/reference/review-routing-standard.md +1 -1
- package/templates-zh-CN/reference/walkthrough-standard.md +7 -7
- package/templates-zh-CN/reference/worktree-standard.md +1 -1
- package/templates-zh-CN/regression/Cadence-Ledger.md +2 -2
- package/templates-zh-CN/ssot/Delivery-SSoT.md +3 -3
- package/templates-zh-CN/ssot/Module-Registry.md +3 -3
- package/templates-zh-CN/ssot/Regression-SSoT.md +2 -2
- package/templates-zh-CN/walkthrough/walkthrough-template.md +5 -5
- package/tsconfig.dist.json +16 -0
- package/tsconfig.json +25 -0
- package/tsconfig.runtime.json +24 -0
- package/examples/minimal-project/.harness-capabilities.json +0 -8
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/progress.md +0 -11
- package/scripts/check-harness.mjs +0 -508
- package/scripts/commands/dashboard-command.mjs +0 -67
- package/scripts/commands/migration-command.mjs +0 -126
- package/scripts/commands/preset-command.mjs +0 -73
- package/scripts/commands/task-command.mjs +0 -328
- package/scripts/harness.mjs +0 -291
- package/scripts/lib/capability-registry.mjs +0 -587
- package/scripts/lib/check-module-parallel.mjs +0 -230
- package/scripts/lib/check-profiles.mjs +0 -372
- package/scripts/lib/check-task-contracts.mjs +0 -55
- package/scripts/lib/core-shared.mjs +0 -249
- package/scripts/lib/dashboard-data.mjs +0 -520
- package/scripts/lib/dashboard-workbench.mjs +0 -336
- package/scripts/lib/dashboard-writer.mjs +0 -202
- package/scripts/lib/git-status-summary.mjs +0 -46
- package/scripts/lib/governance-index-generator.mjs +0 -174
- package/scripts/lib/governance-sync.mjs +0 -611
- package/scripts/lib/governance-table-boundary.mjs +0 -175
- package/scripts/lib/lesson-maintenance.mjs +0 -152
- package/scripts/lib/markdown-utils.mjs +0 -191
- package/scripts/lib/migration-planner.mjs +0 -476
- package/scripts/lib/migration-support.mjs +0 -312
- package/scripts/lib/phase-kind.mjs +0 -50
- package/scripts/lib/preset-audit-contracts.mjs +0 -37
- package/scripts/lib/preset-engine.mjs +0 -494
- package/scripts/lib/preset-registry.mjs +0 -776
- package/scripts/lib/preset-resource-contracts.mjs +0 -83
- package/scripts/lib/review-confirm-git-gate.mjs +0 -248
- package/scripts/lib/status-builder.mjs +0 -88
- package/scripts/lib/subagent-authorization-audit.mjs +0 -196
- package/scripts/lib/task-audit-metadata.mjs +0 -385
- package/scripts/lib/task-audit-migration.mjs +0 -350
- package/scripts/lib/task-completion-consistency.mjs +0 -26
- package/scripts/lib/task-index.mjs +0 -93
- package/scripts/lib/task-lesson-candidates.mjs +0 -242
- package/scripts/lib/task-lesson-sedimentation.mjs +0 -326
- package/scripts/lib/task-lifecycle/create-task-helpers.mjs +0 -67
- package/scripts/lib/task-lifecycle/phase-sync.mjs +0 -88
- package/scripts/lib/task-lifecycle/review-confirm.mjs +0 -112
- package/scripts/lib/task-lifecycle/review-gates.mjs +0 -73
- package/scripts/lib/task-lifecycle/review-submission.mjs +0 -63
- package/scripts/lib/task-lifecycle/scaffold-provenance.mjs +0 -49
- package/scripts/lib/task-lifecycle/template-files.mjs +0 -53
- package/scripts/lib/task-lifecycle/text-utils.mjs +0 -24
- package/scripts/lib/task-lifecycle.mjs +0 -616
- package/scripts/lib/task-metadata.mjs +0 -118
- package/scripts/lib/task-review-model.mjs +0 -455
- package/scripts/lib/task-scanner.mjs +0 -503
- package/scripts/lib/task-tombstone-commands.mjs +0 -140
- package/scripts/postinstall.mjs +0 -14
- package/templates/walkthrough/Closeout-SSoT.md +0 -43
- package/templates-zh-CN/walkthrough/Closeout-SSoT.md +0 -42
- /package/examples/minimal-project/{docs → coding-agent-harness/governance/generated}/Harness-Ledger.md +0 -0
- /package/examples/minimal-project/{docs/09-PLANNING/TASKS → coding-agent-harness/planning/tasks}/demo-task/INDEX.md +0 -0
- /package/examples/minimal-project/{docs/09-PLANNING/TASKS → coding-agent-harness/planning/tasks}/demo-task/brief.md +0 -0
- /package/examples/minimal-project/{docs/09-PLANNING/TASKS → coding-agent-harness/planning/tasks}/demo-task/execution_strategy.md +0 -0
- /package/examples/minimal-project/{docs/09-PLANNING/TASKS → coding-agent-harness/planning/tasks}/demo-task/findings.md +0 -0
- /package/examples/minimal-project/{docs/09-PLANNING/TASKS → coding-agent-harness/planning/tasks}/demo-task/lesson_candidates.md +0 -0
- /package/examples/minimal-project/{docs/09-PLANNING/TASKS → coding-agent-harness/planning/tasks}/demo-task/task_plan.md +0 -0
- /package/examples/minimal-project/{docs/09-PLANNING/TASKS → coding-agent-harness/planning/tasks}/demo-task/visual_map.md +0 -0
|
@@ -1,611 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import crypto from "node:crypto";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
|
-
import { readBundledTemplate, readFileSafe, readJsonSafe, repoRoot, todayDate, toPosix, visualMapFile } from "./core-shared.mjs";
|
|
7
|
-
import { collectTasks } from "./task-scanner.mjs";
|
|
8
|
-
import { appendMarkdownTableRow, firstColumn, fitMarkdownTableRow, splitMarkdownRow, upsertMarkdownTableRow } from "./markdown-utils.mjs";
|
|
9
|
-
|
|
10
|
-
export class GovernanceSyncError extends Error {
|
|
11
|
-
constructor(message, { code = "governance-sync-failed", details = {}, recovery = [] } = {}) {
|
|
12
|
-
super(message);
|
|
13
|
-
this.name = "GovernanceSyncError";
|
|
14
|
-
this.code = code;
|
|
15
|
-
this.details = details;
|
|
16
|
-
this.recovery = recovery;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function beginGovernanceSync(target, { operation = "governance-sync", dryRun = false, allowDirtyWorktree = false, allowedRelativePaths = [] } = {}) {
|
|
21
|
-
if (dryRun) return { target, dryRun, operation, git: inspectGit(target.projectRoot), lockPath: "", active: false };
|
|
22
|
-
const lockPath = path.join(target.projectRoot, ".harness/locks/governance-sync.lock");
|
|
23
|
-
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
24
|
-
acquireGovernanceSyncLock(lockPath, target, { operation });
|
|
25
|
-
|
|
26
|
-
const gitState = inspectGit(target.projectRoot);
|
|
27
|
-
const allowed = [...new Set((allowedRelativePaths || []).filter(Boolean).map(toPosix))].sort();
|
|
28
|
-
if (gitState.inGit) {
|
|
29
|
-
if (real(gitState.gitRoot) !== real(target.projectRoot)) {
|
|
30
|
-
releaseGovernanceSync({ lockPath, active: true });
|
|
31
|
-
throw new GovernanceSyncError("Governance sync requires the target argument to be the Git repository root.", {
|
|
32
|
-
code: "governance-git-root-mismatch",
|
|
33
|
-
details: { targetRoot: target.projectRoot, gitRoot: gitState.gitRoot },
|
|
34
|
-
recovery: ["Run the harness command against the target repository root."],
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
if (gitState.entries.length > 0 && !allowDirtyWorktree) {
|
|
38
|
-
releaseGovernanceSync({ lockPath, active: true });
|
|
39
|
-
throw new GovernanceSyncError("Governance sync requires a clean Git working tree before CLI-owned writes.", {
|
|
40
|
-
code: "governance-git-dirty",
|
|
41
|
-
details: { entries: gitState.entries },
|
|
42
|
-
recovery: ["Commit or otherwise resolve unrelated changes before running this lifecycle command."],
|
|
43
|
-
});
|
|
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
|
-
}
|
|
53
|
-
assertCommitIdentity(target.projectRoot);
|
|
54
|
-
}
|
|
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
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function releaseGovernanceSync(context) {
|
|
121
|
-
if (!context?.active || !context.lockPath) return;
|
|
122
|
-
try {
|
|
123
|
-
fs.unlinkSync(context.lockPath);
|
|
124
|
-
} catch {
|
|
125
|
-
// Best-effort cleanup; command errors report the original failure.
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function commitGovernanceSync(context, allowedRelativePaths, { message = "chore(harness): sync governance state" } = {}) {
|
|
130
|
-
const allowed = [...new Set((allowedRelativePaths || []).filter(Boolean).map(toPosix))].sort();
|
|
131
|
-
if (context?.dryRun || !context?.git?.inGit) return { committed: false, reason: context?.git?.inGit ? "dry-run" : "not-git", allowedPaths: allowed };
|
|
132
|
-
if (allowed.length === 0) return { committed: false, reason: "no-allowed-paths", allowedPaths: allowed };
|
|
133
|
-
assertNoUnexpectedOutsideChanges(context.target.projectRoot, allowed, context.initialDirtyEntries || []);
|
|
134
|
-
git(context.target.projectRoot, ["add", "--", ...allowed]);
|
|
135
|
-
assertOnlyAllowedStaged(context.target.projectRoot, allowed);
|
|
136
|
-
const staged = git(context.target.projectRoot, ["diff", "--cached", "--name-only", "-z"]).stdout.split("\0").filter(Boolean);
|
|
137
|
-
if (staged.length === 0) return { committed: false, reason: "no-changes", allowedPaths: allowed };
|
|
138
|
-
const commitResult = git(context.target.projectRoot, ["-c", "core.hooksPath=/dev/null", "commit", "--no-verify", "-m", message], { allowFailure: true });
|
|
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
|
-
}
|
|
146
|
-
throw new GovernanceSyncError("Governance sync wrote files but Git commit failed.", {
|
|
147
|
-
code: "governance-git-commit-failed",
|
|
148
|
-
details: { stdout: commitResult.stdout.trim(), stderr: commitResult.stderr.trim(), allowedPaths: allowed, outsideChanges },
|
|
149
|
-
recovery: [
|
|
150
|
-
`Inspect files: ${allowed.join(", ")}`,
|
|
151
|
-
`Then run: git add -- ${allowed.join(" ")} && git -c core.hooksPath=/dev/null commit --no-verify -m ${JSON.stringify(message)}`,
|
|
152
|
-
],
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
assertLastCommitOnlyAllowed(context.target.projectRoot, allowed);
|
|
156
|
-
assertNoUnexpectedOutsideChanges(context.target.projectRoot, allowed, context.initialDirtyEntries || []);
|
|
157
|
-
assertWriteScopeClean(context.target.projectRoot, allowed);
|
|
158
|
-
return { committed: true, commitSha: git(context.target.projectRoot, ["rev-parse", "HEAD"]).stdout.trim(), allowedPaths: allowed };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function syncTaskGovernance(target, task, { event = "new-task", state = "planned", message = "", dryRun = false } = {}) {
|
|
162
|
-
const changes = [];
|
|
163
|
-
const planPath = stripTargetPrefix(task.path) + "/task_plan.md";
|
|
164
|
-
const reviewPath = stripTargetPrefix(task.path) + "/review.md";
|
|
165
|
-
const ledger = syncLedgerRow(target, task, { event, state, message, planPath, reviewPath, dryRun });
|
|
166
|
-
if (ledger) changes.push(ledger);
|
|
167
|
-
if (task.module) {
|
|
168
|
-
const moduleRegistry = syncModuleRegistryRow(target, task, { state, planPath, dryRun });
|
|
169
|
-
if (moduleRegistry) changes.push(moduleRegistry);
|
|
170
|
-
changes.push(...syncModuleGeneratedIndexes(target, task.module, { task, dryRun }).changes);
|
|
171
|
-
}
|
|
172
|
-
return { changes };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function syncModuleStepGovernance(target, { moduleKey, stepId, state, dryRun = false } = {}) {
|
|
176
|
-
const changes = [];
|
|
177
|
-
const ledgerPath = path.join(target.docsRoot, "Harness-Ledger.md");
|
|
178
|
-
const ledgerRelative = toPosix(path.relative(target.projectRoot, ledgerPath));
|
|
179
|
-
ensureFileFromTemplate(ledgerPath, "templates/ledger/Harness-Ledger.md", { dryRun });
|
|
180
|
-
if (!dryRun) {
|
|
181
|
-
const content = readFileSafe(ledgerPath);
|
|
182
|
-
const row = [
|
|
183
|
-
`HL-${todayDate().replaceAll("-", "")}-${Date.now().toString().slice(-6)}`,
|
|
184
|
-
"module",
|
|
185
|
-
moduleKey,
|
|
186
|
-
`Module ${moduleKey} step ${stepId}`,
|
|
187
|
-
state === "done" ? "review" : state === "in-progress" ? "active" : state,
|
|
188
|
-
"none",
|
|
189
|
-
`docs/09-PLANNING/MODULES/${moduleKey}/module_plan.md`,
|
|
190
|
-
"n/a",
|
|
191
|
-
"checked-none:module-step",
|
|
192
|
-
"pending",
|
|
193
|
-
"module-step",
|
|
194
|
-
todayDate(),
|
|
195
|
-
];
|
|
196
|
-
fs.writeFileSync(ledgerPath, appendMarkdownTableRow(content, /^ID$/i, row));
|
|
197
|
-
}
|
|
198
|
-
changes.push({ destination: ledgerRelative, action: dryRun ? "would-sync-governance" : "sync-governance", surface: "harness-ledger" });
|
|
199
|
-
return { changes };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export function governanceRelativePaths(changes) {
|
|
203
|
-
return [...new Set((changes || []).map((change) => change.destination).filter(Boolean).map(toPosix))];
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function syncLedgerRow(target, task, { event, state, message, planPath, reviewPath, dryRun }) {
|
|
207
|
-
const ledgerPath = path.join(target.docsRoot, "Harness-Ledger.md");
|
|
208
|
-
ensureFileFromTemplate(ledgerPath, "templates/ledger/Harness-Ledger.md", { dryRun });
|
|
209
|
-
const relative = toPosix(path.relative(target.projectRoot, ledgerPath));
|
|
210
|
-
if (!dryRun) {
|
|
211
|
-
const content = readFileSafe(ledgerPath);
|
|
212
|
-
const row = [
|
|
213
|
-
ledgerId(task),
|
|
214
|
-
task.module ? "module" : "task",
|
|
215
|
-
task.module || "none",
|
|
216
|
-
task.title || task.shortId || task.id,
|
|
217
|
-
mapLedgerState(state),
|
|
218
|
-
"none",
|
|
219
|
-
planPath,
|
|
220
|
-
event === "task-review" || state === "review" ? reviewPath : "pending",
|
|
221
|
-
"pending",
|
|
222
|
-
"pending",
|
|
223
|
-
message || "none",
|
|
224
|
-
todayDate(),
|
|
225
|
-
];
|
|
226
|
-
fs.writeFileSync(ledgerPath, upsertMarkdownTableRow(content, /^ID$/i, (header, existing) => rowMatchesPlan(header, existing, planPath), row));
|
|
227
|
-
}
|
|
228
|
-
return { destination: relative, action: dryRun ? "would-sync-governance" : "sync-governance", surface: "harness-ledger" };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function syncModuleRegistryRow(target, task, { state, planPath, dryRun }) {
|
|
232
|
-
const registryPath = path.join(target.docsRoot, "09-PLANNING/Module-Registry.md");
|
|
233
|
-
ensureFileFromTemplate(registryPath, "templates/ssot/Module-Registry.md", { dryRun });
|
|
234
|
-
const relative = toPosix(path.relative(target.projectRoot, registryPath));
|
|
235
|
-
if (!dryRun) {
|
|
236
|
-
const content = readFileSafe(registryPath);
|
|
237
|
-
const moduleKey = task.module;
|
|
238
|
-
const modulePlan = `docs/09-PLANNING/MODULES/${moduleKey}/module_plan.md`;
|
|
239
|
-
const row = [
|
|
240
|
-
`M-${moduleKey.toUpperCase().replace(/[^A-Z0-9]+/g, "-")}`,
|
|
241
|
-
moduleKey,
|
|
242
|
-
`docs/09-PLANNING/MODULES/${moduleKey}/**`,
|
|
243
|
-
"coordinator",
|
|
244
|
-
state === "planned" ? "reserved" : mapModuleState(state),
|
|
245
|
-
`codex/${moduleKey}`,
|
|
246
|
-
modulePlan,
|
|
247
|
-
"none",
|
|
248
|
-
"none",
|
|
249
|
-
planPath,
|
|
250
|
-
"none",
|
|
251
|
-
todayDate(),
|
|
252
|
-
];
|
|
253
|
-
fs.writeFileSync(registryPath, upsertMarkdownTableRow(content, /^ID$/i, (header, existing) => rowMatchesModule(header, existing, moduleKey, modulePlan), row));
|
|
254
|
-
}
|
|
255
|
-
return { destination: relative, action: dryRun ? "would-sync-governance" : "sync-governance", surface: "module-registry" };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function syncModuleGeneratedIndexes(target, moduleKey, { task = null, dryRun = false } = {}) {
|
|
259
|
-
const moduleTasks = collectModuleTasks(target, moduleKey, task);
|
|
260
|
-
const surfaces = moduleGeneratedIndexSurfaces(target, moduleTasks);
|
|
261
|
-
if (!dryRun) {
|
|
262
|
-
for (const surface of surfaces) {
|
|
263
|
-
fs.mkdirSync(path.dirname(surface.absolute), { recursive: true });
|
|
264
|
-
fs.writeFileSync(surface.absolute, surface.content);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return {
|
|
268
|
-
changes: surfaces.map((surface) => ({
|
|
269
|
-
destination: surface.relative,
|
|
270
|
-
action: dryRun ? "would-sync-governance" : "sync-governance",
|
|
271
|
-
surface: surface.surface,
|
|
272
|
-
})),
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export function moduleGeneratedIndexSurfaces(target, tasks = collectTasks(target)) {
|
|
277
|
-
const modules = [...new Set((tasks || []).map((task) => task.module).filter(Boolean))].sort();
|
|
278
|
-
const surfaces = [];
|
|
279
|
-
for (const moduleKey of modules) {
|
|
280
|
-
const moduleTasks = (tasks || [])
|
|
281
|
-
.filter((task) => task.module === moduleKey)
|
|
282
|
-
.sort((a, b) => String(stripDatePrefix(a.shortId || a.id)).localeCompare(String(stripDatePrefix(b.shortId || b.id))));
|
|
283
|
-
const moduleDir = path.join(target.docsRoot, "09-PLANNING/MODULES", moduleKey);
|
|
284
|
-
const modulePlanPath = path.join(moduleDir, "module_plan.md");
|
|
285
|
-
const moduleVisualPath = path.join(moduleDir, visualMapFile);
|
|
286
|
-
const stepRows = moduleTasks.map((task, index) => {
|
|
287
|
-
const stepId = moduleStepId(task);
|
|
288
|
-
const previous = index === 0 ? "none" : moduleStepId(moduleTasks[index - 1]);
|
|
289
|
-
return [stepId, task.title || task.shortId || task.id, mapModuleState(task.state), stripTargetPrefix(task.taskPlanPath || `${stripTargetPrefix(task.path)}/task_plan.md`), previous];
|
|
290
|
-
});
|
|
291
|
-
surfaces.push({
|
|
292
|
-
surface: "module-plan-index",
|
|
293
|
-
absolute: modulePlanPath,
|
|
294
|
-
relative: toPosix(path.relative(target.projectRoot, modulePlanPath)),
|
|
295
|
-
rows: stepRows,
|
|
296
|
-
content: replaceTableRows(existingOrTemplate(modulePlanPath, "templates/planning/module_plan.md"), /^Step ID$/i, stepRows),
|
|
297
|
-
});
|
|
298
|
-
surfaces.push({
|
|
299
|
-
surface: "module-visual-index",
|
|
300
|
-
absolute: moduleVisualPath,
|
|
301
|
-
relative: toPosix(path.relative(target.projectRoot, moduleVisualPath)),
|
|
302
|
-
rows: stepRows,
|
|
303
|
-
content: renderModuleVisualMap(moduleKey, moduleTasks),
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
return surfaces;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function collectModuleTasks(target, moduleKey, task) {
|
|
310
|
-
const tasks = collectTasks(target).filter((candidate) => candidate.module === moduleKey);
|
|
311
|
-
if (task && !tasks.some((candidate) => stripTargetPrefix(candidate.taskPlanPath) === `${stripTargetPrefix(task.path)}/task_plan.md`)) {
|
|
312
|
-
tasks.push({
|
|
313
|
-
...task,
|
|
314
|
-
module: moduleKey,
|
|
315
|
-
state: task.state || "planned",
|
|
316
|
-
taskPlanPath: `${stripTargetPrefix(task.path)}/task_plan.md`,
|
|
317
|
-
completion: 0,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
return tasks;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function renderModuleVisualMap(moduleKey, tasks) {
|
|
324
|
-
const rows = tasks.map((task, index) => {
|
|
325
|
-
const stepId = moduleStepId(task);
|
|
326
|
-
const previous = index === 0 ? "none" : moduleStepId(tasks[index - 1]);
|
|
327
|
-
const state = mapPhaseState(task.state);
|
|
328
|
-
const completion = Number.isInteger(task.completion) ? task.completion : state === "done" ? 100 : 0;
|
|
329
|
-
return [
|
|
330
|
-
stepId,
|
|
331
|
-
previous,
|
|
332
|
-
state,
|
|
333
|
-
completion,
|
|
334
|
-
task.title || task.shortId || task.id,
|
|
335
|
-
stripTargetPrefix(task.taskPlanPath || `${stripTargetPrefix(task.path)}/task_plan.md`),
|
|
336
|
-
task.materialsReady ? "present" : "missing",
|
|
337
|
-
previous === "none" ? "none" : `depends on ${previous}`,
|
|
338
|
-
"coordinator",
|
|
339
|
-
];
|
|
340
|
-
});
|
|
341
|
-
const graphLines = tasks.map((task, index) => {
|
|
342
|
-
const stepId = moduleStepId(task);
|
|
343
|
-
const label = fitMarkdownTableRow([task.title || task.shortId || task.id], 1)[0].replace(/"/g, "'");
|
|
344
|
-
if (index === 0) return ` ${stepId}["${label}"]`;
|
|
345
|
-
const previous = moduleStepId(tasks[index - 1]);
|
|
346
|
-
return ` ${previous} --> ${stepId}["${label}"]`;
|
|
347
|
-
});
|
|
348
|
-
return `# ${moduleKey} - Visual Map
|
|
349
|
-
|
|
350
|
-
Visual Map Contract: v1.0
|
|
351
|
-
|
|
352
|
-
Generated by \`harness new-task --module\` and \`harness governance rebuild\`.
|
|
353
|
-
|
|
354
|
-
## Map Index
|
|
355
|
-
|
|
356
|
-
| ID | Type | Purpose | Required For Understanding | Source Evidence | Promotion Candidate |
|
|
357
|
-
| --- | --- | --- | --- | --- | --- |
|
|
358
|
-
| MAP-01 | topology | Show module task sequence generated from task files | yes | task scan | no |
|
|
359
|
-
|
|
360
|
-
## Phase Graph
|
|
361
|
-
|
|
362
|
-
\`\`\`mermaid
|
|
363
|
-
flowchart LR
|
|
364
|
-
${graphLines.length ? graphLines.join("\n") : " EMPTY[\"No module tasks\"]"}
|
|
365
|
-
\`\`\`
|
|
366
|
-
|
|
367
|
-
## Phase Table
|
|
368
|
-
|
|
369
|
-
| Phase ID | Depends On | State | Completion | Output | Required Evidence | Evidence Status | Blocking Risk | Owner / Handoff |
|
|
370
|
-
| --- | --- | --- | ---: | --- | --- | --- | --- | --- |
|
|
371
|
-
${rows.map((row) => `| ${fitMarkdownTableRow(row, 9).join(" | ")} |`).join("\n")}
|
|
372
|
-
|
|
373
|
-
Allowed Evidence Status: missing, partial, present, waived.
|
|
374
|
-
`;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function replaceTableRows(content, headerPattern, rows) {
|
|
378
|
-
const lines = String(content || "").split(/\r?\n/);
|
|
379
|
-
for (let index = 0; index < lines.length - 1; index += 1) {
|
|
380
|
-
if (!lines[index].trim().startsWith("|")) continue;
|
|
381
|
-
const header = splitMarkdownRow(lines[index]);
|
|
382
|
-
if (!header.some((cell) => headerPattern.test(cell))) continue;
|
|
383
|
-
const separator = splitMarkdownRow(lines[index + 1]);
|
|
384
|
-
if (!separator.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
|
|
385
|
-
let end = index + 2;
|
|
386
|
-
while (end < lines.length && lines[end].trim().startsWith("|")) end += 1;
|
|
387
|
-
lines.splice(index + 2, end - index - 2, ...rows.map((row) => `| ${fitMarkdownTableRow(row, header.length).join(" | ")} |`));
|
|
388
|
-
return `${lines.join("\n").trimEnd()}\n`;
|
|
389
|
-
}
|
|
390
|
-
return `${String(content || "").trimEnd()}\n\n${rows.map((row) => `| ${fitMarkdownTableRow(row, row.length).join(" | ")} |`).join("\n")}\n`;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function existingOrTemplate(filePath, templateSource) {
|
|
394
|
-
return fs.existsSync(filePath) ? readFileSafe(filePath) : readBundledTemplate(templateSource);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function moduleStepId(task) {
|
|
398
|
-
return `T-${stripDatePrefix(task.shortId || task.id || "task").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-|-$/g, "").toUpperCase().slice(0, 48)}`;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function stripDatePrefix(value) {
|
|
402
|
-
return String(value || "").replace(/^(?:TASKS\/|MODULES\/[^/]+\/)?\d{4}-\d{2}-\d{2}-/, "");
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function mapPhaseState(state) {
|
|
406
|
-
if (state === "in_progress") return "in_progress";
|
|
407
|
-
if (state === "review") return "review";
|
|
408
|
-
if (state === "done") return "done";
|
|
409
|
-
if (state === "blocked") return "blocked";
|
|
410
|
-
return "planned";
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function ensureFileFromTemplate(destinationPath, templateSource, { dryRun = false } = {}) {
|
|
414
|
-
if (fs.existsSync(destinationPath) || dryRun) return;
|
|
415
|
-
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
416
|
-
fs.writeFileSync(destinationPath, readBundledTemplate(templateSource));
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function rowMatchesPlan(header, row, planPath) {
|
|
420
|
-
const planIndex = firstColumn(header, ["Task Plan", "Plan", "当前产物"]);
|
|
421
|
-
return planIndex >= 0 && String(row[planIndex] || "").includes(planPath);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function rowMatchesModule(header, row, moduleKey, modulePlan) {
|
|
425
|
-
const moduleIndex = firstColumn(header, ["Module", "模块", "模块 Key"]);
|
|
426
|
-
const taskPlanIndex = firstColumn(header, ["Task Plan", "当前产物"]);
|
|
427
|
-
return String(row[moduleIndex] || "").toLowerCase() === String(moduleKey).toLowerCase() || String(row[taskPlanIndex] || "").includes(modulePlan);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function ledgerId(task) {
|
|
431
|
-
return `HL-${String(task.shortId || task.id || "task").replace(/^TASKS\//, "").replace(/^MODULES\//, "").replace(/[^A-Za-z0-9-]+/g, "-").slice(0, 72)}`;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function stripTargetPrefix(value) {
|
|
435
|
-
return String(value || "").replace(/^TARGET:/, "").replace(/\/$/, "");
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function mapLedgerState(state) {
|
|
439
|
-
if (state === "in_progress") return "active";
|
|
440
|
-
if (state === "review") return "review";
|
|
441
|
-
if (state === "done") return "closed";
|
|
442
|
-
if (state === "blocked") return "blocked";
|
|
443
|
-
return "planned";
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function mapModuleState(state) {
|
|
447
|
-
if (state === "in_progress") return "active";
|
|
448
|
-
if (state === "review") return "handoff";
|
|
449
|
-
if (state === "done") return "merged";
|
|
450
|
-
if (state === "blocked") return "blocked";
|
|
451
|
-
return "reserved";
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
export function inspectGit(root) {
|
|
455
|
-
const gitRootResult = git(root, ["rev-parse", "--show-toplevel"], { allowFailure: true });
|
|
456
|
-
if (gitRootResult.status !== 0) return { inGit: false, gitRoot: "", entries: [] };
|
|
457
|
-
const gitRoot = path.resolve(gitRootResult.stdout.trim());
|
|
458
|
-
return { inGit: true, gitRoot, entries: statusEntries(root) };
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function currentBranch(root) {
|
|
462
|
-
const result = git(root, ["branch", "--show-current"], { allowFailure: true });
|
|
463
|
-
return result.status === 0 ? result.stdout.trim() : "";
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function assertCommitIdentity(root) {
|
|
467
|
-
const name = git(root, ["config", "--get", "user.name"], { allowFailure: true }).stdout.trim();
|
|
468
|
-
const email = git(root, ["config", "--get", "user.email"], { allowFailure: true }).stdout.trim();
|
|
469
|
-
if (!name || !email) {
|
|
470
|
-
throw new GovernanceSyncError("Governance sync auto-commit requires Git user.name and user.email.", {
|
|
471
|
-
code: "governance-git-identity-missing",
|
|
472
|
-
details: { hasName: Boolean(name), hasEmail: Boolean(email) },
|
|
473
|
-
recovery: ["Configure a local Git identity for the target repository."],
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
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."],
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function assertOnlyAllowedStaged(root, allowedPaths) {
|
|
499
|
-
const outside = statusEntries(root).filter((entry) => entry.index !== " " && entry.index !== "?" && !allowedPaths.includes(entry.path));
|
|
500
|
-
if (outside.length > 0) {
|
|
501
|
-
throw new GovernanceSyncError("Git index contains staged files outside the governance sync allowlist.", {
|
|
502
|
-
code: "governance-index-allowlist-violation",
|
|
503
|
-
details: { disallowed: outside, allowedPaths },
|
|
504
|
-
recovery: ["Unstage unrelated files before retrying the lifecycle command."],
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
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) {
|
|
553
|
-
const entries = statusEntries(root);
|
|
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.", {
|
|
557
|
-
code: "governance-post-commit-dirty",
|
|
558
|
-
details: { entries: remaining, allowedPaths },
|
|
559
|
-
recovery: ["Inspect remaining write-scope files before continuing."],
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
}
|
|
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
|
-
|
|
579
|
-
function statusEntries(root) {
|
|
580
|
-
return git(root, ["status", "--porcelain=v1", "--untracked-files=all"]).stdout
|
|
581
|
-
.split(/\r?\n/)
|
|
582
|
-
.filter(Boolean)
|
|
583
|
-
.map((line) => ({
|
|
584
|
-
index: line.slice(0, 1),
|
|
585
|
-
worktree: line.slice(1, 2),
|
|
586
|
-
path: toPosix(parseStatusPath(line.slice(3))),
|
|
587
|
-
raw: line,
|
|
588
|
-
}))
|
|
589
|
-
.filter((entry) => entry.path !== ".harness/locks/governance-sync.lock");
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function parseStatusPath(value) {
|
|
593
|
-
const unquoted = value.replace(/^"|"$/g, "");
|
|
594
|
-
return unquoted.includes(" -> ") ? unquoted.split(" -> ").pop() : unquoted;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function git(cwd, args, { allowFailure = false } = {}) {
|
|
598
|
-
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
599
|
-
if (!allowFailure && result.status !== 0) {
|
|
600
|
-
throw new GovernanceSyncError(`git ${args.join(" ")} failed`, {
|
|
601
|
-
code: "governance-git-command-failed",
|
|
602
|
-
details: { stdout: result.stdout.trim(), stderr: result.stderr.trim() },
|
|
603
|
-
recovery: ["Inspect the Git error and retry after resolving it."],
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
return result;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
function real(filePath) {
|
|
610
|
-
return fs.realpathSync(filePath);
|
|
611
|
-
}
|