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,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// @ts-nocheck
|
|
3
2
|
import fs from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -105,7 +104,7 @@ export function checkImportGraph({ repoRoot = defaultRepoRoot, expectNodes, expe
|
|
|
105
104
|
const graph = buildImportGraph({ repoRoot });
|
|
106
105
|
const violations = [];
|
|
107
106
|
for (const edge of graph.unresolvedEdges) {
|
|
108
|
-
violations.push({ code: "unresolved-local-edge"
|
|
107
|
+
violations.push({ ...edge, code: "unresolved-local-edge" });
|
|
109
108
|
}
|
|
110
109
|
for (const cycle of graph.cycles) {
|
|
111
110
|
violations.push({
|
|
@@ -115,23 +114,25 @@ export function checkImportGraph({ repoRoot = defaultRepoRoot, expectNodes, expe
|
|
|
115
114
|
});
|
|
116
115
|
}
|
|
117
116
|
for (const edge of graph.runtimeMjsToTsEdges) {
|
|
118
|
-
violations.push({ code: "mjs-imports-ts"
|
|
117
|
+
violations.push({ ...edge, code: "mjs-imports-ts" });
|
|
119
118
|
}
|
|
120
119
|
for (const edge of graph.typesValueImports) {
|
|
121
|
-
violations.push({ code: "types-value-import"
|
|
120
|
+
violations.push({ ...edge, code: "types-value-import" });
|
|
122
121
|
}
|
|
123
122
|
const barrels = graph.nodes.filter((node) => node.path === "scripts/lib/harness-core.mts" || node.path === "scripts/lib/harness-core.mjs");
|
|
124
|
-
for (const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
123
|
+
for (const barrel of barrels) {
|
|
124
|
+
for (const edge of barrel.imports || []) {
|
|
125
|
+
if (edge.kind !== "export" || !edge.target)
|
|
126
|
+
continue;
|
|
127
|
+
const target = graph.nodes.find((node) => node.path === edge.target);
|
|
128
|
+
if (!target?.barrelReachable) {
|
|
129
|
+
violations.push({
|
|
130
|
+
code: "barrel-target-not-reachable",
|
|
131
|
+
file: barrel.path,
|
|
132
|
+
target: edge.target,
|
|
133
|
+
message: `${edge.target} is exported by harness-core but is not marked barrel reachable`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
if (expectNodes !== undefined && graph.summary.fileCount !== expectNodes) {
|
|
@@ -344,6 +345,8 @@ function markReachable(nodesByPath, startPath, field) {
|
|
|
344
345
|
const seen = new Set();
|
|
345
346
|
while (stack.length > 0) {
|
|
346
347
|
const current = stack.pop();
|
|
348
|
+
if (!current)
|
|
349
|
+
continue;
|
|
347
350
|
if (seen.has(current))
|
|
348
351
|
continue;
|
|
349
352
|
seen.add(current);
|
|
@@ -385,10 +388,10 @@ function findCycles(nodesByPath) {
|
|
|
385
388
|
for (const target of adjacency(nodesByPath, file)) {
|
|
386
389
|
if (!indexByPath.has(target)) {
|
|
387
390
|
strongConnect(target);
|
|
388
|
-
lowlinkByPath.set(file, Math.min(lowlinkByPath.get(file), lowlinkByPath.get(target)));
|
|
391
|
+
lowlinkByPath.set(file, Math.min(lowlinkByPath.get(file) ?? 0, lowlinkByPath.get(target) ?? 0));
|
|
389
392
|
}
|
|
390
393
|
else if (onStack.has(target)) {
|
|
391
|
-
lowlinkByPath.set(file, Math.min(lowlinkByPath.get(file), indexByPath.get(target)));
|
|
394
|
+
lowlinkByPath.set(file, Math.min(lowlinkByPath.get(file) ?? 0, indexByPath.get(target) ?? 0));
|
|
392
395
|
}
|
|
393
396
|
}
|
|
394
397
|
if (lowlinkByPath.get(file) === indexByPath.get(file)) {
|
|
@@ -396,10 +399,12 @@ function findCycles(nodesByPath) {
|
|
|
396
399
|
let current;
|
|
397
400
|
do {
|
|
398
401
|
current = stack.pop();
|
|
402
|
+
if (!current)
|
|
403
|
+
break;
|
|
399
404
|
onStack.delete(current);
|
|
400
405
|
component.push(current);
|
|
401
406
|
} while (current !== file);
|
|
402
|
-
if (component.length > 1 || hasSelfLoop(nodesByPath, component[0]))
|
|
407
|
+
if (component.length > 1 || hasSelfLoop(nodesByPath, component[0] ?? ""))
|
|
403
408
|
cycles.push(component.sort());
|
|
404
409
|
}
|
|
405
410
|
}
|
|
@@ -407,13 +412,13 @@ function findCycles(nodesByPath) {
|
|
|
407
412
|
if (!indexByPath.has(file))
|
|
408
413
|
strongConnect(file);
|
|
409
414
|
}
|
|
410
|
-
return cycles.sort((left, right) => left[0].localeCompare(right[0]));
|
|
415
|
+
return cycles.sort((left, right) => (left[0] ?? "").localeCompare(right[0] ?? ""));
|
|
411
416
|
}
|
|
412
417
|
function assignLayers(nodesByPath, cycleNodeSet) {
|
|
413
418
|
const memo = new Map();
|
|
414
419
|
function layerFor(file, visiting = new Set()) {
|
|
415
420
|
if (memo.has(file))
|
|
416
|
-
return memo.get(file);
|
|
421
|
+
return memo.get(file) ?? null;
|
|
417
422
|
if (cycleNodeSet.has(file) || visiting.has(file)) {
|
|
418
423
|
memo.set(file, null);
|
|
419
424
|
return null;
|
|
@@ -435,7 +440,7 @@ function assignLayers(nodesByPath, cycleNodeSet) {
|
|
|
435
440
|
}
|
|
436
441
|
}
|
|
437
442
|
function adjacency(nodesByPath, file) {
|
|
438
|
-
return (nodesByPath.get(file)?.imports || []).map((imported) => imported.target).filter((target) => target && nodesByPath.has(target));
|
|
443
|
+
return (nodesByPath.get(file)?.imports || []).map((imported) => imported.target).filter((target) => Boolean(target && nodesByPath.has(target)));
|
|
439
444
|
}
|
|
440
445
|
function hasSelfLoop(nodesByPath, file) {
|
|
441
446
|
return adjacency(nodesByPath, file).includes(file);
|
|
@@ -460,17 +465,29 @@ function parseCliArgs(argv) {
|
|
|
460
465
|
args.check = true;
|
|
461
466
|
else if (arg === "--json")
|
|
462
467
|
args.json = true;
|
|
463
|
-
else if (arg === "--out")
|
|
464
|
-
args.out = argv
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
else if (arg === "--expect-
|
|
468
|
-
args.
|
|
468
|
+
else if (arg === "--out") {
|
|
469
|
+
args.out = requireValue(argv, index, arg);
|
|
470
|
+
index += 1;
|
|
471
|
+
}
|
|
472
|
+
else if (arg === "--expect-nodes") {
|
|
473
|
+
args.expectNodes = Number(requireValue(argv, index, arg));
|
|
474
|
+
index += 1;
|
|
475
|
+
}
|
|
476
|
+
else if (arg === "--expect-edges") {
|
|
477
|
+
args.expectEdges = Number(requireValue(argv, index, arg));
|
|
478
|
+
index += 1;
|
|
479
|
+
}
|
|
469
480
|
else
|
|
470
481
|
throw new Error(`Unknown argument: ${arg}`);
|
|
471
482
|
}
|
|
472
483
|
return args;
|
|
473
484
|
}
|
|
485
|
+
function requireValue(argv, index, option) {
|
|
486
|
+
const value = argv[index + 1];
|
|
487
|
+
if (!value)
|
|
488
|
+
throw new Error(`${option} requires a value`);
|
|
489
|
+
return value;
|
|
490
|
+
}
|
|
474
491
|
function writeOutput({ graph, args }) {
|
|
475
492
|
if (args.out) {
|
|
476
493
|
fs.mkdirSync(path.dirname(path.resolve(args.out)), { recursive: true });
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const defaultRepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
let repoRoot = defaultRepoRoot;
|
|
8
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
9
|
+
const arg = argv[index];
|
|
10
|
+
if (arg === "--repo-root") {
|
|
11
|
+
const value = argv[index + 1];
|
|
12
|
+
if (!value)
|
|
13
|
+
throw new Error("--repo-root requires a value");
|
|
14
|
+
repoRoot = path.resolve(value);
|
|
15
|
+
index += 1;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
19
|
+
}
|
|
20
|
+
return { repoRoot };
|
|
21
|
+
}
|
|
22
|
+
export function checkLiteForbiddenSurfaces(repoRoot = defaultRepoRoot) {
|
|
23
|
+
const forbiddenPath = path.join(repoRoot, "docs-release/architecture/document-contract-kernel/products/lite-forbidden-surfaces.txt");
|
|
24
|
+
const patterns = readForbiddenPatterns(forbiddenPath);
|
|
25
|
+
const scannedFiles = collectLiteProductFiles(repoRoot);
|
|
26
|
+
const violations = [];
|
|
27
|
+
for (const relativeFile of scannedFiles) {
|
|
28
|
+
const absoluteFile = path.join(repoRoot, relativeFile);
|
|
29
|
+
const lines = fs.readFileSync(absoluteFile, "utf8").split(/\r?\n/);
|
|
30
|
+
for (const [lineIndex, line] of lines.entries()) {
|
|
31
|
+
for (const pattern of patterns) {
|
|
32
|
+
pattern.pattern.lastIndex = 0;
|
|
33
|
+
if (pattern.pattern.test(line)) {
|
|
34
|
+
violations.push({
|
|
35
|
+
file: relativeFile,
|
|
36
|
+
line: lineIndex + 1,
|
|
37
|
+
pattern: pattern.source,
|
|
38
|
+
text: line.trim(),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { ok: violations.length === 0, violations, scannedFiles };
|
|
45
|
+
}
|
|
46
|
+
function readForbiddenPatterns(forbiddenPath) {
|
|
47
|
+
if (!fs.existsSync(forbiddenPath)) {
|
|
48
|
+
throw new Error(`Missing Lite forbidden-surface list: ${path.relative(process.cwd(), forbiddenPath)}`);
|
|
49
|
+
}
|
|
50
|
+
const lines = fs.readFileSync(forbiddenPath, "utf8").split(/\r?\n/);
|
|
51
|
+
return lines
|
|
52
|
+
.map((line) => line.trim())
|
|
53
|
+
.filter((line) => line && !line.startsWith("#"))
|
|
54
|
+
.map((line) => {
|
|
55
|
+
if (line.startsWith("literal:")) {
|
|
56
|
+
const literal = line.slice("literal:".length);
|
|
57
|
+
return { source: line, pattern: new RegExp(escapeRegExp(literal), "i") };
|
|
58
|
+
}
|
|
59
|
+
if (line.startsWith("regex:")) {
|
|
60
|
+
const source = line.slice("regex:".length);
|
|
61
|
+
return { source: line, pattern: new RegExp(source, "i") };
|
|
62
|
+
}
|
|
63
|
+
return { source: line, pattern: new RegExp(escapeRegExp(line), "i") };
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function collectLiteProductFiles(repoRoot) {
|
|
67
|
+
const files = new Set();
|
|
68
|
+
const explicitFiles = [
|
|
69
|
+
"docs-release/architecture/document-contract-kernel/products/lite-skill-overlay.md",
|
|
70
|
+
"skills/coding-agent-harness-lite/SKILL.md",
|
|
71
|
+
];
|
|
72
|
+
for (const relativeFile of explicitFiles) {
|
|
73
|
+
if (fs.existsSync(path.join(repoRoot, relativeFile)))
|
|
74
|
+
files.add(relativeFile);
|
|
75
|
+
}
|
|
76
|
+
for (const relativeDir of ["skill-sources/products/lite", "skill-sources/document-kernel/products/lite"]) {
|
|
77
|
+
const absoluteDir = path.join(repoRoot, relativeDir);
|
|
78
|
+
if (!fs.existsSync(absoluteDir))
|
|
79
|
+
continue;
|
|
80
|
+
for (const relativeFile of walkTextFiles(absoluteDir, repoRoot))
|
|
81
|
+
files.add(relativeFile);
|
|
82
|
+
}
|
|
83
|
+
return [...files].sort();
|
|
84
|
+
}
|
|
85
|
+
function walkTextFiles(current, repoRoot) {
|
|
86
|
+
const stat = fs.lstatSync(current);
|
|
87
|
+
if (stat.isSymbolicLink())
|
|
88
|
+
return [];
|
|
89
|
+
if (stat.isFile()) {
|
|
90
|
+
return /\.(md|txt|template)$/.test(current) ? [toPosix(path.relative(repoRoot, current))] : [];
|
|
91
|
+
}
|
|
92
|
+
const files = [];
|
|
93
|
+
for (const entry of fs.readdirSync(current)) {
|
|
94
|
+
files.push(...walkTextFiles(path.join(current, entry), repoRoot));
|
|
95
|
+
}
|
|
96
|
+
return files;
|
|
97
|
+
}
|
|
98
|
+
function toPosix(value) {
|
|
99
|
+
return value.split(path.sep).join("/");
|
|
100
|
+
}
|
|
101
|
+
function escapeRegExp(value) {
|
|
102
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
103
|
+
}
|
|
104
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
105
|
+
try {
|
|
106
|
+
const { repoRoot } = parseArgs(process.argv.slice(2));
|
|
107
|
+
const result = checkLiteForbiddenSurfaces(repoRoot);
|
|
108
|
+
if (!result.ok) {
|
|
109
|
+
console.error([
|
|
110
|
+
"Lite forbidden-surface check failed:",
|
|
111
|
+
...result.violations.map((violation) => `${violation.file}:${violation.line}: ${violation.pattern}: ${violation.text}`),
|
|
112
|
+
].join("\n"));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
console.log(`Lite forbidden-surface check passed (${result.scannedFiles.length} files scanned)`);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const defaultRepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
+
const sourceRoots = ["scripts", "tests"];
|
|
7
|
+
const tsNocheckPattern = new RegExp(String.raw `^\s*//\s*` + "@ts" + String.raw `-nocheck\b`);
|
|
8
|
+
export function checkNoTsNocheck({ repoRoot = defaultRepoRoot, allowlistPath = path.join(repoRoot, "scripts/ts-nocheck-allowlist.json"), } = {}) {
|
|
9
|
+
const files = collectTypeScriptFiles(repoRoot);
|
|
10
|
+
const allowlist = readAllowlist(allowlistPath);
|
|
11
|
+
const violations = [];
|
|
12
|
+
const observed = new Set();
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
const absolutePath = path.join(repoRoot, file);
|
|
15
|
+
const lines = fs.readFileSync(absolutePath, "utf8").split(/\r?\n/);
|
|
16
|
+
const lineIndex = lines.findIndex((line) => tsNocheckPattern.test(line));
|
|
17
|
+
if (lineIndex === -1)
|
|
18
|
+
continue;
|
|
19
|
+
observed.add(file);
|
|
20
|
+
if (!allowlist.has(file)) {
|
|
21
|
+
violations.push({
|
|
22
|
+
code: "unlisted-ts-nocheck",
|
|
23
|
+
file,
|
|
24
|
+
line: lineIndex + 1,
|
|
25
|
+
message: `${file}:${lineIndex + 1} has ${"@ts"}-nocheck but is not in scripts/ts-nocheck-allowlist.json`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const file of allowlist) {
|
|
30
|
+
if (!observed.has(file)) {
|
|
31
|
+
violations.push({
|
|
32
|
+
code: "stale-ts-nocheck-allowlist",
|
|
33
|
+
file,
|
|
34
|
+
message: `${file} is listed in scripts/ts-nocheck-allowlist.json but no longer has ${"@ts"}-nocheck`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { ok: violations.length === 0, violations };
|
|
39
|
+
}
|
|
40
|
+
function collectTypeScriptFiles(repoRoot) {
|
|
41
|
+
const files = [];
|
|
42
|
+
for (const root of sourceRoots) {
|
|
43
|
+
const absoluteRoot = path.join(repoRoot, root);
|
|
44
|
+
if (!fs.existsSync(absoluteRoot))
|
|
45
|
+
continue;
|
|
46
|
+
walk(absoluteRoot, files, repoRoot);
|
|
47
|
+
}
|
|
48
|
+
return files.sort();
|
|
49
|
+
}
|
|
50
|
+
function walk(current, files, repoRoot) {
|
|
51
|
+
const stat = fs.lstatSync(current);
|
|
52
|
+
if (stat.isSymbolicLink())
|
|
53
|
+
return;
|
|
54
|
+
if (stat.isDirectory()) {
|
|
55
|
+
const name = path.basename(current);
|
|
56
|
+
if (name === "node_modules" || name === ".worktrees" || name === "tmp")
|
|
57
|
+
return;
|
|
58
|
+
for (const entry of fs.readdirSync(current))
|
|
59
|
+
walk(path.join(current, entry), files, repoRoot);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (stat.isFile() && /\.(mts|ts)$/.test(current)) {
|
|
63
|
+
files.push(path.relative(repoRoot, current).split(path.sep).join("/"));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function readAllowlist(allowlistPath) {
|
|
67
|
+
if (!allowlistPath || !fs.existsSync(allowlistPath))
|
|
68
|
+
return new Set();
|
|
69
|
+
const parsed = JSON.parse(fs.readFileSync(allowlistPath, "utf8"));
|
|
70
|
+
const files = normalizeAllowlistFiles(parsed);
|
|
71
|
+
return new Set(files);
|
|
72
|
+
}
|
|
73
|
+
function normalizeAllowlistFiles(parsed) {
|
|
74
|
+
if (Array.isArray(parsed))
|
|
75
|
+
return parsed.filter((value) => typeof value === "string");
|
|
76
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
77
|
+
return [];
|
|
78
|
+
const files = parsed.files;
|
|
79
|
+
return Array.isArray(files) ? files.filter((value) => typeof value === "string") : [];
|
|
80
|
+
}
|
|
81
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
82
|
+
const result = checkNoTsNocheck();
|
|
83
|
+
if (!result.ok) {
|
|
84
|
+
console.error(result.violations.map((violation) => violation.message).join("\n"));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
console.log(`No ${"@ts"}-nocheck gate passed`);
|
|
88
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// @ts-nocheck
|
|
3
2
|
import fs from "node:fs";
|
|
4
3
|
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
@@ -75,11 +74,19 @@ export function checkRuntimeEmitContract({ projectRoot = defaultRepoRoot, config
|
|
|
75
74
|
};
|
|
76
75
|
}
|
|
77
76
|
function runTypeScriptEmit({ projectRoot, configPath, outDir }) {
|
|
78
|
-
|
|
77
|
+
const npmArgs = ["exec", "--yes", "--package", `typescript@${typescriptVersion}`, "--", "tsc", "-p", configPath, "--outDir", outDir, "--noCheck"];
|
|
78
|
+
const npmCommand = resolveNpmCommand(npmArgs);
|
|
79
|
+
return spawnSync(npmCommand.command, npmCommand.args, {
|
|
79
80
|
cwd: projectRoot,
|
|
80
81
|
encoding: "utf8",
|
|
81
82
|
});
|
|
82
83
|
}
|
|
84
|
+
function resolveNpmCommand(npmArgs) {
|
|
85
|
+
const npmExecPath = process.env.npm_execpath;
|
|
86
|
+
if (npmExecPath)
|
|
87
|
+
return { command: process.execPath, args: [npmExecPath, ...npmArgs] };
|
|
88
|
+
return { command: process.platform === "win32" ? "npm.cmd" : "npm", args: npmArgs };
|
|
89
|
+
}
|
|
83
90
|
function compareDirectories({ expectedDir, actualDir, violations }) {
|
|
84
91
|
const expectedFiles = collectFiles(expectedDir).filter((file) => file.endsWith(".mjs")).sort();
|
|
85
92
|
const actualFiles = collectFiles(actualDir).filter((file) => file.endsWith(".mjs")).sort();
|
|
@@ -129,7 +136,7 @@ function collectFiles(directory) {
|
|
|
129
136
|
walk(directory, files, () => true);
|
|
130
137
|
return files.sort();
|
|
131
138
|
}
|
|
132
|
-
function walk(current, files, predicate, sourceRoot) {
|
|
139
|
+
function walk(current, files, predicate, sourceRoot = "") {
|
|
133
140
|
const stat = fs.lstatSync(current);
|
|
134
141
|
if (stat.isSymbolicLink())
|
|
135
142
|
return;
|
|
@@ -1,31 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// @ts-nocheck
|
|
3
2
|
import fs from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
6
5
|
const defaultRepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
6
|
const sourceRoots = ["scripts", "tests"];
|
|
8
7
|
const importPattern = /\b(import|export)\s+(type\s+)?(?:[^'"]*?\s+from\s+)?["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
9
|
-
const tsEscapePattern = /@(ts-ignore|ts-expect-error)\b
|
|
10
|
-
|
|
8
|
+
const tsEscapePattern = /@(ts-ignore|ts-expect-error|ts-nocheck)\b|\bas\s+unknown\s+as\b|<[^>\n]*\bany\b[^>\n]*>|(?:^|[^A-Za-z0-9_$])(?:as\s+any|:\s*any\b)/;
|
|
9
|
+
const crossLineUnknownCastPattern = /\bas\s+unknown\s+as\b/;
|
|
10
|
+
export function checkTypeBoundaries({ repoRoot = defaultRepoRoot, escapeAllowlistPath = path.join(repoRoot, "scripts/type-escape-allowlist.json"), } = {}) {
|
|
11
11
|
const files = collectSourceFiles(repoRoot);
|
|
12
12
|
const violations = [];
|
|
13
|
+
const escapeAllowlist = readEscapeAllowlist(escapeAllowlistPath);
|
|
14
|
+
const observedEscapes = new Set();
|
|
13
15
|
for (const file of files) {
|
|
14
16
|
const absolutePath = path.join(repoRoot, file);
|
|
15
17
|
const content = fs.readFileSync(absolutePath, "utf8");
|
|
16
18
|
const imports = parseImports(content);
|
|
17
|
-
if (file.endsWith(".ts")) {
|
|
19
|
+
if (file.endsWith(".ts") || file.endsWith(".mts")) {
|
|
18
20
|
const lines = content.split(/\r?\n/);
|
|
19
21
|
for (const [index, line] of lines.entries()) {
|
|
20
22
|
if (tsEscapePattern.test(line)) {
|
|
21
|
-
|
|
23
|
+
const violation = {
|
|
22
24
|
code: "ts-escape-hatch",
|
|
23
25
|
file,
|
|
24
26
|
line: index + 1,
|
|
25
27
|
message: `${file}:${index + 1} uses a TypeScript escape hatch that requires review`,
|
|
26
|
-
}
|
|
28
|
+
};
|
|
29
|
+
observedEscapes.add(escapeKey(violation));
|
|
30
|
+
if (!isEscapeAllowed(escapeAllowlist, violation))
|
|
31
|
+
violations.push(violation);
|
|
27
32
|
}
|
|
28
33
|
}
|
|
34
|
+
if (crossLineUnknownCastPattern.test(content) && !lines.some((line) => /\bas\s+unknown\s+as\b/.test(line))) {
|
|
35
|
+
const line = lineForOffset(content, content.search(crossLineUnknownCastPattern));
|
|
36
|
+
const violation = {
|
|
37
|
+
code: "ts-escape-hatch",
|
|
38
|
+
file,
|
|
39
|
+
line,
|
|
40
|
+
message: `${file}:${line} uses a cross-line TypeScript escape hatch that requires review`,
|
|
41
|
+
};
|
|
42
|
+
observedEscapes.add(escapeKey(violation));
|
|
43
|
+
if (!isEscapeAllowed(escapeAllowlist, violation))
|
|
44
|
+
violations.push(violation);
|
|
45
|
+
}
|
|
29
46
|
}
|
|
30
47
|
for (const imported of imports) {
|
|
31
48
|
if (!isLocalSpecifier(imported.specifier))
|
|
@@ -49,6 +66,16 @@ export function checkTypeBoundaries({ repoRoot = defaultRepoRoot } = {}) {
|
|
|
49
66
|
}
|
|
50
67
|
}
|
|
51
68
|
}
|
|
69
|
+
for (const allowed of escapeAllowlist.values()) {
|
|
70
|
+
if (observedEscapes.has(allowed.key))
|
|
71
|
+
continue;
|
|
72
|
+
violations.push({
|
|
73
|
+
code: "stale-ts-escape-allowlist",
|
|
74
|
+
file: allowed.file,
|
|
75
|
+
line: allowed.line,
|
|
76
|
+
message: `${allowed.file}:${allowed.line} is listed in ${path.relative(repoRoot, escapeAllowlistPath)} but no matching TypeScript escape hatch was found`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
52
79
|
return { ok: violations.length === 0, violations };
|
|
53
80
|
}
|
|
54
81
|
function collectSourceFiles(repoRoot) {
|
|
@@ -83,7 +110,7 @@ function parseImports(content) {
|
|
|
83
110
|
imports.push({
|
|
84
111
|
kind: match[1] || "import",
|
|
85
112
|
typeOnly: match[2] === "type ",
|
|
86
|
-
specifier: match[3] || match[4],
|
|
113
|
+
specifier: match[3] || match[4] || "",
|
|
87
114
|
});
|
|
88
115
|
}
|
|
89
116
|
return imports;
|
|
@@ -127,7 +154,39 @@ function hasTypeScriptSourceExtension(filePath) {
|
|
|
127
154
|
return typeof filePath === "string" && /\.(mts|ts)$/.test(filePath);
|
|
128
155
|
}
|
|
129
156
|
function isTypeOnlyTypeScriptImport(file, imported) {
|
|
130
|
-
return file.endsWith(".ts") && imported.kind === "import" && imported.typeOnly;
|
|
157
|
+
return (file.endsWith(".ts") || file.endsWith(".mts")) && imported.kind === "import" && imported.typeOnly;
|
|
158
|
+
}
|
|
159
|
+
function readEscapeAllowlist(allowlistPath) {
|
|
160
|
+
if (!allowlistPath || !fs.existsSync(allowlistPath))
|
|
161
|
+
return new Map();
|
|
162
|
+
const parsed = JSON.parse(fs.readFileSync(allowlistPath, "utf8"));
|
|
163
|
+
const entries = Array.isArray(parsed) ? parsed : parsed.escapes || [];
|
|
164
|
+
return new Map(entries.map((entry) => {
|
|
165
|
+
if (typeof entry === "string") {
|
|
166
|
+
return [
|
|
167
|
+
entry,
|
|
168
|
+
{
|
|
169
|
+
key: entry,
|
|
170
|
+
file: entry.split(":")[0] || entry,
|
|
171
|
+
line: Number.parseInt(entry.split(":")[1] || "", 10) || undefined,
|
|
172
|
+
code: entry.split(":")[2] || "ts-escape-hatch",
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
const key = `${entry.file}:${entry.line}:${entry.code || "ts-escape-hatch"}`;
|
|
177
|
+
return [key, { key, file: entry.file, line: entry.line, code: entry.code || "ts-escape-hatch" }];
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
function isEscapeAllowed(allowlist, violation) {
|
|
181
|
+
return allowlist.has(escapeKey(violation)) || allowlist.has(`${violation.file}:${violation.line}`);
|
|
182
|
+
}
|
|
183
|
+
function escapeKey(violation) {
|
|
184
|
+
return `${violation.file}:${violation.line}:${violation.code}`;
|
|
185
|
+
}
|
|
186
|
+
function lineForOffset(content, offset) {
|
|
187
|
+
if (offset < 0)
|
|
188
|
+
return 1;
|
|
189
|
+
return content.slice(0, offset).split(/\r?\n/).length;
|
|
131
190
|
}
|
|
132
191
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
133
192
|
const result = checkTypeBoundaries();
|
|
@@ -1,9 +1,31 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
// Dashboard command parsing stays behavior-first until command handler types are modeled.
|
|
3
2
|
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { normalizeTarget, serveDashboardWorkbench, writeDashboardFolder, writeDashboardSingleFile, } from "../lib/harness-core.mjs";
|
|
6
6
|
import { dashboardWatchRoots } from "../lib/harness-paths.mjs";
|
|
7
|
+
export async function runDevDashboardCommand({ takeFlag, takeOption, targetArg }) {
|
|
8
|
+
const open = !takeFlag("--no-open");
|
|
9
|
+
const outDir = takeOption("--out-dir", "");
|
|
10
|
+
const host = takeOption("--host", "127.0.0.1");
|
|
11
|
+
const port = Number(takeOption("--port", "0"));
|
|
12
|
+
const localeOverride = takeOption("--locale", "");
|
|
13
|
+
const target = targetArg();
|
|
14
|
+
const usesDefaultOutDir = !outDir;
|
|
15
|
+
const dashboardOutDir = outDir || defaultDevOutDir(target);
|
|
16
|
+
const opts = {
|
|
17
|
+
...(localeOverride ? { localeOverride } : {}),
|
|
18
|
+
recoverGeneratedDashboard: usesDefaultOutDir,
|
|
19
|
+
replaceExistingDashboardOutput: usesDefaultOutDir,
|
|
20
|
+
};
|
|
21
|
+
try {
|
|
22
|
+
await serveDashboardWorkbench(dashboardOutDir, target, { ...opts, host, port, autoRefresh: true, open, label: "harness dev" });
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.error(errorMessage(error));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
7
29
|
export async function runDashboardCommand({ takeFlag, takeOption, targetArg }) {
|
|
8
30
|
const watch = takeFlag("--watch");
|
|
9
31
|
const workbench = takeFlag("--workbench");
|
|
@@ -19,11 +41,11 @@ export async function runDashboardCommand({ takeFlag, takeOption, targetArg }) {
|
|
|
19
41
|
process.exit(2);
|
|
20
42
|
}
|
|
21
43
|
try {
|
|
22
|
-
|
|
23
|
-
await serveDashboardWorkbench(outDir, targetArg(), { ...opts, host, port });
|
|
44
|
+
requireV2DashboardTarget(targetArg());
|
|
45
|
+
await serveDashboardWorkbench(outDir, targetArg(), { ...opts, host, port: Number(port) });
|
|
24
46
|
}
|
|
25
47
|
catch (error) {
|
|
26
|
-
console.error(error
|
|
48
|
+
console.error(errorMessage(error));
|
|
27
49
|
process.exit(1);
|
|
28
50
|
}
|
|
29
51
|
}
|
|
@@ -33,28 +55,29 @@ export async function runDashboardCommand({ takeFlag, takeOption, targetArg }) {
|
|
|
33
55
|
process.exit(2);
|
|
34
56
|
}
|
|
35
57
|
const target = targetArg();
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const watchRoots = dashboardWatchRoots(normalizedTarget.harness);
|
|
58
|
+
const harnessPaths = requireV2DashboardTarget(target);
|
|
59
|
+
const watchRoots = dashboardWatchRoots(harnessPaths);
|
|
39
60
|
const regenerate = () => {
|
|
40
61
|
try {
|
|
41
62
|
console.log(writeDashboardFolder(outDir, target, opts));
|
|
42
63
|
console.log(`dashboard regenerated: ${new Date().toISOString()}`);
|
|
43
64
|
}
|
|
44
65
|
catch (error) {
|
|
45
|
-
console.error(`dashboard regeneration failed: ${error
|
|
66
|
+
console.error(`dashboard regeneration failed: ${errorMessage(error)}`);
|
|
46
67
|
}
|
|
47
68
|
};
|
|
48
69
|
regenerate();
|
|
49
70
|
let timer = null;
|
|
50
71
|
const watchers = watchRoots.map((watchRoot) => fs.watch(watchRoot, { recursive: true }, () => {
|
|
51
|
-
|
|
72
|
+
if (timer)
|
|
73
|
+
clearTimeout(timer);
|
|
52
74
|
timer = setTimeout(regenerate, 300);
|
|
53
75
|
}));
|
|
54
76
|
const close = () => {
|
|
55
77
|
for (const watcher of watchers)
|
|
56
78
|
watcher.close();
|
|
57
|
-
|
|
79
|
+
if (timer)
|
|
80
|
+
clearTimeout(timer);
|
|
58
81
|
process.exit(0);
|
|
59
82
|
};
|
|
60
83
|
process.on("SIGINT", close);
|
|
@@ -62,7 +85,7 @@ export async function runDashboardCommand({ takeFlag, takeOption, targetArg }) {
|
|
|
62
85
|
console.log(`watching ${watchRoots.join(", ")}`);
|
|
63
86
|
await new Promise(() => { });
|
|
64
87
|
}
|
|
65
|
-
|
|
88
|
+
requireV2DashboardTarget(targetArg());
|
|
66
89
|
if (outDir) {
|
|
67
90
|
console.log(writeDashboardFolder(outDir, targetArg(), opts));
|
|
68
91
|
}
|
|
@@ -71,10 +94,25 @@ export async function runDashboardCommand({ takeFlag, takeOption, targetArg }) {
|
|
|
71
94
|
}
|
|
72
95
|
process.exit(0);
|
|
73
96
|
}
|
|
74
|
-
function
|
|
97
|
+
function requireV2DashboardTarget(target) {
|
|
75
98
|
const normalizedTarget = normalizeTarget(target);
|
|
76
|
-
|
|
77
|
-
|
|
99
|
+
const harnessPaths = readV2HarnessPaths(normalizedTarget);
|
|
100
|
+
if (harnessPaths)
|
|
101
|
+
return harnessPaths;
|
|
78
102
|
console.error("dashboard requires v2 harness structure; run `harness migrate-structure --plan` then `harness migrate-structure --apply`");
|
|
79
103
|
process.exit(1);
|
|
80
104
|
}
|
|
105
|
+
function defaultDevOutDir(targetInput) {
|
|
106
|
+
const target = path.resolve(targetInput || ".");
|
|
107
|
+
return path.join(os.tmpdir(), "coding-agent-harness-dev", `${path.basename(target) || "project"}-${Buffer.from(target).toString("hex").slice(0, 16)}`);
|
|
108
|
+
}
|
|
109
|
+
function readV2HarnessPaths(target) {
|
|
110
|
+
const harness = target.harness;
|
|
111
|
+
return isRecord(harness) && harness.version === 2 ? harness : null;
|
|
112
|
+
}
|
|
113
|
+
function isRecord(value) {
|
|
114
|
+
return typeof value === "object" && value !== null;
|
|
115
|
+
}
|
|
116
|
+
function errorMessage(error) {
|
|
117
|
+
return error instanceof Error ? error.message : String(error);
|
|
118
|
+
}
|