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.
Files changed (238) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/CONTRIBUTING.md +9 -5
  3. package/README.md +12 -2
  4. package/README.zh-CN.md +10 -2
  5. package/SKILL.md +14 -3
  6. package/dist/build-dist.mjs +32 -6
  7. package/dist/check-dist-observation.mjs +73 -28
  8. package/dist/check-harness.mjs +0 -1
  9. package/dist/check-import-graph.mjs +44 -27
  10. package/dist/check-lite-forbidden-surfaces.mjs +121 -0
  11. package/dist/check-no-ts-nocheck.mjs +88 -0
  12. package/dist/check-runtime-emit.mjs +10 -3
  13. package/dist/check-type-boundaries.mjs +67 -8
  14. package/dist/commands/dashboard-command.mjs +52 -14
  15. package/dist/commands/migration-command.mjs +18 -8
  16. package/dist/commands/module-command.mjs +142 -0
  17. package/dist/commands/preset-command.mjs +65 -4
  18. package/dist/commands/registry.mjs +483 -0
  19. package/dist/commands/task-command.mjs +111 -53
  20. package/dist/harness.mjs +6 -303
  21. package/dist/lib/capability-registry.mjs +229 -53
  22. package/dist/lib/check-module-parallel.mjs +1 -6
  23. package/dist/lib/check-profiles.mjs +39 -46
  24. package/dist/lib/check-task-contracts.mjs +6 -4
  25. package/dist/lib/command-registry.mjs +248 -0
  26. package/dist/lib/core-shared.mjs +78 -3
  27. package/dist/lib/dashboard-data.mjs +203 -22
  28. package/dist/lib/dashboard-workbench.mjs +245 -21
  29. package/dist/lib/dashboard-writer.mjs +4 -1
  30. package/dist/lib/git-status-summary.mjs +0 -1
  31. package/dist/lib/governance-index-generator.mjs +7 -5
  32. package/dist/lib/governance-sync.mjs +46 -121
  33. package/dist/lib/governance-table-boundary.mjs +1 -14
  34. package/dist/lib/harness-core.mjs +5 -1
  35. package/dist/lib/harness-paths.mjs +115 -1
  36. package/dist/lib/impact-classifier.mjs +420 -0
  37. package/dist/lib/lesson-maintenance.mjs +1 -2
  38. package/dist/lib/markdown-utils.mjs +50 -1
  39. package/dist/lib/migration-planner.mjs +31 -16
  40. package/dist/lib/migration-support.mjs +5 -4
  41. package/dist/lib/module-registry.mjs +296 -0
  42. package/dist/lib/preset-audit-contracts.mjs +24 -1
  43. package/dist/lib/preset-engine.mjs +68 -29
  44. package/dist/lib/preset-registry.mjs +374 -72
  45. package/dist/lib/preset-runner.mjs +560 -0
  46. package/dist/lib/review-confirm-git-gate.mjs +73 -19
  47. package/dist/lib/status-builder.mjs +23 -8
  48. package/dist/lib/structure-migration.mjs +6 -4
  49. package/dist/lib/subagent-authorization-audit.mjs +8 -2
  50. package/dist/lib/task-archive-eligibility.mjs +65 -0
  51. package/dist/lib/task-audit-metadata.mjs +25 -11
  52. package/dist/lib/task-audit-migration.mjs +21 -14
  53. package/dist/lib/task-discovery-contract.mjs +32 -0
  54. package/dist/lib/task-index.mjs +4 -2
  55. package/dist/lib/task-lesson-candidates.mjs +1 -2
  56. package/dist/lib/task-lesson-sedimentation.mjs +310 -9
  57. package/dist/lib/task-lifecycle/create-task-helpers.mjs +6 -3
  58. package/dist/lib/task-lifecycle/phase-sync.mjs +0 -1
  59. package/dist/lib/task-lifecycle/preset-interop.mjs +16 -0
  60. package/dist/lib/task-lifecycle/review-confirm.mjs +34 -2
  61. package/dist/lib/task-lifecycle/review-gates.mjs +12 -5
  62. package/dist/lib/task-lifecycle/review-submission.mjs +1 -2
  63. package/dist/lib/task-lifecycle/scaffold-provenance.mjs +0 -1
  64. package/dist/lib/task-lifecycle/template-files.mjs +2 -5
  65. package/dist/lib/task-lifecycle.mjs +117 -159
  66. package/dist/lib/task-metadata.mjs +10 -5
  67. package/dist/lib/task-preset-contract-drift.mjs +45 -0
  68. package/dist/lib/task-repository.mjs +192 -0
  69. package/dist/lib/task-review-model.mjs +38 -17
  70. package/dist/lib/task-scanner.mjs +75 -23
  71. package/dist/lib/task-template-materials.mjs +131 -0
  72. package/dist/lib/task-tombstone-commands.mjs +187 -18
  73. package/dist/lib/types/check-profiles.js +1 -0
  74. package/dist/lib/types/impact.js +1 -0
  75. package/dist/lib/types/preset.js +1 -0
  76. package/dist/lib/types/task-lifecycle.js +1 -0
  77. package/dist/lib/types/task-scanner.js +1 -0
  78. package/dist/postinstall.mjs +2 -2
  79. package/dist/run-built-tests.mjs +10 -3
  80. package/docs-release/README.md +2 -1
  81. package/docs-release/architecture/document-contract-kernel/README.md +150 -0
  82. package/docs-release/architecture/document-contract-kernel/products/full-skill-overlay.md +29 -0
  83. package/docs-release/architecture/document-contract-kernel/products/lite-forbidden-surfaces.txt +26 -0
  84. package/docs-release/architecture/document-contract-kernel/products/lite-skill-overlay.md +37 -0
  85. package/docs-release/architecture/overview.md +2 -2
  86. package/docs-release/architecture/overview.zh-CN.md +2 -2
  87. package/docs-release/architecture/system-explainer/01-system-overview.md +11 -7
  88. package/docs-release/architecture/system-explainer/02-module-dependency.md +4 -4
  89. package/docs-release/architecture/system-explainer/03-task-lifecycle.md +17 -12
  90. package/docs-release/architecture/system-explainer/05-data-flow.md +6 -6
  91. package/docs-release/architecture/system-explainer/06-preset-and-migration.md +2 -2
  92. package/docs-release/architecture/system-explainer/README.md +1 -1
  93. package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +12 -8
  94. package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +5 -5
  95. package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +19 -11
  96. package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +5 -5
  97. package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +2 -2
  98. package/docs-release/architecture/system-explainer/en-US/README.md +1 -1
  99. package/docs-release/guides/agent-installation.en-US.md +4 -6
  100. package/docs-release/guides/agent-installation.md +11 -8
  101. package/docs-release/guides/contributing.md +10 -3
  102. package/docs-release/guides/contributing.zh-CN.md +10 -3
  103. package/docs-release/guides/legacy-migration-agent-prompt.md +1 -1
  104. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +1 -1
  105. package/docs-release/guides/migration-playbook.en-US.md +9 -6
  106. package/docs-release/guides/migration-playbook.md +9 -6
  107. package/docs-release/guides/preset-development.md +68 -2
  108. package/docs-release/guides/task-state-machine.en-US.md +8 -8
  109. package/docs-release/guides/task-state-machine.md +7 -7
  110. package/docs-release/guides/typescript-runtime-migration-closeout.md +17 -13
  111. package/package.json +19 -11
  112. package/postinstall.mjs +37 -0
  113. package/presets/legacy-migration/preset.yaml +5 -5
  114. package/presets/legacy-migration/templates/execution_strategy.append.md +1 -1
  115. package/presets/lesson-sedimentation/preset.yaml +3 -3
  116. package/presets/module/preset.yaml +2 -2
  117. package/presets/module/templates/execution_strategy.append.md +1 -1
  118. package/presets/module/templates/task_plan.append.md +3 -3
  119. package/presets/release-closeout/checks/check-release-package.mjs +29 -0
  120. package/presets/release-closeout/preset.yaml +100 -0
  121. package/presets/release-closeout/scripts/generate-release-package.mjs +572 -0
  122. package/presets/release-closeout/templates/execution_strategy.append.md +7 -0
  123. package/presets/release-closeout/templates/findings.seed.md +5 -0
  124. package/presets/release-closeout/templates/review.seed.md +3 -0
  125. package/presets/release-closeout/templates/task_plan.append.md +24 -0
  126. package/presets/standard-task/preset.yaml +2 -2
  127. package/references/agents-md-pattern.md +23 -17
  128. package/references/lessons-governance.md +2 -2
  129. package/references/module-parallel-standard.md +3 -6
  130. package/references/pull-request-standard.md +2 -2
  131. package/references/ssot-governance.md +2 -2
  132. package/references/taskr-gap-analysis.md +3 -3
  133. package/run-dist.mjs +34 -0
  134. package/skills/preset-creator/SKILL.md +40 -8
  135. package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -8
  136. package/skills/preset-creator/references/preset-package-skeleton.md +15 -5
  137. package/skills/preset-creator/references/structure-aware-paths.md +112 -0
  138. package/templates/AGENTS.md.template +28 -26
  139. package/templates/architecture/README.md +2 -2
  140. package/templates/architecture/service-catalog.md +2 -2
  141. package/templates/architecture/services/service-template.md +1 -1
  142. package/templates/dashboard/assets/app-src/00-state.js +5 -1
  143. package/templates/dashboard/assets/app-src/10-router.js +7 -0
  144. package/templates/dashboard/assets/app-src/20-overview.js +8 -8
  145. package/templates/dashboard/assets/app-src/30-tasks.js +132 -40
  146. package/templates/dashboard/assets/app-src/32-task-swimlane.js +314 -0
  147. package/templates/dashboard/assets/app-src/35-task-detail.js +35 -5
  148. package/templates/dashboard/assets/app-src/40-modules.js +257 -41
  149. package/templates/dashboard/assets/app-src/45-review.js +127 -1
  150. package/templates/dashboard/assets/app-src/90-bindings.js +185 -2
  151. package/templates/dashboard/assets/app.css +928 -53
  152. package/templates/dashboard/assets/app.css.manifest.json +2 -0
  153. package/templates/dashboard/assets/app.js +1071 -98
  154. package/templates/dashboard/assets/app.manifest.json +1 -0
  155. package/templates/dashboard/assets/css-src/00-foundation.css +12 -6
  156. package/templates/dashboard/assets/css-src/10-panels-flow.css +2 -2
  157. package/templates/dashboard/assets/css-src/30-task-index.css +21 -13
  158. package/templates/dashboard/assets/css-src/31-archive.css +94 -0
  159. package/templates/dashboard/assets/css-src/32-task-swimlane.css +487 -0
  160. package/templates/dashboard/assets/css-src/35-review-workspace.css +78 -0
  161. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +191 -14
  162. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +23 -0
  163. package/templates/dashboard/assets/i18n.js +166 -2
  164. package/templates/development/README.md +9 -9
  165. package/templates/development/cross-repo-debugging.md +3 -3
  166. package/templates/development/external-context/service-template.md +1 -1
  167. package/templates/development/external-source-packs/README.md +2 -2
  168. package/templates/integrations/README.md +4 -4
  169. package/templates/integrations/api-contract.md +1 -1
  170. package/templates/integrations/event-contract.md +1 -1
  171. package/templates/integrations/third-party/vendor-template.md +1 -1
  172. package/templates/integrations/webhook-contract.md +1 -1
  173. package/templates/ledger/Harness-Ledger.md +1 -1
  174. package/templates/modules/module_brief.md +50 -0
  175. package/templates/modules/module_plan.md +49 -0
  176. package/templates/modules/registry_view.md +9 -0
  177. package/templates/modules/session_prompt_pack.md +55 -0
  178. package/templates/planning/brief.md +32 -8
  179. package/templates/planning/module_brief.md +28 -3
  180. package/templates/planning/module_plan.md +26 -11
  181. package/templates/planning/module_session_prompt.md +11 -2
  182. package/templates/planning/optional/slices/_slice-template/brief.md +28 -0
  183. package/templates/planning/review.md +1 -1
  184. package/templates/planning/visual_map.md +1 -1
  185. package/templates/reference/docs-library-standard.md +7 -7
  186. package/templates/reference/execution-workflow-standard.md +13 -0
  187. package/templates/reference/external-source-intake-standard.md +10 -10
  188. package/templates/reference/pull-request-standard.md +2 -2
  189. package/templates/reference/repo-governance-standard.md +1 -1
  190. package/templates/reference/review-routing-standard.md +4 -0
  191. package/templates/ssot/Module-Registry.md +4 -38
  192. package/templates/walkthrough/walkthrough-template.md +1 -1
  193. package/templates-zh-CN/AGENTS.md.template +27 -25
  194. package/templates-zh-CN/CLAUDE.md.template +1 -1
  195. package/templates-zh-CN/architecture/README.md +2 -2
  196. package/templates-zh-CN/architecture/service-catalog.md +2 -2
  197. package/templates-zh-CN/architecture/services/service-template.md +1 -1
  198. package/templates-zh-CN/development/README.md +9 -9
  199. package/templates-zh-CN/development/cross-repo-debugging.md +3 -3
  200. package/templates-zh-CN/development/external-context/service-template.md +1 -1
  201. package/templates-zh-CN/development/external-source-packs/README.md +2 -2
  202. package/templates-zh-CN/integrations/README.md +4 -4
  203. package/templates-zh-CN/integrations/api-contract.md +1 -1
  204. package/templates-zh-CN/integrations/event-contract.md +1 -1
  205. package/templates-zh-CN/integrations/third-party/vendor-template.md +1 -1
  206. package/templates-zh-CN/integrations/webhook-contract.md +1 -1
  207. package/templates-zh-CN/ledger/Harness-Ledger.md +1 -1
  208. package/templates-zh-CN/lessons/lesson-arch-process-change.md +1 -1
  209. package/templates-zh-CN/lessons/lesson-new-doc.md +3 -3
  210. package/templates-zh-CN/lessons/lesson-ref-change.md +4 -4
  211. package/templates-zh-CN/modules/module_brief.md +47 -0
  212. package/templates-zh-CN/modules/module_plan.md +48 -0
  213. package/templates-zh-CN/modules/registry_view.md +9 -0
  214. package/templates-zh-CN/modules/session_prompt_pack.md +50 -0
  215. package/templates-zh-CN/planning/INDEX.md +1 -0
  216. package/templates-zh-CN/planning/brief.md +26 -7
  217. package/templates-zh-CN/planning/module_brief.md +24 -2
  218. package/templates-zh-CN/planning/module_plan.md +35 -29
  219. package/templates-zh-CN/planning/module_session_prompt.md +15 -11
  220. package/templates-zh-CN/planning/optional/slices/_slice-template/brief.md +28 -11
  221. package/templates-zh-CN/planning/review.md +1 -1
  222. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  223. package/templates-zh-CN/reference/delivery-operating-model-standard.md +3 -3
  224. package/templates-zh-CN/reference/docs-library-standard.md +27 -27
  225. package/templates-zh-CN/reference/execution-workflow-standard.md +12 -2
  226. package/templates-zh-CN/reference/external-source-intake-standard.md +10 -10
  227. package/templates-zh-CN/reference/harness-ledger-standard.md +3 -3
  228. package/templates-zh-CN/reference/pull-request-standard.md +1 -1
  229. package/templates-zh-CN/reference/regression-ssot-governance.md +2 -2
  230. package/templates-zh-CN/reference/repo-governance-standard.md +1 -1
  231. package/templates-zh-CN/reference/review-routing-standard.md +3 -0
  232. package/templates-zh-CN/reference/walkthrough-standard.md +2 -2
  233. package/templates-zh-CN/reference/worktree-standard.md +1 -1
  234. package/templates-zh-CN/regression/Cadence-Ledger.md +2 -2
  235. package/templates-zh-CN/ssot/Delivery-SSoT.md +2 -2
  236. package/templates-zh-CN/ssot/Module-Registry.md +5 -44
  237. package/templates-zh-CN/ssot/Regression-SSoT.md +2 -2
  238. 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", ...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", ...edge });
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", ...edge });
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 edge of barrels.flatMap((barrel) => 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
- });
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[++index];
465
- else if (arg === "--expect-nodes")
466
- args.expectNodes = Number(argv[++index]);
467
- else if (arg === "--expect-edges")
468
- args.expectEdges = Number(argv[++index]);
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
- return spawnSync("npm", ["exec", "--yes", "--package", `typescript@${typescriptVersion}`, "--", "tsc", "-p", configPath, "--outDir", outDir, "--noCheck"], {
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|(?:^|[^A-Za-z0-9_$])(?:as\s+any|:\s*any\b)/;
10
- export function checkTypeBoundaries({ repoRoot = defaultRepoRoot } = {}) {
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
- violations.push({
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
- assertV2DashboardTarget(targetArg());
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.message);
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
- assertV2DashboardTarget(target);
37
- const normalizedTarget = normalizeTarget(target);
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.message}`);
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
- clearTimeout(timer);
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
- clearTimeout(timer);
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
- assertV2DashboardTarget(targetArg());
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 assertV2DashboardTarget(target) {
97
+ function requireV2DashboardTarget(target) {
75
98
  const normalizedTarget = normalizeTarget(target);
76
- if (normalizedTarget.harness?.version === 2)
77
- return;
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
+ }