coding-agent-harness 1.0.7 → 1.1.0
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 +33 -0
- package/CONTRIBUTING.md +9 -5
- package/README.md +12 -2
- package/README.zh-CN.md +10 -2
- package/SKILL.md +14 -3
- package/dist/build-dist.mjs +32 -6
- package/dist/check-dist-observation.mjs +73 -28
- package/dist/check-harness.mjs +0 -1
- package/dist/check-import-graph.mjs +44 -27
- package/dist/check-lite-forbidden-surfaces.mjs +121 -0
- package/dist/check-no-ts-nocheck.mjs +88 -0
- package/dist/check-runtime-emit.mjs +10 -3
- package/dist/check-type-boundaries.mjs +67 -8
- package/dist/commands/dashboard-command.mjs +52 -14
- package/dist/commands/migration-command.mjs +18 -8
- package/dist/commands/module-command.mjs +142 -0
- package/dist/commands/preset-command.mjs +65 -4
- package/dist/commands/registry.mjs +483 -0
- package/dist/commands/task-command.mjs +111 -53
- package/dist/harness.mjs +6 -303
- package/dist/lib/capability-registry.mjs +229 -53
- package/dist/lib/check-module-parallel.mjs +1 -6
- package/dist/lib/check-profiles.mjs +39 -46
- package/dist/lib/check-task-contracts.mjs +6 -4
- package/dist/lib/command-registry.mjs +248 -0
- package/dist/lib/core-shared.mjs +78 -3
- package/dist/lib/dashboard-data.mjs +203 -22
- package/dist/lib/dashboard-workbench.mjs +245 -21
- package/dist/lib/dashboard-writer.mjs +4 -1
- package/dist/lib/git-status-summary.mjs +0 -1
- package/dist/lib/governance-index-generator.mjs +7 -5
- package/dist/lib/governance-sync.mjs +46 -121
- package/dist/lib/governance-table-boundary.mjs +1 -14
- package/dist/lib/harness-core.mjs +5 -1
- package/dist/lib/harness-paths.mjs +115 -1
- package/dist/lib/impact-classifier.mjs +420 -0
- package/dist/lib/lesson-maintenance.mjs +1 -2
- package/dist/lib/markdown-utils.mjs +50 -1
- package/dist/lib/migration-planner.mjs +31 -16
- package/dist/lib/migration-support.mjs +5 -4
- package/dist/lib/module-registry.mjs +296 -0
- package/dist/lib/preset-audit-contracts.mjs +24 -1
- package/dist/lib/preset-engine.mjs +68 -29
- package/dist/lib/preset-registry.mjs +374 -72
- package/dist/lib/preset-runner.mjs +560 -0
- package/dist/lib/review-confirm-git-gate.mjs +73 -19
- package/dist/lib/status-builder.mjs +23 -8
- package/dist/lib/structure-migration.mjs +6 -4
- package/dist/lib/subagent-authorization-audit.mjs +8 -2
- package/dist/lib/task-archive-eligibility.mjs +65 -0
- package/dist/lib/task-audit-metadata.mjs +25 -11
- package/dist/lib/task-audit-migration.mjs +21 -14
- package/dist/lib/task-discovery-contract.mjs +32 -0
- package/dist/lib/task-index.mjs +4 -2
- package/dist/lib/task-lesson-candidates.mjs +1 -2
- package/dist/lib/task-lesson-sedimentation.mjs +310 -9
- package/dist/lib/task-lifecycle/create-task-helpers.mjs +6 -3
- package/dist/lib/task-lifecycle/phase-sync.mjs +0 -1
- package/dist/lib/task-lifecycle/preset-interop.mjs +16 -0
- package/dist/lib/task-lifecycle/review-confirm.mjs +34 -2
- package/dist/lib/task-lifecycle/review-gates.mjs +12 -5
- package/dist/lib/task-lifecycle/review-submission.mjs +1 -2
- package/dist/lib/task-lifecycle/scaffold-provenance.mjs +0 -1
- package/dist/lib/task-lifecycle/template-files.mjs +2 -5
- package/dist/lib/task-lifecycle.mjs +117 -159
- package/dist/lib/task-metadata.mjs +10 -5
- package/dist/lib/task-preset-contract-drift.mjs +45 -0
- package/dist/lib/task-repository.mjs +192 -0
- package/dist/lib/task-review-model.mjs +38 -17
- package/dist/lib/task-scanner.mjs +75 -23
- package/dist/lib/task-template-materials.mjs +131 -0
- package/dist/lib/task-tombstone-commands.mjs +187 -18
- package/dist/lib/types/check-profiles.js +1 -0
- package/dist/lib/types/impact.js +1 -0
- package/dist/lib/types/preset.js +1 -0
- package/dist/lib/types/task-lifecycle.js +1 -0
- package/dist/lib/types/task-scanner.js +1 -0
- package/dist/postinstall.mjs +2 -2
- package/dist/run-built-tests.mjs +10 -3
- package/docs-release/README.md +2 -1
- package/docs-release/architecture/document-contract-kernel/README.md +150 -0
- package/docs-release/architecture/document-contract-kernel/products/full-skill-overlay.md +29 -0
- package/docs-release/architecture/document-contract-kernel/products/lite-forbidden-surfaces.txt +26 -0
- package/docs-release/architecture/document-contract-kernel/products/lite-skill-overlay.md +37 -0
- package/docs-release/architecture/overview.md +2 -2
- package/docs-release/architecture/overview.zh-CN.md +2 -2
- package/docs-release/architecture/system-explainer/01-system-overview.md +11 -7
- package/docs-release/architecture/system-explainer/02-module-dependency.md +4 -4
- package/docs-release/architecture/system-explainer/03-task-lifecycle.md +17 -12
- package/docs-release/architecture/system-explainer/05-data-flow.md +6 -6
- package/docs-release/architecture/system-explainer/06-preset-and-migration.md +2 -2
- package/docs-release/architecture/system-explainer/README.md +1 -1
- package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +12 -8
- package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +5 -5
- package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +19 -11
- 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 +2 -2
- package/docs-release/architecture/system-explainer/en-US/README.md +1 -1
- package/docs-release/guides/agent-installation.en-US.md +4 -6
- package/docs-release/guides/agent-installation.md +11 -8
- package/docs-release/guides/contributing.md +10 -3
- package/docs-release/guides/contributing.zh-CN.md +10 -3
- package/docs-release/guides/legacy-migration-agent-prompt.md +1 -1
- package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +1 -1
- package/docs-release/guides/migration-playbook.en-US.md +9 -6
- package/docs-release/guides/migration-playbook.md +9 -6
- package/docs-release/guides/preset-development.md +68 -2
- package/docs-release/guides/task-state-machine.en-US.md +8 -8
- package/docs-release/guides/task-state-machine.md +7 -7
- package/docs-release/guides/typescript-runtime-migration-closeout.md +17 -13
- package/package.json +19 -11
- package/postinstall.mjs +37 -0
- 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/release-closeout/checks/check-release-package.mjs +29 -0
- package/presets/release-closeout/preset.yaml +100 -0
- package/presets/release-closeout/scripts/generate-release-package.mjs +572 -0
- package/presets/release-closeout/templates/execution_strategy.append.md +7 -0
- package/presets/release-closeout/templates/findings.seed.md +5 -0
- package/presets/release-closeout/templates/review.seed.md +3 -0
- package/presets/release-closeout/templates/task_plan.append.md +24 -0
- package/presets/standard-task/preset.yaml +2 -2
- package/references/agents-md-pattern.md +23 -17
- package/references/lessons-governance.md +2 -2
- package/references/module-parallel-standard.md +3 -6
- package/references/pull-request-standard.md +2 -2
- package/references/ssot-governance.md +2 -2
- package/references/taskr-gap-analysis.md +3 -3
- package/run-dist.mjs +34 -0
- package/skills/preset-creator/SKILL.md +40 -8
- package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -8
- package/skills/preset-creator/references/preset-package-skeleton.md +15 -5
- package/skills/preset-creator/references/structure-aware-paths.md +112 -0
- package/templates/AGENTS.md.template +28 -26
- package/templates/architecture/README.md +2 -2
- package/templates/architecture/service-catalog.md +2 -2
- package/templates/architecture/services/service-template.md +1 -1
- package/templates/dashboard/assets/app-src/00-state.js +5 -1
- package/templates/dashboard/assets/app-src/10-router.js +7 -0
- package/templates/dashboard/assets/app-src/20-overview.js +8 -8
- package/templates/dashboard/assets/app-src/30-tasks.js +132 -40
- package/templates/dashboard/assets/app-src/32-task-swimlane.js +314 -0
- package/templates/dashboard/assets/app-src/35-task-detail.js +35 -5
- package/templates/dashboard/assets/app-src/40-modules.js +257 -41
- package/templates/dashboard/assets/app-src/45-review.js +127 -1
- package/templates/dashboard/assets/app-src/90-bindings.js +185 -2
- package/templates/dashboard/assets/app.css +928 -53
- package/templates/dashboard/assets/app.css.manifest.json +2 -0
- package/templates/dashboard/assets/app.js +1071 -98
- package/templates/dashboard/assets/app.manifest.json +1 -0
- package/templates/dashboard/assets/css-src/00-foundation.css +12 -6
- package/templates/dashboard/assets/css-src/10-panels-flow.css +2 -2
- package/templates/dashboard/assets/css-src/30-task-index.css +21 -13
- package/templates/dashboard/assets/css-src/31-archive.css +94 -0
- package/templates/dashboard/assets/css-src/32-task-swimlane.css +487 -0
- package/templates/dashboard/assets/css-src/35-review-workspace.css +78 -0
- package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +191 -14
- package/templates/dashboard/assets/css-src/50-responsive-overrides.css +23 -0
- package/templates/dashboard/assets/i18n.js +166 -2
- package/templates/development/README.md +9 -9
- package/templates/development/cross-repo-debugging.md +3 -3
- package/templates/development/external-context/service-template.md +1 -1
- package/templates/development/external-source-packs/README.md +2 -2
- package/templates/integrations/README.md +4 -4
- package/templates/integrations/api-contract.md +1 -1
- package/templates/integrations/event-contract.md +1 -1
- package/templates/integrations/third-party/vendor-template.md +1 -1
- package/templates/integrations/webhook-contract.md +1 -1
- package/templates/ledger/Harness-Ledger.md +1 -1
- package/templates/modules/module_brief.md +50 -0
- package/templates/modules/module_plan.md +49 -0
- package/templates/modules/registry_view.md +9 -0
- package/templates/modules/session_prompt_pack.md +55 -0
- package/templates/planning/brief.md +32 -8
- package/templates/planning/module_brief.md +28 -3
- package/templates/planning/module_plan.md +26 -11
- package/templates/planning/module_session_prompt.md +11 -2
- package/templates/planning/optional/slices/_slice-template/brief.md +28 -0
- package/templates/planning/review.md +1 -1
- package/templates/planning/visual_map.md +1 -1
- package/templates/reference/docs-library-standard.md +7 -7
- package/templates/reference/execution-workflow-standard.md +13 -0
- package/templates/reference/external-source-intake-standard.md +10 -10
- package/templates/reference/pull-request-standard.md +2 -2
- package/templates/reference/repo-governance-standard.md +1 -1
- package/templates/reference/review-routing-standard.md +4 -0
- package/templates/ssot/Module-Registry.md +4 -38
- package/templates/walkthrough/walkthrough-template.md +1 -1
- package/templates-zh-CN/AGENTS.md.template +27 -25
- package/templates-zh-CN/CLAUDE.md.template +1 -1
- package/templates-zh-CN/architecture/README.md +2 -2
- 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 +9 -9
- package/templates-zh-CN/development/cross-repo-debugging.md +3 -3
- package/templates-zh-CN/development/external-context/service-template.md +1 -1
- package/templates-zh-CN/development/external-source-packs/README.md +2 -2
- package/templates-zh-CN/integrations/README.md +4 -4
- package/templates-zh-CN/integrations/api-contract.md +1 -1
- package/templates-zh-CN/integrations/event-contract.md +1 -1
- package/templates-zh-CN/integrations/third-party/vendor-template.md +1 -1
- package/templates-zh-CN/integrations/webhook-contract.md +1 -1
- 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/modules/module_brief.md +47 -0
- package/templates-zh-CN/modules/module_plan.md +48 -0
- package/templates-zh-CN/modules/registry_view.md +9 -0
- package/templates-zh-CN/modules/session_prompt_pack.md +50 -0
- package/templates-zh-CN/planning/INDEX.md +1 -0
- package/templates-zh-CN/planning/brief.md +26 -7
- package/templates-zh-CN/planning/module_brief.md +24 -2
- package/templates-zh-CN/planning/module_plan.md +35 -29
- package/templates-zh-CN/planning/module_session_prompt.md +15 -11
- package/templates-zh-CN/planning/optional/slices/_slice-template/brief.md +28 -11
- package/templates-zh-CN/planning/review.md +1 -1
- package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
- package/templates-zh-CN/reference/delivery-operating-model-standard.md +3 -3
- package/templates-zh-CN/reference/docs-library-standard.md +27 -27
- package/templates-zh-CN/reference/execution-workflow-standard.md +12 -2
- package/templates-zh-CN/reference/external-source-intake-standard.md +10 -10
- package/templates-zh-CN/reference/harness-ledger-standard.md +3 -3
- package/templates-zh-CN/reference/pull-request-standard.md +1 -1
- 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 +3 -0
- package/templates-zh-CN/reference/walkthrough-standard.md +2 -2
- 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 +2 -2
- package/templates-zh-CN/ssot/Module-Registry.md +5 -44
- package/templates-zh-CN/ssot/Regression-SSoT.md +2 -2
- package/templates-zh-CN/walkthrough/walkthrough-template.md +4 -4
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
// Governance sync spans dynamic target metadata and Git porcelain until the governance domain model PR.
|
|
3
2
|
import fs from "node:fs";
|
|
4
3
|
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import crypto from "node:crypto";
|
|
7
6
|
import { spawnSync } from "node:child_process";
|
|
8
|
-
import { readBundledTemplate, readFileSafe, readJsonSafe, repoRoot, todayDate, toPosix
|
|
7
|
+
import { readBundledTemplate, readFileSafe, readJsonSafe, repoRoot, todayDate, toPosix } from "./core-shared.mjs";
|
|
9
8
|
import { collectTasks } from "./task-scanner.mjs";
|
|
10
9
|
import { appendMarkdownTableRow, firstColumn, fitMarkdownTableRow, splitMarkdownRow, upsertMarkdownTableRow } from "./markdown-utils.mjs";
|
|
11
10
|
import { resolveHarnessPaths } from "./harness-paths.mjs";
|
|
11
|
+
import { moduleRegistryViewPath, renderModuleRegistryView } from "./module-registry.mjs";
|
|
12
12
|
export class GovernanceSyncError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
details;
|
|
15
|
+
recovery;
|
|
13
16
|
constructor(message, { code = "governance-sync-failed", details = {}, recovery = [] } = {}) {
|
|
14
17
|
super(message);
|
|
15
18
|
this.name = "GovernanceSyncError";
|
|
@@ -18,7 +21,7 @@ export class GovernanceSyncError extends Error {
|
|
|
18
21
|
this.recovery = recovery;
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
|
-
export function beginGovernanceSync(target, { operation = "governance-sync", dryRun = false, allowDirtyWorktree = false, allowedRelativePaths = [] } = {}) {
|
|
24
|
+
export function beginGovernanceSync(target, { operation = "governance-sync", dryRun = false, allowDirtyWorktree = false, allowedRelativePaths = [], allowDirtyWriteScope = false } = {}) {
|
|
22
25
|
if (dryRun)
|
|
23
26
|
return { target, dryRun, operation, git: inspectGit(target.projectRoot), lockPath: "", active: false };
|
|
24
27
|
const lockPath = path.join(target.projectRoot, ".harness/locks/governance-sync.lock");
|
|
@@ -45,7 +48,7 @@ export function beginGovernanceSync(target, { operation = "governance-sync", dry
|
|
|
45
48
|
}
|
|
46
49
|
if (gitState.entries.length > 0 && allowDirtyWorktree) {
|
|
47
50
|
try {
|
|
48
|
-
assertDirtyCompatibleWithWriteScope(gitState.entries, allowed);
|
|
51
|
+
assertDirtyCompatibleWithWriteScope(gitState.entries, allowed, { allowDirtyWriteScope });
|
|
49
52
|
}
|
|
50
53
|
catch (error) {
|
|
51
54
|
releaseGovernanceSync({ lockPath, active: true });
|
|
@@ -79,7 +82,7 @@ function acquireGovernanceSyncLock(lockPath, target, { operation }) {
|
|
|
79
82
|
catch (error) {
|
|
80
83
|
if (fd !== null)
|
|
81
84
|
fs.closeSync(fd);
|
|
82
|
-
if (error
|
|
85
|
+
if (isNodeError(error) && error.code === "EEXIST" && attempt === 0 && removeStaleGovernanceSyncLock(lockPath))
|
|
83
86
|
continue;
|
|
84
87
|
throw governanceLockExistsError(lockPath, error);
|
|
85
88
|
}
|
|
@@ -92,14 +95,15 @@ function removeStaleGovernanceSyncLock(lockPath) {
|
|
|
92
95
|
return false;
|
|
93
96
|
if (lock.host !== governanceLockHost())
|
|
94
97
|
return false;
|
|
95
|
-
|
|
98
|
+
const pid = lock.pid;
|
|
99
|
+
if (!Number.isInteger(pid) || !pid || pid <= 0)
|
|
96
100
|
return false;
|
|
97
101
|
try {
|
|
98
|
-
process.kill(
|
|
102
|
+
process.kill(pid, 0);
|
|
99
103
|
return false;
|
|
100
104
|
}
|
|
101
105
|
catch (error) {
|
|
102
|
-
if (error
|
|
106
|
+
if (!isNodeError(error) || error.code !== "ESRCH")
|
|
103
107
|
return false;
|
|
104
108
|
}
|
|
105
109
|
if (readFileSafe(lockPath) !== lockContent)
|
|
@@ -113,7 +117,7 @@ function governanceLockHost() {
|
|
|
113
117
|
function governanceLockExistsError(lockPath, error) {
|
|
114
118
|
return new GovernanceSyncError("Governance sync lock already exists; refusing concurrent registry writes.", {
|
|
115
119
|
code: "governance-lock-exists",
|
|
116
|
-
details: { lockPath, error:
|
|
120
|
+
details: { lockPath, error: errorMessage(error) },
|
|
117
121
|
recovery: [
|
|
118
122
|
`Inspect ${lockPath}.`,
|
|
119
123
|
"If no process owns the lock, remove it manually and retry.",
|
|
@@ -149,7 +153,7 @@ export function commitGovernanceSync(context, allowedRelativePaths, { message =
|
|
|
149
153
|
assertNoUnexpectedOutsideChanges(context.target.projectRoot, allowed, context.initialDirtyEntries || []);
|
|
150
154
|
}
|
|
151
155
|
catch (error) {
|
|
152
|
-
outsideChanges = error.details
|
|
156
|
+
outsideChanges = error instanceof GovernanceSyncError ? error.details : null;
|
|
153
157
|
}
|
|
154
158
|
throw new GovernanceSyncError("Governance sync wrote files but Git commit failed.", {
|
|
155
159
|
code: "governance-git-commit-failed",
|
|
@@ -172,15 +176,17 @@ export function syncTaskGovernance(target, task, { event = "new-task", state = "
|
|
|
172
176
|
const ledger = syncLedgerRow(target, task, { event, state, message, planPath, reviewPath, dryRun });
|
|
173
177
|
if (ledger)
|
|
174
178
|
changes.push(ledger);
|
|
175
|
-
|
|
176
|
-
|
|
179
|
+
const moduleKey = task.module;
|
|
180
|
+
if (moduleKey) {
|
|
181
|
+
const taskWithModule = { ...task, module: moduleKey };
|
|
182
|
+
const moduleRegistry = syncModuleRegistryView(target, { dryRun });
|
|
177
183
|
if (moduleRegistry)
|
|
178
184
|
changes.push(moduleRegistry);
|
|
179
|
-
changes.push(...syncModuleGeneratedIndexes(target,
|
|
185
|
+
changes.push(...syncModuleGeneratedIndexes(target, moduleKey, { task: taskWithModule, dryRun }).changes);
|
|
180
186
|
}
|
|
181
187
|
return { changes };
|
|
182
188
|
}
|
|
183
|
-
export function syncModuleStepGovernance(target, { moduleKey, stepId, state, dryRun = false }
|
|
189
|
+
export function syncModuleStepGovernance(target, { moduleKey, stepId, state, dryRun = false }) {
|
|
184
190
|
const changes = [];
|
|
185
191
|
const harnessPaths = activeHarnessPaths(target);
|
|
186
192
|
const ledgerPath = harnessPaths.ledgerPath;
|
|
@@ -235,31 +241,12 @@ function syncLedgerRow(target, task, { event, state, message, planPath, reviewPa
|
|
|
235
241
|
}
|
|
236
242
|
return { destination: relative, action: dryRun ? "would-sync-governance" : "sync-governance", surface: "harness-ledger" };
|
|
237
243
|
}
|
|
238
|
-
function
|
|
239
|
-
const
|
|
240
|
-
const registryPath = path.join(harnessPaths.modulesRoot, "Module-Registry.md");
|
|
241
|
-
ensureFileFromTemplate(registryPath, "templates/ssot/Module-Registry.md", { dryRun });
|
|
244
|
+
function syncModuleRegistryView(target, { dryRun }) {
|
|
245
|
+
const registryPath = moduleRegistryViewPath(target);
|
|
242
246
|
const relative = toPosix(path.relative(target.projectRoot, registryPath));
|
|
243
247
|
if (!dryRun) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const moduleDirRelative = toPosix(path.relative(target.projectRoot, path.join(harnessPaths.modulesRoot, moduleKey)));
|
|
247
|
-
const modulePlan = `${moduleDirRelative}/module_plan.md`;
|
|
248
|
-
const row = [
|
|
249
|
-
`M-${moduleKey.toUpperCase().replace(/[^A-Z0-9]+/g, "-")}`,
|
|
250
|
-
moduleKey,
|
|
251
|
-
`${moduleDirRelative}/**`,
|
|
252
|
-
"coordinator",
|
|
253
|
-
state === "planned" ? "reserved" : mapModuleState(state),
|
|
254
|
-
`codex/${moduleKey}`,
|
|
255
|
-
modulePlan,
|
|
256
|
-
"none",
|
|
257
|
-
"none",
|
|
258
|
-
planPath,
|
|
259
|
-
"none",
|
|
260
|
-
todayDate(),
|
|
261
|
-
];
|
|
262
|
-
fs.writeFileSync(registryPath, upsertMarkdownTableRow(content, /^ID$/i, (header, existing) => rowMatchesModule(header, existing, moduleKey, modulePlan), row));
|
|
248
|
+
fs.mkdirSync(path.dirname(registryPath), { recursive: true });
|
|
249
|
+
fs.writeFileSync(registryPath, renderModuleRegistryView(target));
|
|
263
250
|
}
|
|
264
251
|
return { destination: relative, action: dryRun ? "would-sync-governance" : "sync-governance", surface: "module-registry" };
|
|
265
252
|
}
|
|
@@ -282,7 +269,7 @@ function syncModuleGeneratedIndexes(target, moduleKey, { task = null, dryRun = f
|
|
|
282
269
|
}
|
|
283
270
|
export function moduleGeneratedIndexSurfaces(target, tasks = collectTasks(target)) {
|
|
284
271
|
const harnessPaths = activeHarnessPaths(target);
|
|
285
|
-
const modules = [...new Set((tasks || []).map((task) => task.module).filter(
|
|
272
|
+
const modules = [...new Set((tasks || []).map((task) => task.module).filter(isNonEmptyString))].sort();
|
|
286
273
|
const surfaces = [];
|
|
287
274
|
for (const moduleKey of modules) {
|
|
288
275
|
const moduleTasks = (tasks || [])
|
|
@@ -290,11 +277,11 @@ export function moduleGeneratedIndexSurfaces(target, tasks = collectTasks(target
|
|
|
290
277
|
.sort((a, b) => String(stripDatePrefix(a.shortId || a.id)).localeCompare(String(stripDatePrefix(b.shortId || b.id))));
|
|
291
278
|
const moduleDir = path.join(harnessPaths.modulesRoot, moduleKey);
|
|
292
279
|
const modulePlanPath = path.join(moduleDir, "module_plan.md");
|
|
293
|
-
const moduleVisualPath = path.join(moduleDir, visualMapFile);
|
|
294
280
|
const stepRows = moduleTasks.map((task, index) => {
|
|
295
281
|
const stepId = moduleStepId(task);
|
|
296
|
-
const
|
|
297
|
-
|
|
282
|
+
const previousTask = moduleTasks[index - 1];
|
|
283
|
+
const previous = index === 0 || !previousTask ? "none" : moduleStepId(previousTask);
|
|
284
|
+
return [stepId, task.title || task.shortId || task.id || "task", mapModuleState(task.state), stripTargetPrefix(task.taskPlanPath || `${stripTargetPrefix(task.path)}/task_plan.md`), previous];
|
|
298
285
|
});
|
|
299
286
|
surfaces.push({
|
|
300
287
|
surface: "module-plan-index",
|
|
@@ -303,20 +290,14 @@ export function moduleGeneratedIndexSurfaces(target, tasks = collectTasks(target
|
|
|
303
290
|
rows: stepRows,
|
|
304
291
|
content: replaceTableRows(existingOrTemplate(modulePlanPath, "templates/planning/module_plan.md"), /^Step ID$/i, stepRows),
|
|
305
292
|
});
|
|
306
|
-
surfaces.push({
|
|
307
|
-
surface: "module-visual-index",
|
|
308
|
-
absolute: moduleVisualPath,
|
|
309
|
-
relative: toPosix(path.relative(target.projectRoot, moduleVisualPath)),
|
|
310
|
-
rows: stepRows,
|
|
311
|
-
content: renderModuleVisualMap(moduleKey, moduleTasks),
|
|
312
|
-
});
|
|
313
293
|
}
|
|
314
294
|
return surfaces;
|
|
315
295
|
}
|
|
316
296
|
function collectModuleTasks(target, moduleKey, task) {
|
|
317
|
-
const tasks = collectTasks(target)
|
|
318
|
-
|
|
319
|
-
|
|
297
|
+
const tasks = collectTasks(target);
|
|
298
|
+
const moduleTasks = tasks.filter((candidate) => candidate.module === moduleKey);
|
|
299
|
+
if (task && !moduleTasks.some((candidate) => stripTargetPrefix(candidate.taskPlanPath) === `${stripTargetPrefix(task.path)}/task_plan.md`)) {
|
|
300
|
+
moduleTasks.push({
|
|
320
301
|
...task,
|
|
321
302
|
module: moduleKey,
|
|
322
303
|
state: task.state || "planned",
|
|
@@ -324,61 +305,7 @@ function collectModuleTasks(target, moduleKey, task) {
|
|
|
324
305
|
completion: 0,
|
|
325
306
|
});
|
|
326
307
|
}
|
|
327
|
-
return
|
|
328
|
-
}
|
|
329
|
-
function renderModuleVisualMap(moduleKey, tasks) {
|
|
330
|
-
const rows = tasks.map((task, index) => {
|
|
331
|
-
const stepId = moduleStepId(task);
|
|
332
|
-
const previous = index === 0 ? "none" : moduleStepId(tasks[index - 1]);
|
|
333
|
-
const state = mapPhaseState(task.state);
|
|
334
|
-
const completion = Number.isInteger(task.completion) ? task.completion : state === "done" ? 100 : 0;
|
|
335
|
-
return [
|
|
336
|
-
stepId,
|
|
337
|
-
previous,
|
|
338
|
-
state,
|
|
339
|
-
completion,
|
|
340
|
-
task.title || task.shortId || task.id,
|
|
341
|
-
stripTargetPrefix(task.taskPlanPath || `${stripTargetPrefix(task.path)}/task_plan.md`),
|
|
342
|
-
task.materialsReady ? "present" : "missing",
|
|
343
|
-
previous === "none" ? "none" : `depends on ${previous}`,
|
|
344
|
-
"coordinator",
|
|
345
|
-
];
|
|
346
|
-
});
|
|
347
|
-
const graphLines = tasks.map((task, index) => {
|
|
348
|
-
const stepId = moduleStepId(task);
|
|
349
|
-
const label = fitMarkdownTableRow([task.title || task.shortId || task.id], 1)[0].replace(/"/g, "'");
|
|
350
|
-
if (index === 0)
|
|
351
|
-
return ` ${stepId}["${label}"]`;
|
|
352
|
-
const previous = moduleStepId(tasks[index - 1]);
|
|
353
|
-
return ` ${previous} --> ${stepId}["${label}"]`;
|
|
354
|
-
});
|
|
355
|
-
return `# ${moduleKey} - Visual Map
|
|
356
|
-
|
|
357
|
-
Visual Map Contract: v1.0
|
|
358
|
-
|
|
359
|
-
Generated by \`harness new-task --module\` and \`harness governance rebuild\`.
|
|
360
|
-
|
|
361
|
-
## Map Index
|
|
362
|
-
|
|
363
|
-
| ID | Type | Purpose | Required For Understanding | Source Evidence | Promotion Candidate |
|
|
364
|
-
| --- | --- | --- | --- | --- | --- |
|
|
365
|
-
| MAP-01 | topology | Show module task sequence generated from task files | yes | task scan | no |
|
|
366
|
-
|
|
367
|
-
## Phase Graph
|
|
368
|
-
|
|
369
|
-
\`\`\`mermaid
|
|
370
|
-
flowchart LR
|
|
371
|
-
${graphLines.length ? graphLines.join("\n") : " EMPTY[\"No module tasks\"]"}
|
|
372
|
-
\`\`\`
|
|
373
|
-
|
|
374
|
-
## Phase Table
|
|
375
|
-
|
|
376
|
-
| Phase ID | Depends On | State | Completion | Output | Required Evidence | Evidence Status | Blocking Risk | Owner / Handoff |
|
|
377
|
-
| --- | --- | --- | ---: | --- | --- | --- | --- | --- |
|
|
378
|
-
${rows.map((row) => `| ${fitMarkdownTableRow(row, 9).join(" | ")} |`).join("\n")}
|
|
379
|
-
|
|
380
|
-
Allowed Evidence Status: missing, partial, present, waived.
|
|
381
|
-
`;
|
|
308
|
+
return moduleTasks;
|
|
382
309
|
}
|
|
383
310
|
function replaceTableRows(content, headerPattern, rows) {
|
|
384
311
|
const lines = String(content || "").split(/\r?\n/);
|
|
@@ -408,17 +335,6 @@ function moduleStepId(task) {
|
|
|
408
335
|
function stripDatePrefix(value) {
|
|
409
336
|
return String(value || "").replace(/^(?:TASKS\/|MODULES\/[^/]+\/)?\d{4}-\d{2}-\d{2}-/, "");
|
|
410
337
|
}
|
|
411
|
-
function mapPhaseState(state) {
|
|
412
|
-
if (state === "in_progress")
|
|
413
|
-
return "in_progress";
|
|
414
|
-
if (state === "review")
|
|
415
|
-
return "review";
|
|
416
|
-
if (state === "done")
|
|
417
|
-
return "done";
|
|
418
|
-
if (state === "blocked")
|
|
419
|
-
return "blocked";
|
|
420
|
-
return "planned";
|
|
421
|
-
}
|
|
422
338
|
function ensureFileFromTemplate(destinationPath, templateSource, { dryRun = false } = {}) {
|
|
423
339
|
if (fs.existsSync(destinationPath) || dryRun)
|
|
424
340
|
return;
|
|
@@ -487,10 +403,10 @@ function assertCommitIdentity(root) {
|
|
|
487
403
|
});
|
|
488
404
|
}
|
|
489
405
|
}
|
|
490
|
-
function assertDirtyCompatibleWithWriteScope(entries, allowedPaths) {
|
|
406
|
+
function assertDirtyCompatibleWithWriteScope(entries, allowedPaths, { allowDirtyWriteScope = false } = {}) {
|
|
491
407
|
const allowed = new Set(allowedPaths);
|
|
492
408
|
const overlapping = entries.filter((entry) => allowed.has(entry.path));
|
|
493
|
-
if (overlapping.length > 0) {
|
|
409
|
+
if (overlapping.length > 0 && !allowDirtyWriteScope) {
|
|
494
410
|
throw new GovernanceSyncError("Governance sync write scope overlaps existing dirty files; refusing to overwrite user-owned changes.", {
|
|
495
411
|
code: "governance-write-scope-dirty",
|
|
496
412
|
details: { overlapping, allowedPaths },
|
|
@@ -575,7 +491,7 @@ function fingerprintEntry(root, entry) {
|
|
|
575
491
|
if (stat.isSymbolicLink())
|
|
576
492
|
return `symlink:${fs.readlinkSync(absolute)}`;
|
|
577
493
|
if (stat.isFile()) {
|
|
578
|
-
return `file:${stat.size}:${crypto.createHash("sha256").update(fs.readFileSync(absolute)).digest("hex")}`;
|
|
494
|
+
return `file:${stat.size}:${crypto.createHash("sha256").update(new Uint8Array(fs.readFileSync(absolute))).digest("hex")}`;
|
|
579
495
|
}
|
|
580
496
|
if (stat.isDirectory())
|
|
581
497
|
return "directory";
|
|
@@ -599,7 +515,7 @@ function statusEntries(root) {
|
|
|
599
515
|
}
|
|
600
516
|
function parseStatusPath(value) {
|
|
601
517
|
const unquoted = value.replace(/^"|"$/g, "");
|
|
602
|
-
return unquoted.includes(" -> ") ? unquoted.split(" -> ").pop() : unquoted;
|
|
518
|
+
return unquoted.includes(" -> ") ? unquoted.split(" -> ").pop() ?? unquoted : unquoted;
|
|
603
519
|
}
|
|
604
520
|
function git(cwd, args, { allowFailure = false } = {}) {
|
|
605
521
|
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
@@ -615,3 +531,12 @@ function git(cwd, args, { allowFailure = false } = {}) {
|
|
|
615
531
|
function real(filePath) {
|
|
616
532
|
return fs.realpathSync(filePath);
|
|
617
533
|
}
|
|
534
|
+
function isNodeError(error) {
|
|
535
|
+
return error instanceof Error && "code" in error;
|
|
536
|
+
}
|
|
537
|
+
function errorMessage(error) {
|
|
538
|
+
return error instanceof Error ? error.message : String(error);
|
|
539
|
+
}
|
|
540
|
+
function isNonEmptyString(value) {
|
|
541
|
+
return typeof value === "string" && value.length > 0;
|
|
542
|
+
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
// Governance table parsing remains behavior-first until target/table domain types are modeled.
|
|
3
2
|
import fs from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { readFileSafe, toPosix } from "./core-shared.mjs";
|
|
6
5
|
import { getCell, parseAllMarkdownTables } from "./markdown-utils.mjs";
|
|
7
|
-
const newRuleCutoff = "2026-05-24";
|
|
8
6
|
const globalTableSpecs = [
|
|
9
7
|
{ key: "feature-ssot", pathFor: (target) => path.join(target.harness.planningRoot, "Feature-SSoT.md"), allowed: "index-state-route-summary", evaluate: evaluateFeatureRow },
|
|
10
8
|
{ key: "harness-ledger", pathFor: (target) => target.harness.ledgerPath, allowed: "task-audit-summary-route", evaluate: evaluateLedgerRow },
|
|
@@ -34,10 +32,7 @@ export function validateGovernanceTableBoundaries(target) {
|
|
|
34
32
|
`allowed=${spec.allowed}`,
|
|
35
33
|
`route=${finding.route}`,
|
|
36
34
|
].join(": ");
|
|
37
|
-
|
|
38
|
-
warnings.push(`${message}: legacy-report-only`);
|
|
39
|
-
else
|
|
40
|
-
failures.push(message);
|
|
35
|
+
failures.push(message);
|
|
41
36
|
}
|
|
42
37
|
}
|
|
43
38
|
}
|
|
@@ -132,14 +127,6 @@ function governanceRowKey(row) {
|
|
|
132
127
|
function rowText(row) {
|
|
133
128
|
return Object.values(row.cells || {}).join(" ");
|
|
134
129
|
}
|
|
135
|
-
function rowUpdatedDate(row) {
|
|
136
|
-
const value = getCell(row.cells || {}, ["Updated", "Date", "日期", "Last Verified", "最近验证"], "");
|
|
137
|
-
const match = String(value).match(/\d{4}-\d{2}-\d{2}/);
|
|
138
|
-
return match ? match[0] : "";
|
|
139
|
-
}
|
|
140
|
-
function isLegacyRow(updated) {
|
|
141
|
-
return updated && updated < newRuleCutoff;
|
|
142
|
-
}
|
|
143
130
|
function isPlaceholderRow(row) {
|
|
144
131
|
const text = rowText(row);
|
|
145
132
|
return /\b(?:YYYY|MM|DD|NNN)\b|L-YYYY|HL-YYYY|\[[^\]]+\]|\.\.\.md|\bowner\b|\bShort lesson title\b/i.test(text);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
export * from "./core-shared.mjs";
|
|
3
2
|
export * from "./markdown-utils.mjs";
|
|
4
3
|
export * from "./capability-registry.mjs";
|
|
5
4
|
export * from "./task-scanner.mjs";
|
|
5
|
+
export * from "./task-discovery-contract.mjs";
|
|
6
6
|
export * from "./status-builder.mjs";
|
|
7
7
|
export * from "./check-profiles.mjs";
|
|
8
8
|
export * from "./dashboard-data.mjs";
|
|
@@ -10,9 +10,13 @@ export * from "./dashboard-workbench.mjs";
|
|
|
10
10
|
export * from "./migration-planner.mjs";
|
|
11
11
|
export * from "./structure-migration.mjs";
|
|
12
12
|
export * from "./preset-registry.mjs";
|
|
13
|
+
export * from "./preset-runner.mjs";
|
|
13
14
|
export * from "./governance-index-generator.mjs";
|
|
14
15
|
export * from "./task-lifecycle.mjs";
|
|
16
|
+
export * from "./module-registry.mjs";
|
|
15
17
|
export * from "./task-lesson-sedimentation.mjs";
|
|
16
18
|
export * from "./lesson-maintenance.mjs";
|
|
17
19
|
export * from "./task-index.mjs";
|
|
18
20
|
export * from "./task-tombstone-commands.mjs";
|
|
21
|
+
export * from "./task-archive-eligibility.mjs";
|
|
22
|
+
export * from "./impact-classifier.mjs";
|
|
@@ -48,7 +48,7 @@ export function resolveHarnessPaths(targetInput = ".") {
|
|
|
48
48
|
planningRoot: resolved.planningRoot,
|
|
49
49
|
tasksRoot: resolved.tasksRoot,
|
|
50
50
|
modulesRoot: resolved.modulesRoot,
|
|
51
|
-
taskRoots: [resolved.tasksRoot, resolved.modulesRoot],
|
|
51
|
+
taskRoots: [resolved.tasksRoot, resolved.modulesRoot, resolved.externalRoot].filter(Boolean),
|
|
52
52
|
externalRoot: resolved.externalRoot,
|
|
53
53
|
governanceRoot: resolved.governanceRoot,
|
|
54
54
|
generatedRoot: resolved.generatedRoot,
|
|
@@ -281,6 +281,9 @@ function readHarnessManifest(manifestPath) {
|
|
|
281
281
|
return null;
|
|
282
282
|
const manifest = { version: 2, locale: "en-US", capabilities: [], structure: {} };
|
|
283
283
|
let section = "";
|
|
284
|
+
let inModuleItems = false;
|
|
285
|
+
let currentModuleKey = "";
|
|
286
|
+
let currentModuleListField = "";
|
|
284
287
|
for (const rawLine of fs.readFileSync(manifestPath, "utf8").split(/\r?\n/)) {
|
|
285
288
|
const line = rawLine.replace(/\s+#.*$/, "");
|
|
286
289
|
if (!line.trim())
|
|
@@ -288,10 +291,15 @@ function readHarnessManifest(manifestPath) {
|
|
|
288
291
|
const top = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
289
292
|
if (top) {
|
|
290
293
|
section = top[1];
|
|
294
|
+
inModuleItems = false;
|
|
295
|
+
currentModuleKey = "";
|
|
296
|
+
currentModuleListField = "";
|
|
291
297
|
if (section === "version")
|
|
292
298
|
manifest.version = Number(top[2]) || 2;
|
|
293
299
|
else if (section === "locale")
|
|
294
300
|
manifest.locale = top[2] || "en-US";
|
|
301
|
+
else if (section === "modules")
|
|
302
|
+
manifest.modules = { items: {} };
|
|
295
303
|
else if (section !== "structure" && section !== "capabilities")
|
|
296
304
|
manifest[section] = top[2];
|
|
297
305
|
continue;
|
|
@@ -304,6 +312,54 @@ function readHarnessManifest(manifestPath) {
|
|
|
304
312
|
const nested = line.match(/^\s+([A-Za-z][A-Za-z0-9_-]*):\s*(.+)$/);
|
|
305
313
|
if (section === "structure" && nested)
|
|
306
314
|
manifest.structure[nested[1]] = nested[2].trim();
|
|
315
|
+
if (section === "modules") {
|
|
316
|
+
if (!manifest.modules)
|
|
317
|
+
manifest.modules = { items: {} };
|
|
318
|
+
const moduleTop = line.match(/^ ([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
319
|
+
if (moduleTop) {
|
|
320
|
+
currentModuleKey = "";
|
|
321
|
+
currentModuleListField = "";
|
|
322
|
+
if (moduleTop[1] === "items")
|
|
323
|
+
inModuleItems = true;
|
|
324
|
+
else if (moduleTop[1] === "schema")
|
|
325
|
+
manifest.modules.schema = moduleTop[2].trim();
|
|
326
|
+
else if (moduleTop[1] === "generatedView")
|
|
327
|
+
manifest.modules.generatedView = moduleTop[2].trim();
|
|
328
|
+
else
|
|
329
|
+
manifest.modules[moduleTop[1]] = moduleTop[2].trim();
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const moduleItem = line.match(/^ ([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
333
|
+
if (inModuleItems && moduleItem) {
|
|
334
|
+
currentModuleKey = moduleItem[1];
|
|
335
|
+
currentModuleListField = "";
|
|
336
|
+
if (!manifest.modules.items[currentModuleKey])
|
|
337
|
+
manifest.modules.items[currentModuleKey] = {};
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const moduleField = line.match(/^ ([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
341
|
+
if (inModuleItems && currentModuleKey && moduleField) {
|
|
342
|
+
const field = moduleField[1];
|
|
343
|
+
const raw = moduleField[2].trim();
|
|
344
|
+
if (["scope", "shared", "dependsOn"].includes(field)) {
|
|
345
|
+
manifest.modules.items[currentModuleKey][field] = raw === "[]" ? [] : raw ? [raw] : [];
|
|
346
|
+
currentModuleListField = field;
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
manifest.modules.items[currentModuleKey][field] = raw;
|
|
350
|
+
currentModuleListField = "";
|
|
351
|
+
}
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const moduleListItem = line.match(/^ -\s*(.+)$/);
|
|
355
|
+
if (inModuleItems && currentModuleKey && currentModuleListField && moduleListItem) {
|
|
356
|
+
const existing = manifest.modules.items[currentModuleKey][currentModuleListField];
|
|
357
|
+
manifest.modules.items[currentModuleKey][currentModuleListField] = [
|
|
358
|
+
...(Array.isArray(existing) ? existing : []),
|
|
359
|
+
moduleListItem[1].trim(),
|
|
360
|
+
];
|
|
361
|
+
}
|
|
362
|
+
}
|
|
307
363
|
}
|
|
308
364
|
if (!manifest.structure.harnessRoot && manifest.harnessRoot)
|
|
309
365
|
manifest.structure.harnessRoot = manifest.harnessRoot;
|
|
@@ -311,6 +367,64 @@ function readHarnessManifest(manifestPath) {
|
|
|
311
367
|
manifest.structure.planningRoot = `${manifest.harnessRoot}/planning`;
|
|
312
368
|
return manifest;
|
|
313
369
|
}
|
|
370
|
+
export function renderHarnessManifest({ locale, capabilities, structure = null, modules = null }) {
|
|
371
|
+
const manifestStructure = structure || {
|
|
372
|
+
harnessRoot: v2HarnessRoot,
|
|
373
|
+
planningRoot: `${v2HarnessRoot}/planning`,
|
|
374
|
+
tasksRoot: `${v2HarnessRoot}/planning/tasks`,
|
|
375
|
+
modulesRoot: `${v2HarnessRoot}/planning/modules`,
|
|
376
|
+
externalRoot: `${v2HarnessRoot}/planning/external`,
|
|
377
|
+
governanceRoot: `${v2HarnessRoot}/governance`,
|
|
378
|
+
generatedRoot: `${v2HarnessRoot}/governance/generated`,
|
|
379
|
+
};
|
|
380
|
+
const lines = [
|
|
381
|
+
"version: 2",
|
|
382
|
+
`locale: ${locale}`,
|
|
383
|
+
"capabilities:",
|
|
384
|
+
...capabilities.map((capability) => ` - ${capability}`),
|
|
385
|
+
"structure:",
|
|
386
|
+
...Object.entries(manifestStructure).map(([key, value]) => ` ${key}: ${value}`),
|
|
387
|
+
];
|
|
388
|
+
if (modules && (modules.schema || modules.generatedView || Object.keys(modules.items || {}).length > 0)) {
|
|
389
|
+
lines.push("modules:");
|
|
390
|
+
if (modules.schema)
|
|
391
|
+
lines.push(` schema: ${yamlScalar(modules.schema)}`);
|
|
392
|
+
if (modules.generatedView)
|
|
393
|
+
lines.push(` generatedView: ${yamlScalar(modules.generatedView)}`);
|
|
394
|
+
lines.push(" items:");
|
|
395
|
+
for (const [key, module] of Object.entries(modules.items || {}).sort(([left], [right]) => left.localeCompare(right))) {
|
|
396
|
+
lines.push(` ${key}:`);
|
|
397
|
+
for (const field of ["title", "prefix", "status", "branch", "owner", "currentStep", "scope", "shared", "dependsOn", "plan", "brief", "updated"]) {
|
|
398
|
+
const value = module[field];
|
|
399
|
+
if (Array.isArray(value)) {
|
|
400
|
+
lines.push(` ${field}:${value.length ? "" : " []"}`);
|
|
401
|
+
for (const item of value)
|
|
402
|
+
lines.push(` - ${yamlScalar(String(item))}`);
|
|
403
|
+
}
|
|
404
|
+
else if (value !== undefined && value !== null && String(value) !== "") {
|
|
405
|
+
lines.push(` ${field}: ${yamlScalar(String(value))}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return `${lines.join("\n")}\n`;
|
|
411
|
+
}
|
|
412
|
+
export function assertRenderableHarnessManifest(manifest) {
|
|
413
|
+
if (!manifest)
|
|
414
|
+
return;
|
|
415
|
+
const allowed = new Set(["version", "locale", "capabilities", "structure", "modules", "harnessRoot"]);
|
|
416
|
+
const unknown = Object.keys(manifest).filter((key) => !allowed.has(key));
|
|
417
|
+
if (unknown.length > 0)
|
|
418
|
+
throw new Error(`Cannot rewrite harness.yaml with unknown top-level fields: ${unknown.join(", ")}`);
|
|
419
|
+
}
|
|
420
|
+
function yamlScalar(value) {
|
|
421
|
+
const raw = String(value || "");
|
|
422
|
+
if (!raw)
|
|
423
|
+
return "''";
|
|
424
|
+
if (/[:#\n\r]|^\s|\s$|^(?:true|false|null|\d+(?:\.\d+)?)$/i.test(raw))
|
|
425
|
+
return JSON.stringify(raw);
|
|
426
|
+
return raw;
|
|
427
|
+
}
|
|
314
428
|
function resolveManifestStructurePath(projectRoot, fieldName, relativePath) {
|
|
315
429
|
const raw = String(relativePath || "").trim();
|
|
316
430
|
if (!raw)
|