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,23 +1,34 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import crypto from "node:crypto";
|
|
5
|
+
import zlib from "node:zlib";
|
|
4
6
|
import { builtinPresetRoot, projectPresetRoot, repoRoot, toPosix, userPresetRoot, userPresetRootForHome } from "./core-shared.mjs";
|
|
5
7
|
|
|
6
8
|
const allowedEntrypoints = new Set(["newTask", "plan", "scaffold", "check"]);
|
|
7
9
|
const allowedEntrypointTypes = new Set(["template", "script", "check"]);
|
|
8
10
|
const allowedEvidenceTypes = new Set(["text", "json", "input-json", "preset-audit", "preset-manifest", "write-scope", "migration-verify", "migration-ledger", "dashboard-hash", "target-git-status", "target-commit", "harness-version", "generated-at"]);
|
|
11
|
+
const allowedNewTaskTemplateKeys = new Set(["taskPlanAppend", "executionStrategyAppend", "visualMapAppend", "findingsSeed", "reviewSeed", "prompt"]);
|
|
12
|
+
const maxPresetArchiveBytes = 25 * 1024 * 1024;
|
|
13
|
+
const maxPresetArchiveUncompressedBytes = 50 * 1024 * 1024;
|
|
14
|
+
const maxPresetArchiveEntries = 500;
|
|
9
15
|
|
|
10
16
|
export function listPresetPackages({ targetInput = "", home = "" } = {}) {
|
|
11
|
-
|
|
17
|
+
return listPresetPackageLayers({ targetInput, home }).filter((preset) => preset.effective);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function listPresetPackageLayers({ targetInput = "", home = "" } = {}) {
|
|
21
|
+
const effectiveIds = new Set();
|
|
12
22
|
const presets = [];
|
|
13
23
|
for (const { root, source } of presetSearchRoots({ targetInput, home })) {
|
|
14
24
|
if (!fs.existsSync(root)) continue;
|
|
15
25
|
for (const entry of fs.readdirSync(root, { withFileTypes: true }).filter((item) => item.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
16
26
|
const id = tryNormalizePresetId(entry.name);
|
|
17
27
|
if (!id) continue;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
const preset = readPresetPackageFromPath(path.join(root, id), source);
|
|
29
|
+
const effective = !effectiveIds.has(preset.id);
|
|
30
|
+
if (effective) effectiveIds.add(preset.id);
|
|
31
|
+
presets.push({ ...preset, effective });
|
|
21
32
|
}
|
|
22
33
|
}
|
|
23
34
|
return presets;
|
|
@@ -67,13 +78,13 @@ export function checkPresetPackage(id, { targetInput = "", home = "" } = {}) {
|
|
|
67
78
|
};
|
|
68
79
|
}
|
|
69
80
|
|
|
70
|
-
function readPresetPackageFromPath(directory) {
|
|
81
|
+
function readPresetPackageFromPath(directory, source = "local") {
|
|
71
82
|
const manifestPath = path.join(directory, "preset.yaml");
|
|
72
83
|
assertPresetDirectory(directory);
|
|
73
84
|
assertPresetManifestFile(directory, manifestPath);
|
|
74
85
|
const raw = fs.readFileSync(manifestPath, "utf8");
|
|
75
86
|
const manifest = parseSimpleYaml(raw);
|
|
76
|
-
return normalizePresetManifest(manifest, { id: normalizePresetId(manifest.id || path.basename(directory)), manifestPath, raw, source
|
|
87
|
+
return normalizePresetManifest(manifest, { id: normalizePresetId(manifest.id || path.basename(directory)), manifestPath, raw, source });
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
function assertPresetDirectory(directory) {
|
|
@@ -95,38 +106,43 @@ function assertPresetManifestFile(directory, manifestPath) {
|
|
|
95
106
|
|
|
96
107
|
export function installPresetPackage(source, { force = false, scope = "user", targetInput = ".", home = "" } = {}) {
|
|
97
108
|
if (!source) throw new Error("Missing preset source");
|
|
98
|
-
const
|
|
99
|
-
const stagedPreset = readPresetPackageFromPath(sourcePath);
|
|
100
|
-
const stagedReport = validatePresetPackage(stagedPreset);
|
|
101
|
-
if (stagedReport.failures.length) throw new Error(`Invalid preset package ${stagedPreset.id}: ${stagedReport.failures.join("; ")}`);
|
|
102
|
-
const id = stagedPreset.id;
|
|
103
|
-
if (!id) throw new Error("Preset manifest missing id");
|
|
104
|
-
const destination = scope === "project" ? projectPresetDestination(id, targetInput) : userPresetDestination(id, { home });
|
|
105
|
-
if (fs.existsSync(destination)) {
|
|
106
|
-
if (!force) throw new Error(`Preset already installed: ${id}. Re-run with --force to overwrite.`);
|
|
107
|
-
}
|
|
108
|
-
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
109
|
-
const tempDestination = path.join(path.dirname(destination), `.${id}.install-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`);
|
|
110
|
-
fs.rmSync(tempDestination, { recursive: true, force: true });
|
|
111
|
-
copyDirectory(sourcePath, tempDestination);
|
|
109
|
+
const resolvedSource = resolveInstallSource(source);
|
|
112
110
|
try {
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
manifestPath: preset.manifestRelativePath,
|
|
126
|
-
};
|
|
127
|
-
} catch (error) {
|
|
111
|
+
const sourcePath = resolvedSource.path;
|
|
112
|
+
const stagedPreset = readPresetPackageFromPath(sourcePath);
|
|
113
|
+
const stagedReport = validatePresetPackage(stagedPreset);
|
|
114
|
+
if (stagedReport.failures.length) throw new Error(`Invalid preset package ${stagedPreset.id}: ${stagedReport.failures.join("; ")}`);
|
|
115
|
+
const id = stagedPreset.id;
|
|
116
|
+
if (!id) throw new Error("Preset manifest missing id");
|
|
117
|
+
const destination = scope === "project" ? projectPresetDestination(id, targetInput) : userPresetDestination(id, { home });
|
|
118
|
+
if (fs.existsSync(destination)) {
|
|
119
|
+
if (!force) throw new Error(`Preset already installed: ${id}. Re-run with --force to overwrite.`);
|
|
120
|
+
}
|
|
121
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
122
|
+
const tempDestination = path.join(path.dirname(destination), `.${id}.install-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`);
|
|
128
123
|
fs.rmSync(tempDestination, { recursive: true, force: true });
|
|
129
|
-
|
|
124
|
+
copyDirectory(sourcePath, tempDestination);
|
|
125
|
+
try {
|
|
126
|
+
const tempPreset = readPresetPackageFromPath(tempDestination);
|
|
127
|
+
const tempReport = validatePresetPackage(tempPreset);
|
|
128
|
+
if (tempReport.failures.length) throw new Error(`Invalid preset package ${id}: ${tempReport.failures.join("; ")}`);
|
|
129
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
130
|
+
fs.renameSync(tempDestination, destination);
|
|
131
|
+
const preset = readPresetPackage(id, scope === "project" ? { targetInput, home } : { home });
|
|
132
|
+
return {
|
|
133
|
+
installed: true,
|
|
134
|
+
id: preset.id,
|
|
135
|
+
version: preset.version,
|
|
136
|
+
source: preset.source,
|
|
137
|
+
destination: toPosix(destination),
|
|
138
|
+
manifestPath: preset.manifestRelativePath,
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
fs.rmSync(tempDestination, { recursive: true, force: true });
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
} finally {
|
|
145
|
+
resolvedSource.cleanup();
|
|
130
146
|
}
|
|
131
147
|
}
|
|
132
148
|
|
|
@@ -226,7 +242,11 @@ export function validatePresetPackage(preset) {
|
|
|
226
242
|
else validatePresetPackageFile(preset, entrypoint.command, `${name} command`, failures);
|
|
227
243
|
}
|
|
228
244
|
}
|
|
229
|
-
for (const templatePath of Object.
|
|
245
|
+
for (const [templateKey, templatePath] of Object.entries(preset.newTaskTemplates)) {
|
|
246
|
+
if (!allowedNewTaskTemplateKeys.has(templateKey)) {
|
|
247
|
+
failures.push(`unsupported newTask template: ${templateKey}`);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
230
250
|
const absolute = path.join(preset.directory, templatePath);
|
|
231
251
|
if (!isInside(preset.directory, absolute)) failures.push(`template escapes preset package: ${templatePath}`);
|
|
232
252
|
else validatePresetPackageFile(preset, templatePath, "template", failures);
|
|
@@ -593,12 +613,141 @@ function presetSearchRoots({ targetInput = "", home = "" } = {}) {
|
|
|
593
613
|
|
|
594
614
|
function resolveInstallSource(source) {
|
|
595
615
|
const localPath = path.resolve(source);
|
|
596
|
-
if (fs.existsSync(path.join(localPath, "preset.yaml"))) return localPath;
|
|
616
|
+
if (fs.existsSync(path.join(localPath, "preset.yaml"))) return { path: localPath, cleanup: () => {} };
|
|
617
|
+
if (fs.existsSync(localPath) && fs.statSync(localPath).isFile()) {
|
|
618
|
+
if (!localPath.toLowerCase().endsWith(".zip")) throw new Error(`Preset source file must be a .zip archive: ${toPosix(localPath)}`);
|
|
619
|
+
return resolveZipInstallSource(localPath);
|
|
620
|
+
}
|
|
597
621
|
const builtinPath = path.join(builtinPresetRoot, normalizePresetId(source));
|
|
598
|
-
if (fs.existsSync(path.join(builtinPath, "preset.yaml"))) return builtinPath;
|
|
622
|
+
if (fs.existsSync(path.join(builtinPath, "preset.yaml"))) return { path: builtinPath, cleanup: () => {} };
|
|
599
623
|
throw new Error(`Preset source not found: ${source}`);
|
|
600
624
|
}
|
|
601
625
|
|
|
626
|
+
function resolveZipInstallSource(sourcePath) {
|
|
627
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "harness-preset-archive-"));
|
|
628
|
+
try {
|
|
629
|
+
extractPresetZip(sourcePath, tempRoot);
|
|
630
|
+
return {
|
|
631
|
+
path: presetRootFromExtractedArchive(tempRoot),
|
|
632
|
+
cleanup: () => fs.rmSync(tempRoot, { recursive: true, force: true }),
|
|
633
|
+
};
|
|
634
|
+
} catch (error) {
|
|
635
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
636
|
+
throw error;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function presetRootFromExtractedArchive(tempRoot) {
|
|
641
|
+
if (fs.existsSync(path.join(tempRoot, "preset.yaml"))) return tempRoot;
|
|
642
|
+
const children = fs.readdirSync(tempRoot, { withFileTypes: true })
|
|
643
|
+
.filter((entry) => entry.name !== "__MACOSX" && entry.name !== ".DS_Store");
|
|
644
|
+
const presetDirs = children
|
|
645
|
+
.filter((entry) => entry.isDirectory())
|
|
646
|
+
.map((entry) => path.join(tempRoot, entry.name))
|
|
647
|
+
.filter((directory) => fs.existsSync(path.join(directory, "preset.yaml")));
|
|
648
|
+
if (presetDirs.length === 1) return presetDirs[0];
|
|
649
|
+
throw new Error("Preset archive must contain preset.yaml at the archive root or inside one top-level directory.");
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function extractPresetZip(sourcePath, destinationRoot) {
|
|
653
|
+
const archiveStat = fs.statSync(sourcePath);
|
|
654
|
+
if (archiveStat.size > maxPresetArchiveBytes) throw new Error("Preset archive file is too large.");
|
|
655
|
+
const archive = fs.readFileSync(sourcePath);
|
|
656
|
+
const eocdOffset = findZipEndOfCentralDirectory(archive);
|
|
657
|
+
const entryCount = archive.readUInt16LE(eocdOffset + 10);
|
|
658
|
+
const centralSize = archive.readUInt32LE(eocdOffset + 12);
|
|
659
|
+
const centralOffset = archive.readUInt32LE(eocdOffset + 16);
|
|
660
|
+
if (entryCount === 0xffff || centralSize === 0xffffffff || centralOffset === 0xffffffff) {
|
|
661
|
+
throw new Error("Zip64 preset archives are not supported.");
|
|
662
|
+
}
|
|
663
|
+
if (entryCount > maxPresetArchiveEntries) throw new Error(`Preset archive has too many entries: ${entryCount}`);
|
|
664
|
+
if (centralOffset + centralSize > archive.length) throw new Error("Invalid preset archive central directory.");
|
|
665
|
+
const written = new Set();
|
|
666
|
+
let cursor = centralOffset;
|
|
667
|
+
let totalUncompressed = 0;
|
|
668
|
+
for (let index = 0; index < entryCount; index += 1) {
|
|
669
|
+
if (archive.readUInt32LE(cursor) !== 0x02014b50) throw new Error("Invalid preset archive central directory entry.");
|
|
670
|
+
const flags = archive.readUInt16LE(cursor + 8);
|
|
671
|
+
const method = archive.readUInt16LE(cursor + 10);
|
|
672
|
+
const compressedSize = archive.readUInt32LE(cursor + 20);
|
|
673
|
+
const uncompressedSize = archive.readUInt32LE(cursor + 24);
|
|
674
|
+
const nameLength = archive.readUInt16LE(cursor + 28);
|
|
675
|
+
const extraLength = archive.readUInt16LE(cursor + 30);
|
|
676
|
+
const commentLength = archive.readUInt16LE(cursor + 32);
|
|
677
|
+
const externalAttributes = archive.readUInt32LE(cursor + 38);
|
|
678
|
+
const localOffset = archive.readUInt32LE(cursor + 42);
|
|
679
|
+
const rawName = archive.slice(cursor + 46, cursor + 46 + nameLength).toString(flags & 0x0800 ? "utf8" : "utf8");
|
|
680
|
+
cursor += 46 + nameLength + extraLength + commentLength;
|
|
681
|
+
if (shouldSkipZipEntry(rawName)) continue;
|
|
682
|
+
if (flags & 0x0001) throw new Error(`Encrypted preset archive entries are not supported: ${rawName}`);
|
|
683
|
+
if (method !== 0 && method !== 8) throw new Error(`Unsupported preset archive compression method ${method}: ${rawName}`);
|
|
684
|
+
const mode = (externalAttributes >>> 16) & 0o170000;
|
|
685
|
+
if (mode === 0o120000) throw new Error(`Preset archive must not contain symlinks: ${rawName}`);
|
|
686
|
+
const entryName = safeZipEntryName(rawName);
|
|
687
|
+
if (!entryName) continue;
|
|
688
|
+
if (entryName.endsWith("/")) {
|
|
689
|
+
fs.mkdirSync(path.join(destinationRoot, entryName), { recursive: true });
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
if (written.has(entryName)) throw new Error(`Preset archive contains duplicate entry: ${entryName}`);
|
|
693
|
+
if (uncompressedSize > maxPresetArchiveUncompressedBytes - totalUncompressed) throw new Error("Preset archive is too large.");
|
|
694
|
+
const data = readZipEntryData(archive, { localOffset, compressedSize, uncompressedSize, method, name: entryName });
|
|
695
|
+
totalUncompressed += data.length;
|
|
696
|
+
const destination = path.resolve(destinationRoot, entryName);
|
|
697
|
+
if (!isInside(path.resolve(destinationRoot), destination)) throw new Error(`Preset archive entry escapes extraction root: ${rawName}`);
|
|
698
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
699
|
+
fs.writeFileSync(destination, data);
|
|
700
|
+
written.add(entryName);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function findZipEndOfCentralDirectory(archive) {
|
|
705
|
+
const minOffset = Math.max(0, archive.length - 22 - 65535);
|
|
706
|
+
for (let offset = archive.length - 22; offset >= minOffset; offset -= 1) {
|
|
707
|
+
if (archive.readUInt32LE(offset) === 0x06054b50) return offset;
|
|
708
|
+
}
|
|
709
|
+
throw new Error("Invalid preset zip archive: end of central directory not found.");
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function readZipEntryData(archive, { localOffset, compressedSize, uncompressedSize, method, name }) {
|
|
713
|
+
if (localOffset + 30 > archive.length || archive.readUInt32LE(localOffset) !== 0x04034b50) {
|
|
714
|
+
throw new Error(`Invalid preset archive local header: ${name}`);
|
|
715
|
+
}
|
|
716
|
+
const localNameLength = archive.readUInt16LE(localOffset + 26);
|
|
717
|
+
const localExtraLength = archive.readUInt16LE(localOffset + 28);
|
|
718
|
+
const dataStart = localOffset + 30 + localNameLength + localExtraLength;
|
|
719
|
+
const dataEnd = dataStart + compressedSize;
|
|
720
|
+
if (dataEnd > archive.length) throw new Error(`Invalid preset archive entry size: ${name}`);
|
|
721
|
+
const compressed = archive.slice(dataStart, dataEnd);
|
|
722
|
+
let data;
|
|
723
|
+
try {
|
|
724
|
+
data = method === 0 ? Buffer.from(compressed) : zlib.inflateRawSync(compressed, { maxOutputLength: uncompressedSize });
|
|
725
|
+
} catch (error) {
|
|
726
|
+
throw new Error(`Preset archive entry could not be decompressed within its declared size: ${name}`);
|
|
727
|
+
}
|
|
728
|
+
if (data.length !== uncompressedSize) throw new Error(`Preset archive entry size mismatch: ${name}`);
|
|
729
|
+
return data;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function shouldSkipZipEntry(rawName) {
|
|
733
|
+
const normalized = String(rawName || "").replace(/\\/g, "/");
|
|
734
|
+
return normalized === "__MACOSX/" || normalized.startsWith("__MACOSX/") || normalized.endsWith("/.DS_Store") || normalized === ".DS_Store";
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function safeZipEntryName(rawName) {
|
|
738
|
+
if (String(rawName).includes("\0")) throw new Error("Preset archive entry contains NUL byte.");
|
|
739
|
+
const withSlashes = String(rawName || "").replace(/\\/g, "/");
|
|
740
|
+
if (/^[A-Za-z]:/.test(withSlashes) || withSlashes.startsWith("/")) {
|
|
741
|
+
throw new Error(`Preset archive entry must be relative: ${rawName}`);
|
|
742
|
+
}
|
|
743
|
+
const normalized = path.posix.normalize(withSlashes);
|
|
744
|
+
if (normalized === "." || normalized === "") return "";
|
|
745
|
+
if (normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
|
|
746
|
+
throw new Error(`Preset archive entry escapes extraction root: ${rawName}`);
|
|
747
|
+
}
|
|
748
|
+
return withSlashes.endsWith("/") ? `${normalized}/` : normalized;
|
|
749
|
+
}
|
|
750
|
+
|
|
602
751
|
function copyDirectory(source, destination) {
|
|
603
752
|
fs.mkdirSync(destination, { recursive: true });
|
|
604
753
|
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
@@ -141,7 +141,7 @@ function assertAllowedPaths(paths) {
|
|
|
141
141
|
throw new ReviewConfirmGitGateError("Review confirmation write allowlist contains forbidden paths.", {
|
|
142
142
|
code: "git-allowlist-forbidden-path",
|
|
143
143
|
details: { disallowed },
|
|
144
|
-
recovery: ["Limit review-confirm writes to the current task
|
|
144
|
+
recovery: ["Limit review-confirm writes to the current task INDEX.md file."],
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
147
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { normalizeTarget, toPosix } from "./core-shared.mjs";
|
|
3
|
+
import { capabilityDefinitions, readCapabilityRegistry } from "./capability-registry.mjs";
|
|
4
|
+
import { summarizeGitState } from "./git-status-summary.mjs";
|
|
5
|
+
import { collectTasks, taskCutoverCounters } from "./task-scanner.mjs";
|
|
6
|
+
|
|
7
|
+
export function buildStatusData(targetInput, options = {}) {
|
|
8
|
+
const target = targetInput?.projectRoot ? targetInput : normalizeTarget(targetInput);
|
|
9
|
+
const validationMode = options.validationMode || "data-only";
|
|
10
|
+
const gitState = options.gitState || summarizeGitState(target);
|
|
11
|
+
const registry = options.capabilityState?.registry || readCapabilityRegistry(target);
|
|
12
|
+
const detected = options.capabilityState?.detected || [];
|
|
13
|
+
const capabilityWarnings = options.capabilityState?.warnings || [];
|
|
14
|
+
const failures = [...(options.failures || [])];
|
|
15
|
+
const warnings = [...(options.warnings || [])];
|
|
16
|
+
const legacy = options.legacy || { status: "skipped", code: 0, stdout: "", stderr: "" };
|
|
17
|
+
const tasks = options.tasks || collectTasks(target, {
|
|
18
|
+
requireGeneratedScaffoldProvenance: options.requireGeneratedScaffoldProvenance === true,
|
|
19
|
+
taskPlanPaths: options.taskPlanPaths,
|
|
20
|
+
closeoutContent: options.closeoutContent,
|
|
21
|
+
});
|
|
22
|
+
const briefReady = tasks.filter((task) => task.briefSource === "standalone").length;
|
|
23
|
+
const briefMissing = tasks.length - briefReady;
|
|
24
|
+
const capabilityNames = new Map(registry.capabilities.map((capability) => [capability.name, capability]));
|
|
25
|
+
for (const capability of detected) {
|
|
26
|
+
if (!capabilityNames.has(capability)) capabilityNames.set(capability, { name: capability, state: "configured" });
|
|
27
|
+
}
|
|
28
|
+
const cutoverCounters = taskCutoverCounters(tasks);
|
|
29
|
+
const fullCutoverEligible =
|
|
30
|
+
validationMode === "validated" &&
|
|
31
|
+
failures.length === 0 &&
|
|
32
|
+
warnings.length === 0 &&
|
|
33
|
+
cutoverCounters.legacyVisualOnlyCount === 0 &&
|
|
34
|
+
cutoverCounters.unknownClassificationCount === 0 &&
|
|
35
|
+
cutoverCounters.weakBriefCount === 0 &&
|
|
36
|
+
cutoverCounters.missingCanonicalVisualMapCount === 0;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
project: {
|
|
40
|
+
name: path.basename(target.projectRoot),
|
|
41
|
+
root: `TARGET:${target.docsOnly ? toPosix(path.relative(target.projectRoot, target.docsRoot)) : "."}`,
|
|
42
|
+
docsOnly: target.docsOnly,
|
|
43
|
+
},
|
|
44
|
+
schemaVersion: 2,
|
|
45
|
+
generatedAt: options.generatedAt || new Date().toISOString(),
|
|
46
|
+
mode: registry.mode,
|
|
47
|
+
checkState: {
|
|
48
|
+
status: failures.length > 0 ? "fail" : warnings.length > 0 ? "warn" : "pass",
|
|
49
|
+
validationMode,
|
|
50
|
+
failures: failures.length,
|
|
51
|
+
warnings: warnings.length,
|
|
52
|
+
details: { failures, warnings },
|
|
53
|
+
legacy,
|
|
54
|
+
},
|
|
55
|
+
git: gitState.summary,
|
|
56
|
+
summary: {
|
|
57
|
+
tasks: tasks.length,
|
|
58
|
+
briefCoverage: {
|
|
59
|
+
ready: briefReady,
|
|
60
|
+
missing: briefMissing,
|
|
61
|
+
total: tasks.length,
|
|
62
|
+
},
|
|
63
|
+
visualMapCoverage: {
|
|
64
|
+
canonical: tasks.filter((task) => task.visualMapSource === "canonical").length,
|
|
65
|
+
legacyOnly: cutoverCounters.legacyVisualOnlyCount,
|
|
66
|
+
missing: tasks.filter((task) => task.visualMapStatus === "missing").length,
|
|
67
|
+
total: tasks.length,
|
|
68
|
+
},
|
|
69
|
+
fullCutoverEligible,
|
|
70
|
+
legacyVisualOnlyCount: cutoverCounters.legacyVisualOnlyCount,
|
|
71
|
+
unknownClassificationCount: cutoverCounters.unknownClassificationCount,
|
|
72
|
+
weakBriefCount: cutoverCounters.weakBriefCount,
|
|
73
|
+
visualMapRequiredCount: cutoverCounters.visualMapRequiredCount,
|
|
74
|
+
missingCanonicalVisualMapCount: cutoverCounters.missingCanonicalVisualMapCount,
|
|
75
|
+
},
|
|
76
|
+
capabilities: [...capabilityNames.values()].map((capability) => ({
|
|
77
|
+
name: capability.name,
|
|
78
|
+
state: capability.state || "configured",
|
|
79
|
+
dependencyStatus: capabilityDefinitions[capability.name]?.dependencies.every((dependency) => capabilityNames.has(dependency))
|
|
80
|
+
? "valid"
|
|
81
|
+
: "invalid",
|
|
82
|
+
warnings: capabilityWarnings.filter((warning) => warning.includes(capability.name)),
|
|
83
|
+
})),
|
|
84
|
+
tasks,
|
|
85
|
+
handoffs: tasks.flatMap((task) => task.handoffs || []),
|
|
86
|
+
recentActivity: tasks.slice(0, 8).map((task) => ({ at: new Date().toISOString(), type: "task", summary: task.title })),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { implementationPhases } from "./phase-kind.mjs";
|
|
2
|
+
|
|
1
3
|
export function renderDashboard(status) {
|
|
2
4
|
const taskCards = status.tasks
|
|
3
5
|
.map((task) => {
|
|
4
6
|
const phases = task.phases
|
|
5
7
|
.map(
|
|
6
|
-
(phase) => `<div class="phase ${escapeHtml(phase.state)}">
|
|
7
|
-
<div class="phase-top"><strong>${escapeHtml(phase.id)}</strong><span>${phase.completion}%</span></div>
|
|
8
|
+
(phase) => `<div class="phase ${escapeHtml(phase.state)} ${escapeHtml(phase.kind || "execution")}">
|
|
9
|
+
<div class="phase-top"><strong>${escapeHtml(phase.id)}</strong><span>${escapeHtml(phase.kind || "execution")} · ${phase.completion}%</span></div>
|
|
8
10
|
<div class="phase-output">${escapeHtml(phase.output)}</div>
|
|
9
11
|
<div class="meter"><i style="width:${phase.completion}%"></i></div>
|
|
10
|
-
<div class="muted">${escapeHtml(phase.state)} · evidence ${escapeHtml(phase.evidenceStatus)}</div>
|
|
12
|
+
<div class="muted">${escapeHtml(phase.state)} · actor ${escapeHtml(phase.actor || "agent")} · evidence ${escapeHtml(phase.evidenceStatus)}</div>
|
|
13
|
+
${phase.exitCommand ? `<div class="muted">exit ${escapeHtml(phase.exitCommand)}</div>` : ""}
|
|
11
14
|
</div>`,
|
|
12
15
|
)
|
|
13
16
|
.join("");
|
|
@@ -91,7 +94,7 @@ function escapeHtml(value) {
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
function evidenceCompletion(phases) {
|
|
94
|
-
const scored = phases
|
|
97
|
+
const scored = implementationPhases(phases);
|
|
95
98
|
if (scored.length === 0) return 0;
|
|
96
99
|
const score = scored.reduce((sum, phase) => {
|
|
97
100
|
if (["present", "waived"].includes(phase.evidenceStatus)) return sum + 100;
|